@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
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-hooks-runtime.sh — hooks del arnés como cliente del runtime [EP-OR-08-B].
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
5
|
+
fail=0
|
|
6
|
+
TMP="$(mktemp -d)"
|
|
7
|
+
PROJ="$TMP/proj"
|
|
8
|
+
mkdir -p "$PROJ/.claude/state" "$PROJ/.claude/config"
|
|
9
|
+
export CLAUDE_PROJECT_DIR="$PROJ"
|
|
10
|
+
export BUILD_CONFIG_FILE="$PROJ/.claude/config/build-config.json"
|
|
11
|
+
echo '{"runtime":{"mode":"runtime","stale_seconds":900},"context":{"warning_pct":35,"critical_pct":25,"stale_seconds":60,"auto_checkpoint":false}}' > "$BUILD_CONFIG_FILE"
|
|
12
|
+
# El daemon de heartbeat puede quedar vivo si un test falla a mitad: se apaga en el trap.
|
|
13
|
+
# (heartbeat.sh nace en la Task 5; hasta entonces el trap simplemente no encuentra el script
|
|
14
|
+
# y el error queda silenciado — nunca ensucia la salida del test.)
|
|
15
|
+
cleanup() {
|
|
16
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
17
|
+
rm -rf "$TMP"
|
|
18
|
+
}
|
|
19
|
+
trap cleanup EXIT
|
|
20
|
+
|
|
21
|
+
source "$ROOT/hooks/build/lib/projection.sh" 2>/dev/null || { echo "FAIL no existe lib/projection.sh"; exit 1; }
|
|
22
|
+
|
|
23
|
+
# --- lib/projection.sh ---
|
|
24
|
+
got="$(projection_get active_slice.phase sin-cache)"
|
|
25
|
+
[ "$got" = "sin-cache" ] && echo "OK projection_get sin cache -> default" || { echo "FAIL projection_get sin cache ($got)"; fail=1; }
|
|
26
|
+
|
|
27
|
+
runtime_projection_write '{"fetched_at":"2026-08-19T10:00:00Z","project":{"scaffold_confirmed":true,"design_source_applies":false},"active_slice":{"slice_id":"sl-1","phase":"green","gates":{"dor":true,"tdd":false},"wiring_failing":[{"item_id":"w1","kind":"e2e"}]},"nudges":[]}'
|
|
28
|
+
|
|
29
|
+
got="$(projection_get active_slice.phase "")"
|
|
30
|
+
[ "$got" = green ] && echo "OK projection_get escalar" || { echo "FAIL projection_get escalar ($got)"; fail=1; }
|
|
31
|
+
got="$(projection_get project.scaffold_confirmed false)"
|
|
32
|
+
[ "$got" = true ] && echo "OK projection_get bool en minusculas" || { echo "FAIL projection_get bool ($got)"; fail=1; }
|
|
33
|
+
got="$(projection_get active_slice.gates '{}')"
|
|
34
|
+
echo "$got" | grep -q '"tdd": *false' && echo "OK projection_get subarbol como JSON" || { echo "FAIL projection_get subarbol ($got)"; fail=1; }
|
|
35
|
+
got="$(projection_get active_slice.no_existe fallback)"
|
|
36
|
+
[ "$got" = fallback ] && echo "OK projection_get clave ausente -> default" || { echo "FAIL projection_get ausente ($got)"; fail=1; }
|
|
37
|
+
|
|
38
|
+
# python3 ausente: PATH vacío -> default, jamás excepción (config_root usa CLAUDE_PROJECT_DIR,
|
|
39
|
+
# así que no necesita git en el PATH para resolver la ruta).
|
|
40
|
+
got="$(PATH=/var/empty projection_get active_slice.phase degradado)"
|
|
41
|
+
[ "$got" = degradado ] && echo "OK projection_get sin python3 -> default" || { echo "FAIL projection_get sin python3 ($got)"; fail=1; }
|
|
42
|
+
|
|
43
|
+
# Frescura: la caché escrita arriba tiene fetched_at de 2026-08-19 (fija) -> con
|
|
44
|
+
# stale_seconds=900 el veredicto depende del reloj; se re-escribe con la hora actual
|
|
45
|
+
# para una comprobación determinista de "fresca", y con una vieja para "stale".
|
|
46
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"active_slice\":null,\"nudges\":[]}"
|
|
47
|
+
if projection_is_stale; then echo "FAIL caché recién escrita se declara stale"; fail=1; else echo "OK caché fresca no es stale"; fi
|
|
48
|
+
runtime_projection_write '{"fetched_at":"2000-01-01T00:00:00Z","active_slice":null,"nudges":[]}'
|
|
49
|
+
if projection_is_stale; then echo "OK caché vieja es stale"; else echo "FAIL caché vieja no se declara stale"; fail=1; fi
|
|
50
|
+
age="$(projection_age_seconds)"
|
|
51
|
+
[ "$age" -gt 900 ] && echo "OK projection_age_seconds" || { echo "FAIL projection_age_seconds ($age)"; fail=1; }
|
|
52
|
+
|
|
53
|
+
# --- lib/agent-context.sh: normalización tolerante ---
|
|
54
|
+
source "$ROOT/hooks/build/lib/agent-context.sh" 2>/dev/null || { echo "FAIL no existe lib/agent-context.sh"; fail=1; }
|
|
55
|
+
|
|
56
|
+
cat > "$TMP/raw1.json" <<'JSON'
|
|
57
|
+
{"project":{"scaffold":{"confirmed":true},"design_source":{"applies":true,"confirmed":false},
|
|
58
|
+
"project_kind":"greenfield","harness_phase":"active"},
|
|
59
|
+
"active_slice":{"slice_id":"sl-7","epic_code":"EP-003","epic_title":"Cobros","stories":["HU-011","HU-012"],
|
|
60
|
+
"phase":"green","gates":{"dor":true,"tdd":false},"branch":"feature/cobros",
|
|
61
|
+
"openspec_change":"add-cobros",
|
|
62
|
+
"wiring_checklist":[{"item_id":"w1","kind":"e2e","status":"failing"},
|
|
63
|
+
{"item_id":"w2","kind":"api","status":"passing"}],
|
|
64
|
+
"sub_slices":[{"id":"ss-1","status":"done"},{"id":"ss-2","status":"pending"}],
|
|
65
|
+
"session_continuity":{"resume_hint":"cablear w1"}},
|
|
66
|
+
"nudges":[{"kind":"reflect","message":"3 slices sin reflexionar"},"recordatorio suelto"],
|
|
67
|
+
"context":{"manifest_hash":"sha256:aaa","version":14},
|
|
68
|
+
"lease":{"expires_at":"2026-08-19T12:00:00Z","ttl_s":300}}
|
|
69
|
+
JSON
|
|
70
|
+
norm="$(agent_context_normalize "$TMP/raw1.json")"
|
|
71
|
+
echo "$norm" | python3 -c '
|
|
72
|
+
import json,sys
|
|
73
|
+
d=json.load(sys.stdin)
|
|
74
|
+
ok=True
|
|
75
|
+
ok &= d["project"]["scaffold_confirmed"] is True
|
|
76
|
+
ok &= d["project"]["design_source_applies"] is True
|
|
77
|
+
ok &= d["project"]["design_source_confirmed"] is False
|
|
78
|
+
ok &= d["active_slice"]["slice_id"]=="sl-7"
|
|
79
|
+
ok &= d["active_slice"]["epic_code"]=="EP-003"
|
|
80
|
+
ok &= d["active_slice"]["hus"]==["HU-011","HU-012"]
|
|
81
|
+
ok &= d["active_slice"]["wiring_failing"]==[{"item_id":"w1","kind":"e2e"}]
|
|
82
|
+
ok &= d["active_slice"]["sub_slices_pending"]==["ss-2"]
|
|
83
|
+
ok &= d["active_slice"]["resume_hint"]=="cablear w1"
|
|
84
|
+
ok &= d["context"]["manifest_hash"]=="sha256:aaa"
|
|
85
|
+
ok &= d["lease"]["ttl_s"]==300
|
|
86
|
+
ok &= len(d["nudges"])==2 and d["nudges"][1]["kind"]=="info"
|
|
87
|
+
ok &= "fetched_at" in d
|
|
88
|
+
sys.exit(0 if ok else 1)' \
|
|
89
|
+
&& echo "OK agent_context_normalize mapea el contrato completo" \
|
|
90
|
+
|| { echo "FAIL agent_context_normalize ($norm)"; fail=1; }
|
|
91
|
+
|
|
92
|
+
# Respuesta pobre/desconocida: nunca lanza, produce el esqueleto con defaults seguros.
|
|
93
|
+
echo '{"unknown":true}' > "$TMP/raw2.json"
|
|
94
|
+
norm2="$(agent_context_normalize "$TMP/raw2.json")"
|
|
95
|
+
echo "$norm2" | python3 -c '
|
|
96
|
+
import json,sys
|
|
97
|
+
d=json.load(sys.stdin)
|
|
98
|
+
sys.exit(0 if d["active_slice"] is None and d["project"]["scaffold_confirmed"] is False and d["nudges"]==[] else 1)' \
|
|
99
|
+
&& echo "OK agent_context_normalize tolera respuesta desconocida" \
|
|
100
|
+
|| { echo "FAIL normalize respuesta desconocida ($norm2)"; fail=1; }
|
|
101
|
+
|
|
102
|
+
# JSON corrupto: degrada a {} y rc 1 (no solo el stdout: el rc debe distinguir
|
|
103
|
+
# "parseo OK con datos vacíos" de "entrada corrupta" para quien consuma la función
|
|
104
|
+
# directamente, fuera de agent_context_fetch_and_cache).
|
|
105
|
+
printf 'no-json' > "$TMP/raw3.json"
|
|
106
|
+
norm3="$(agent_context_normalize "$TMP/raw3.json")"
|
|
107
|
+
rc3=$?
|
|
108
|
+
[ "$norm3" = "{}" ] && echo "OK agent_context_normalize JSON corrupto -> {}" || { echo "FAIL normalize corrupto ($norm3)"; fail=1; }
|
|
109
|
+
[ "$rc3" -eq 1 ] && echo "OK agent_context_normalize JSON corrupto -> rc 1" || { echo "FAIL normalize corrupto rc ($rc3)"; fail=1; }
|
|
110
|
+
|
|
111
|
+
# --- agent_context_fetch_and_cache contra el stub ---
|
|
112
|
+
PORT_AC="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
113
|
+
cat > "$TMP/routes-ac.json" <<JSON
|
|
114
|
+
{"GET /agent/context": {"status": 200, "body": {"active_slice": {"slice_id": "sl-9", "phase": "red", "gates": {"dor": true}}, "context": {"manifest_hash": "sha256:bbb", "version": 3}}}}
|
|
115
|
+
JSON
|
|
116
|
+
: > "$TMP/calls-ac.jsonl"
|
|
117
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT_AC" "$TMP/routes-ac.json" "$TMP/calls-ac.jsonl" &
|
|
118
|
+
STUB_AC=$!
|
|
119
|
+
sleep 0.3
|
|
120
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT_AC\",\"project_token\":\"tok-b\",\"project_id\":\"p-1\",\"agent_key\":\"ak-1\"}"
|
|
121
|
+
|
|
122
|
+
agent_context_fetch_and_cache
|
|
123
|
+
rc=$?
|
|
124
|
+
[ $rc -eq 0 ] && echo "OK fetch_and_cache rc 0" || { echo "FAIL fetch_and_cache rc ($rc)"; fail=1; }
|
|
125
|
+
got="$(projection_get active_slice.slice_id "")"
|
|
126
|
+
[ "$got" = "sl-9" ] && echo "OK fetch_and_cache escribe la cache" || { echo "FAIL cache tras fetch ($got)"; fail=1; }
|
|
127
|
+
if projection_is_stale; then echo "FAIL cache recien traida es stale"; fail=1; else echo "OK cache recien traida es fresca"; fi
|
|
128
|
+
|
|
129
|
+
kill "$STUB_AC" 2>/dev/null; wait "$STUB_AC" 2>/dev/null
|
|
130
|
+
# Servidor caído: rc 1 y la caché ANTERIOR intacta (nunca se pisa con vacío).
|
|
131
|
+
agent_context_fetch_and_cache
|
|
132
|
+
rc=$?
|
|
133
|
+
[ $rc -eq 1 ] && echo "OK fetch_and_cache offline rc 1" || { echo "FAIL fetch offline rc ($rc)"; fail=1; }
|
|
134
|
+
got="$(projection_get active_slice.slice_id "")"
|
|
135
|
+
[ "$got" = "sl-9" ] && echo "OK fetch offline preserva la cache previa" || { echo "FAIL cache pisada offline ($got)"; fail=1; }
|
|
136
|
+
|
|
137
|
+
# --- context-sync.sh ---
|
|
138
|
+
PORT_CS="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
139
|
+
python3 - "$TMP/routes-cs.json" <<'PY'
|
|
140
|
+
import json,sys
|
|
141
|
+
allow={"allow":["react","vite"]}
|
|
142
|
+
policy={"branch_prefixes":["feature","fix","chore"],"protected_branches":["main"]}
|
|
143
|
+
routes={
|
|
144
|
+
"GET /projects/p-1/context/manifest": {"status":200,"body":{
|
|
145
|
+
"manifest_hash":"sha256:v2",
|
|
146
|
+
"files":[
|
|
147
|
+
{"path":"config/stack-allowlist.json","sha256":"h1","type_key":"core.library_allowlist","version":7},
|
|
148
|
+
{"path":"config/policy.json","sha256":"h2","type_key":"core.policy","version":3},
|
|
149
|
+
{"path":"../../../etc/evil.json","sha256":"h3","type_key":"core.policy","version":1},
|
|
150
|
+
{"path":"CLAUDE.md","sha256":"h4","type_key":"core.claude_md","version":2}]}},
|
|
151
|
+
"GET /projects/p-1/context/files?path=config%2Fstack-allowlist.json&sha256=h1": {"status":200,"body":{
|
|
152
|
+
"path":"config/stack-allowlist.json","sha256":"h1","content":json.dumps(allow)}},
|
|
153
|
+
"GET /projects/p-1/context/files?path=config%2Fpolicy.json&sha256=h2": {"status":200,"body":{
|
|
154
|
+
"path":"config/policy.json","sha256":"h2","content":json.dumps(policy)}},
|
|
155
|
+
"POST /context/synced": {"status":200,"body":{"ok":True}},
|
|
156
|
+
}
|
|
157
|
+
json.dump(routes,open(sys.argv[1],"w"))
|
|
158
|
+
PY
|
|
159
|
+
: > "$TMP/calls-cs.jsonl"
|
|
160
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT_CS" "$TMP/routes-cs.json" "$TMP/calls-cs.jsonl" &
|
|
161
|
+
STUB_CS=$!
|
|
162
|
+
sleep 0.3
|
|
163
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT_CS\",\"project_token\":\"tok-c\",\"project_id\":\"p-1\",\"manifest_hash\":\"sha256:v2\"}"
|
|
164
|
+
|
|
165
|
+
out="$(bash "$ROOT/hooks/build/context-sync.sh" 2>&1)"
|
|
166
|
+
[ -f "$PROJ/.claude/config/stack-allowlist.json" ] && echo "OK context-sync escribe allowlist" || { echo "FAIL allowlist no escrita ($out)"; fail=1; }
|
|
167
|
+
[ -f "$PROJ/.claude/config/policy.json" ] && echo "OK context-sync escribe policy" || { echo "FAIL policy no escrita"; fail=1; }
|
|
168
|
+
got="$(python3 -c "import json;print(json.load(open('$PROJ/.claude/config/stack-allowlist.json'))['allow'][0])")"
|
|
169
|
+
[ "$got" = react ] && echo "OK contenido sincronizado" || { echo "FAIL contenido ($got)"; fail=1; }
|
|
170
|
+
[ ! -e "$TMP/etc/evil.json" ] && [ ! -e "/etc/evil.json" ] && echo "OK context-sync rechaza traversal" || { echo "FAIL traversal aplicado"; fail=1; }
|
|
171
|
+
echo "$out" | grep -q 'CLAUDE.md' && echo "OK context-sync lista lo no aplicado" || { echo "FAIL no reporta destinos no gobernados ($out)"; fail=1; }
|
|
172
|
+
got="$(runtime_lock_manifest_hash)"
|
|
173
|
+
[ "$got" = "sha256:v2" ] && echo "OK context-sync actualiza el lock" || { echo "FAIL lock ($got)"; fail=1; }
|
|
174
|
+
sleep 0.2
|
|
175
|
+
synced="$(grep -c '"method": "POST".*"/context/synced"' "$TMP/calls-cs.jsonl" 2>/dev/null | tr -d ' ')"
|
|
176
|
+
if [ -z "$synced" ]; then synced=0; fi
|
|
177
|
+
[ "$synced" -ge 1 ] && echo "OK context-sync emite context/synced" || { echo "FAIL sin context/synced"; fail=1; }
|
|
178
|
+
|
|
179
|
+
# Segunda corrida: hash igual -> no-op, ni una sola llamada más al stub.
|
|
180
|
+
before="$(wc -l < "$TMP/calls-cs.jsonl" | tr -d ' ')"
|
|
181
|
+
bash "$ROOT/hooks/build/context-sync.sh" >/dev/null 2>&1
|
|
182
|
+
after="$(wc -l < "$TMP/calls-cs.jsonl" | tr -d ' ')"
|
|
183
|
+
[ "$before" = "$after" ] && echo "OK context-sync no-op con hash igual" || { echo "FAIL context-sync re-sincroniza ($before -> $after)"; fail=1; }
|
|
184
|
+
|
|
185
|
+
# legacy: no-op absoluto aunque el hash difiera.
|
|
186
|
+
runtime_credentials_merge '{"manifest_hash":"sha256:v3"}'
|
|
187
|
+
before="$(wc -l < "$TMP/calls-cs.jsonl" | tr -d ' ')"
|
|
188
|
+
TRYCORE_RUNTIME_MODE=legacy bash "$ROOT/hooks/build/context-sync.sh" >/dev/null 2>&1
|
|
189
|
+
after="$(wc -l < "$TMP/calls-cs.jsonl" | tr -d ' ')"
|
|
190
|
+
[ "$before" = "$after" ] && echo "OK context-sync no-op en legacy" || { echo "FAIL context-sync corrio en legacy"; fail=1; }
|
|
191
|
+
|
|
192
|
+
# Regresión: manifiesto servido con status 200 pero body no-objeto (JSON válido, p.ej. un
|
|
193
|
+
# array) — el lock existente NO debe corromperse ni emitirse context/synced con hash vacío.
|
|
194
|
+
python3 - "$TMP/routes-cs.json" <<'PY'
|
|
195
|
+
import json,sys
|
|
196
|
+
routes=json.load(open(sys.argv[1]))
|
|
197
|
+
routes["GET /projects/p-1/context/manifest"]={"status":200,"body":["not","an","object"]}
|
|
198
|
+
json.dump(routes,open(sys.argv[1],"w"))
|
|
199
|
+
PY
|
|
200
|
+
before_lock="$(runtime_lock_manifest_hash)"
|
|
201
|
+
bash "$ROOT/hooks/build/context-sync.sh" >/dev/null 2>&1
|
|
202
|
+
rc=$?
|
|
203
|
+
after_lock="$(runtime_lock_manifest_hash)"
|
|
204
|
+
if [ $rc -eq 0 ] && [ "$after_lock" = "$before_lock" ]; then
|
|
205
|
+
echo "OK context-sync ignora manifiesto no-objeto (lock intacto)"
|
|
206
|
+
else
|
|
207
|
+
echo "FAIL manifiesto no-objeto corrompio el lock (rc=$rc, '$before_lock' -> '$after_lock')"; fail=1
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
kill "$STUB_CS" 2>/dev/null; wait "$STUB_CS" 2>/dev/null
|
|
211
|
+
# Servidor caído: sale limpio y NO degrada el lock existente.
|
|
212
|
+
bash "$ROOT/hooks/build/context-sync.sh" >/dev/null 2>&1
|
|
213
|
+
rc=$?
|
|
214
|
+
[ $rc -eq 0 ] && echo "OK context-sync offline exit 0" || { echo "FAIL context-sync offline rc ($rc)"; fail=1; }
|
|
215
|
+
got="$(runtime_lock_manifest_hash)"
|
|
216
|
+
[ "$got" = "sha256:v2" ] && echo "OK context-sync offline preserva el lock" || { echo "FAIL lock pisado offline ($got)"; fail=1; }
|
|
217
|
+
runtime_credentials_merge '{"manifest_hash":"sha256:v2"}'
|
|
218
|
+
|
|
219
|
+
# --- heartbeat.sh (daemon §4.3) ---
|
|
220
|
+
PORT_HB="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
221
|
+
cat > "$TMP/routes-hb.json" <<JSON
|
|
222
|
+
{"PUT /leases/renew": {"status": 200, "body": {"expires_at": "2099-01-01T00:00:00Z"}},
|
|
223
|
+
"POST /events": {"status": 200, "body": {"results": []}}}
|
|
224
|
+
JSON
|
|
225
|
+
: > "$TMP/calls-hb.jsonl"
|
|
226
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT_HB" "$TMP/routes-hb.json" "$TMP/calls-hb.jsonl" &
|
|
227
|
+
STUB_HB=$!
|
|
228
|
+
sleep 0.3
|
|
229
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT_HB\",\"project_token\":\"tok-h\",\"agent_key\":\"ak-1\",\"project_id\":\"p-1\",\"lease_ttl_s\":300}"
|
|
230
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"active_slice\":{\"slice_id\":\"sl-9\",\"phase\":\"green\",\"gates\":{},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
231
|
+
|
|
232
|
+
export TRYCORE_HEARTBEAT_TICK_S=1
|
|
233
|
+
export TRYCORE_HEARTBEAT_INTERVAL_S=1
|
|
234
|
+
|
|
235
|
+
# legacy: --ensure es no-op absoluto (ni pidfile, ni proceso).
|
|
236
|
+
TRYCORE_RUNTIME_MODE=legacy bash "$ROOT/hooks/build/heartbeat.sh" --ensure $$ >/dev/null 2>&1
|
|
237
|
+
[ ! -f "$PROJ/.claude/state/heartbeat.pid" ] && echo "OK heartbeat no-op en legacy" || { echo "FAIL heartbeat arranco en legacy"; fail=1; }
|
|
238
|
+
|
|
239
|
+
# Sesiones falsas: dos procesos vivos que hacen de "sesiones de Claude Code".
|
|
240
|
+
sleep 60 & SESS1=$!
|
|
241
|
+
sleep 60 & SESS2=$!
|
|
242
|
+
|
|
243
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --ensure "$SESS1" >/dev/null 2>&1
|
|
244
|
+
sleep 1
|
|
245
|
+
HBPID="$(cat "$PROJ/.claude/state/heartbeat.pid" 2>/dev/null)"
|
|
246
|
+
[ -n "$HBPID" ] && kill -0 "$HBPID" 2>/dev/null && echo "OK heartbeat lanza el daemon" || { echo "FAIL daemon no arranco"; fail=1; }
|
|
247
|
+
|
|
248
|
+
# Segundo --ensure: comparte daemon (mismo pid), no lanza otro.
|
|
249
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --ensure "$SESS2" >/dev/null 2>&1
|
|
250
|
+
sleep 1
|
|
251
|
+
HBPID2="$(cat "$PROJ/.claude/state/heartbeat.pid" 2>/dev/null)"
|
|
252
|
+
[ "$HBPID" = "$HBPID2" ] && echo "OK dos sesiones comparten daemon" || { echo "FAIL se lanzo un segundo daemon ($HBPID -> $HBPID2)"; fail=1; }
|
|
253
|
+
|
|
254
|
+
# Late: con TTL 300 el intervalo real sería 100 s; el override lo baja a 1 s.
|
|
255
|
+
sleep 3
|
|
256
|
+
renews="$(grep -c '"method": "PUT"' "$TMP/calls-hb.jsonl" 2>/dev/null | tr -d ' ')"
|
|
257
|
+
[ "${renews:-0}" -ge 1 ] && echo "OK daemon renueva el lease" || { echo "FAIL sin renew ($renews)"; fail=1; }
|
|
258
|
+
|
|
259
|
+
# Flush: el sentinela dispara un despacho de la cola sin que el hook toque la red.
|
|
260
|
+
runtime_enqueue_event "checkpoint_created" '{"n":1}' "sl-9"
|
|
261
|
+
mkdir -p "$PROJ/.claude/state/outbox"
|
|
262
|
+
: > "$PROJ/.claude/state/outbox/.flush-request"
|
|
263
|
+
sleep 3
|
|
264
|
+
[ ! -f "$PROJ/.claude/state/outbox/.flush-request" ] && echo "OK daemon consume el sentinela de flush" || { echo "FAIL sentinela sin consumir"; fail=1; }
|
|
265
|
+
posts="$(grep -c '"method": "POST"' "$TMP/calls-hb.jsonl" 2>/dev/null | tr -d ' ')"
|
|
266
|
+
[ "${posts:-0}" -ge 1 ] && echo "OK daemon despacha la cola" || { echo "FAIL cola sin despachar ($posts)"; fail=1; }
|
|
267
|
+
|
|
268
|
+
# Cerrar la PRIMERA sesión NO mata el daemon (regresión del diseño anterior).
|
|
269
|
+
kill "$SESS1" 2>/dev/null; wait "$SESS1" 2>/dev/null
|
|
270
|
+
sleep 3
|
|
271
|
+
kill -0 "$HBPID" 2>/dev/null && echo "OK daemon sobrevive al cierre de la primera sesion" || { echo "FAIL daemon murio con la primera sesion"; fail=1; }
|
|
272
|
+
|
|
273
|
+
# kill -9 del daemon: el siguiente --ensure lo relanza con OTRO pid.
|
|
274
|
+
kill -9 "$HBPID" 2>/dev/null; sleep 1
|
|
275
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --ensure "$SESS2" >/dev/null 2>&1
|
|
276
|
+
sleep 1
|
|
277
|
+
HBPID3="$(cat "$PROJ/.claude/state/heartbeat.pid" 2>/dev/null)"
|
|
278
|
+
[ -n "$HBPID3" ] && [ "$HBPID3" != "$HBPID" ] && kill -0 "$HBPID3" 2>/dev/null \
|
|
279
|
+
&& echo "OK heartbeat relanza el daemon muerto" || { echo "FAIL daemon no relanzado ($HBPID3)"; fail=1; }
|
|
280
|
+
|
|
281
|
+
# Muere la ÚLTIMA sesión -> el daemon se apaga solo y limpia el pidfile.
|
|
282
|
+
kill "$SESS2" 2>/dev/null; wait "$SESS2" 2>/dev/null
|
|
283
|
+
sleep 4
|
|
284
|
+
kill -0 "$HBPID3" 2>/dev/null && { echo "FAIL daemon sobrevive sin sesiones"; fail=1; } || echo "OK daemon muere con la ultima sesion"
|
|
285
|
+
[ ! -f "$PROJ/.claude/state/heartbeat.pid" ] && echo "OK daemon limpia el pidfile" || { echo "FAIL pidfile huerfano"; fail=1; }
|
|
286
|
+
|
|
287
|
+
# 409 en el renew: se marca lease perdido y NO se apaga el daemon (spec §4.3).
|
|
288
|
+
cat > "$TMP/routes-hb.json" <<JSON
|
|
289
|
+
{"PUT /leases/renew": {"status": 409, "body": {"error": "lease_conflict"}},
|
|
290
|
+
"POST /events": {"status": 200, "body": {"results": []}}}
|
|
291
|
+
JSON
|
|
292
|
+
sleep 60 & SESS3=$!
|
|
293
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --ensure "$SESS3" >/dev/null 2>&1
|
|
294
|
+
sleep 3
|
|
295
|
+
lost="$(python3 -c "import json;print(json.load(open('$PROJ/.claude/state/heartbeat-status.json')).get('lease_lost'))" 2>/dev/null)"
|
|
296
|
+
[ "$lost" = True ] && echo "OK renew 409 marca lease perdido" || { echo "FAIL 409 no marcado ($lost)"; fail=1; }
|
|
297
|
+
HBPID4="$(cat "$PROJ/.claude/state/heartbeat.pid" 2>/dev/null)"
|
|
298
|
+
kill -0 "$HBPID4" 2>/dev/null && echo "OK daemon sigue vivo tras 409" || { echo "FAIL daemon murio por 409"; fail=1; }
|
|
299
|
+
|
|
300
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
301
|
+
sleep 1
|
|
302
|
+
kill -0 "$HBPID4" 2>/dev/null && { echo "FAIL --stop no apago el daemon"; fail=1; } || echo "OK --stop apaga el daemon"
|
|
303
|
+
kill "$SESS3" 2>/dev/null; wait "$SESS3" 2>/dev/null
|
|
304
|
+
kill "$STUB_HB" 2>/dev/null; wait "$STUB_HB" 2>/dev/null
|
|
305
|
+
unset TRYCORE_HEARTBEAT_TICK_S TRYCORE_HEARTBEAT_INTERVAL_S
|
|
306
|
+
|
|
307
|
+
# --- session-start.sh ---
|
|
308
|
+
PORT_SS="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
309
|
+
cat > "$TMP/routes-ss.json" <<JSON
|
|
310
|
+
{"POST /agents/register": {"status": 200, "body": {"agent_key": "ak-2", "project_id": "p-1", "poll_interval_s": 30, "lease_ttl_s": 300, "manifest_hash": "sha256:v2"}},
|
|
311
|
+
"GET /agent/context": {"status": 200, "body": {"project": {"scaffold": {"confirmed": true}}, "active_slice": {"slice_id": "sl-11", "epic_code": "EP-009", "phase": "green", "gates": {"dor": true, "tdd": false}}, "nudges": [{"kind": "reflect", "message": "2 slices sin reflexionar"}], "context": {"manifest_hash": "sha256:v2", "version": 9}}}}
|
|
312
|
+
JSON
|
|
313
|
+
: > "$TMP/calls-ss.jsonl"
|
|
314
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT_SS" "$TMP/routes-ss.json" "$TMP/calls-ss.jsonl" &
|
|
315
|
+
STUB_SS=$!
|
|
316
|
+
sleep 0.3
|
|
317
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT_SS\",\"project_token\":\"tok-s\",\"manifest_hash\":\"sha256:v2\"}"
|
|
318
|
+
export TRYCORE_HEARTBEAT_TICK_S=1 TRYCORE_HEARTBEAT_INTERVAL_S=1
|
|
319
|
+
|
|
320
|
+
# legacy: cero llamadas.
|
|
321
|
+
before="$(wc -l < "$TMP/calls-ss.jsonl" | tr -d ' ')"
|
|
322
|
+
echo '{"session_id":"s-legacy"}' | TRYCORE_RUNTIME_MODE=legacy bash "$ROOT/hooks/build/session-start.sh" >/dev/null 2>&1
|
|
323
|
+
after="$(wc -l < "$TMP/calls-ss.jsonl" | tr -d ' ')"
|
|
324
|
+
[ "$before" = "$after" ] && echo "OK session-start no-op en legacy" || { echo "FAIL session-start hablo en legacy"; fail=1; }
|
|
325
|
+
|
|
326
|
+
echo '{"session_id":"s-1"}' | bash "$ROOT/hooks/build/session-start.sh" >/dev/null 2>&1
|
|
327
|
+
got="$(runtime_field agent_key)"
|
|
328
|
+
[ "$got" = "ak-2" ] && echo "OK session-start registra el agente" || { echo "FAIL registro ($got)"; fail=1; }
|
|
329
|
+
got="$(projection_get active_slice.slice_id "")"
|
|
330
|
+
[ "$got" = "sl-11" ] && echo "OK session-start hidrata la cache" || { echo "FAIL cache ($got)"; fail=1; }
|
|
331
|
+
at_sent="$(python3 -c "
|
|
332
|
+
import json
|
|
333
|
+
body=None
|
|
334
|
+
for line in open('$TMP/calls-ss.jsonl'):
|
|
335
|
+
c=json.loads(line)
|
|
336
|
+
if c['path']=='/agents/register': body=json.loads(c['body'])
|
|
337
|
+
print(len(body.get('asset_types') or []) if body else -1)
|
|
338
|
+
")"
|
|
339
|
+
[ "$at_sent" -ge 1 ] && echo "OK session-start declara asset_types del paquete" || { echo "FAIL asset_types ($at_sent)"; fail=1; }
|
|
340
|
+
hv_sent="$(python3 -c "
|
|
341
|
+
import json
|
|
342
|
+
body=None
|
|
343
|
+
for line in open('$TMP/calls-ss.jsonl'):
|
|
344
|
+
c=json.loads(line)
|
|
345
|
+
if c['path']=='/agents/register': body=json.loads(c['body'])
|
|
346
|
+
print((body or {}).get('harness_version',''))
|
|
347
|
+
")"
|
|
348
|
+
[ -n "$hv_sent" ] && [ "$hv_sent" != "0.0.0" ] && echo "OK session-start envia la version real del arnes" || { echo "FAIL harness_version ($hv_sent)"; fail=1; }
|
|
349
|
+
sleep 1
|
|
350
|
+
[ -f "$PROJ/.claude/state/heartbeat.pid" ] && echo "OK session-start asegura el daemon" || { echo "FAIL daemon no asegurado"; fail=1; }
|
|
351
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
352
|
+
|
|
353
|
+
# 409 incompatible_version: avisa y sale 0 (jamás rompe el arranque de sesión).
|
|
354
|
+
cat > "$TMP/routes-ss.json" <<JSON
|
|
355
|
+
{"POST /agents/register": {"status": 409, "body": {"error": "incompatible_version", "min_version": "0.9.0"}}}
|
|
356
|
+
JSON
|
|
357
|
+
out="$(echo '{"session_id":"s-2"}' | bash "$ROOT/hooks/build/session-start.sh" 2>&1)"
|
|
358
|
+
rc=$?
|
|
359
|
+
[ $rc -eq 0 ] && echo "OK session-start exit 0 con 409" || { echo "FAIL session-start rc con 409 ($rc)"; fail=1; }
|
|
360
|
+
echo "$out" | grep -q 'incompatible\|Actualiza' && echo "OK session-start explica el 409" || { echo "FAIL 409 sin mensaje ($out)"; fail=1; }
|
|
361
|
+
|
|
362
|
+
kill "$STUB_SS" 2>/dev/null; wait "$STUB_SS" 2>/dev/null
|
|
363
|
+
# Runtime caído: sale 0, avisa y NO borra la caché previa.
|
|
364
|
+
out="$(echo '{"session_id":"s-3"}' | bash "$ROOT/hooks/build/session-start.sh" 2>&1)"
|
|
365
|
+
rc=$?
|
|
366
|
+
[ $rc -eq 0 ] && echo "OK session-start exit 0 offline" || { echo "FAIL session-start rc offline ($rc)"; fail=1; }
|
|
367
|
+
got="$(projection_get active_slice.slice_id "")"
|
|
368
|
+
[ "$got" = "sl-11" ] && echo "OK session-start offline preserva la cache" || { echo "FAIL cache perdida offline ($got)"; fail=1; }
|
|
369
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
370
|
+
unset TRYCORE_HEARTBEAT_TICK_S TRYCORE_HEARTBEAT_INTERVAL_S
|
|
371
|
+
|
|
372
|
+
# --- event-emitter.sh ---
|
|
373
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json 2>/dev/null
|
|
374
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
375
|
+
export TRYCORE_HEARTBEAT_TICK_S=1 TRYCORE_HEARTBEAT_INTERVAL_S=1
|
|
376
|
+
|
|
377
|
+
# legacy: ni encola ni arranca daemon.
|
|
378
|
+
echo '{"tool_name":"Edit","tool_input":{"file_path":"'"$PROJ"'/src/a.ts"}}' \
|
|
379
|
+
| TRYCORE_RUNTIME_MODE=legacy bash "$ROOT/hooks/build/event-emitter.sh" >/dev/null 2>&1
|
|
380
|
+
n="$(find "$PROJ/.claude/state/outbox" -maxdepth 1 -name '*.json' 2>/dev/null | wc -l | tr -d ' ')"
|
|
381
|
+
[ "$n" = 0 ] && echo "OK event-emitter no-op en legacy" || { echo "FAIL event-emitter encolo en legacy ($n)"; fail=1; }
|
|
382
|
+
|
|
383
|
+
echo '{"tool_name":"Edit","tool_input":{"file_path":"'"$PROJ"'/src/a.ts"}}' \
|
|
384
|
+
| bash "$ROOT/hooks/build/event-emitter.sh" >/dev/null 2>&1
|
|
385
|
+
ev="$(python3 -c "
|
|
386
|
+
import json,glob
|
|
387
|
+
files=sorted(p for p in glob.glob('$PROJ/.claude/state/outbox/*.json') if not p.endswith('.dispatch-state.json'))
|
|
388
|
+
d=json.load(open(files[0]))
|
|
389
|
+
print(d['type'], d.get('slice_id'), d['payload'].get('path'), d['payload'].get('tool'))
|
|
390
|
+
" 2>/dev/null)"
|
|
391
|
+
[ "$ev" = "tool_use_recorded sl-11 src/a.ts Edit" ] && echo "OK event-emitter encola ruta relativa con slice_id" || { echo "FAIL evento ($ev)"; fail=1; }
|
|
392
|
+
|
|
393
|
+
# Bash: solo el binario, jamás el comando completo (puede llevar secretos — protocolo §8).
|
|
394
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json
|
|
395
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"curl -H \"Authorization: Bearer SECRETO\" https://x"}}' \
|
|
396
|
+
| bash "$ROOT/hooks/build/event-emitter.sh" >/dev/null 2>&1
|
|
397
|
+
body="$(cat "$PROJ"/.claude/state/outbox/*.json 2>/dev/null)"
|
|
398
|
+
echo "$body" | grep -q 'SECRETO' && { echo "FAIL event-emitter filtra el comando completo"; fail=1; } || echo "OK event-emitter no filtra el comando"
|
|
399
|
+
echo "$body" | grep -q '"argv0": *"curl"' && echo "OK event-emitter reporta solo argv0" || { echo "FAIL argv0 ($body)"; fail=1; }
|
|
400
|
+
|
|
401
|
+
# Bash: secreto en asignación de entorno inline al frente del comando (`VAR=valor cmd ...`)
|
|
402
|
+
# tampoco debe filtrarse — argv0 debe ser el binario, no la asignación (protocolo §8).
|
|
403
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json
|
|
404
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"API_TOKEN=sk-live-XYZ curl https://api.example.com"}}' \
|
|
405
|
+
| bash "$ROOT/hooks/build/event-emitter.sh" >/dev/null 2>&1
|
|
406
|
+
body="$(cat "$PROJ"/.claude/state/outbox/*.json 2>/dev/null)"
|
|
407
|
+
echo "$body" | grep -q 'sk-live-XYZ' && { echo "FAIL event-emitter filtra secreto en asignacion de entorno"; fail=1; } || echo "OK event-emitter no filtra secreto en asignacion de entorno"
|
|
408
|
+
echo "$body" | grep -q 'API_TOKEN' && { echo "FAIL event-emitter filtra nombre de variable de entorno"; fail=1; } || echo "OK event-emitter no filtra nombre de variable de entorno"
|
|
409
|
+
echo "$body" | grep -q '"argv0": *"curl"' && echo "OK event-emitter salta asignacion de entorno y reporta binario" || { echo "FAIL argv0 con asignacion de entorno ($body)"; fail=1; }
|
|
410
|
+
|
|
411
|
+
# Bash: el valor citado de la asignación de entorno inline lleva un espacio interno
|
|
412
|
+
# (`KEY="a b" cmd`) — una tokenización naïve por espacios trocearía el valor citado y el
|
|
413
|
+
# segundo fragmento (parte del secreto) se colaría como argv0. shlex debe tratarlo como
|
|
414
|
+
# un solo token y seguir saltándolo.
|
|
415
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json
|
|
416
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"AWS_SECRET_ACCESS_KEY=\"abc def-secretpart\" aws s3 ls"}}' \
|
|
417
|
+
| bash "$ROOT/hooks/build/event-emitter.sh" >/dev/null 2>&1
|
|
418
|
+
body="$(cat "$PROJ"/.claude/state/outbox/*.json 2>/dev/null)"
|
|
419
|
+
echo "$body" | grep -q 'def-secretpart' && { echo "FAIL event-emitter filtra fragmento de secreto citado con espacio"; fail=1; } || echo "OK event-emitter no filtra fragmento de secreto citado con espacio"
|
|
420
|
+
echo "$body" | grep -q '"argv0": *"aws"' && echo "OK event-emitter reporta binario con asignacion de entorno citada" || { echo "FAIL argv0 con asignacion citada ($body)"; fail=1; }
|
|
421
|
+
|
|
422
|
+
# Sin slice reclamado: no se encola nada de ámbito slice (el servidor lo rechazaría).
|
|
423
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json
|
|
424
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"active_slice\":null,\"nudges\":[]}"
|
|
425
|
+
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$PROJ"'/src/b.ts"}}' \
|
|
426
|
+
| bash "$ROOT/hooks/build/event-emitter.sh" >/dev/null 2>&1
|
|
427
|
+
n="$(find "$PROJ/.claude/state/outbox" -maxdepth 1 -name '*.json' 2>/dev/null | wc -l | tr -d ' ')"
|
|
428
|
+
[ "$n" = 0 ] && echo "OK event-emitter no encola sin slice" || { echo "FAIL encolo sin slice ($n)"; fail=1; }
|
|
429
|
+
|
|
430
|
+
# Nunca abre socket: un `curl` falso al frente del PATH no debe llegar a ejecutarse.
|
|
431
|
+
mkdir -p "$TMP/fakebin"
|
|
432
|
+
cat > "$TMP/fakebin/curl" <<'FAKE'
|
|
433
|
+
#!/usr/bin/env bash
|
|
434
|
+
touch "$FAKE_CURL_MARK"
|
|
435
|
+
exit 0
|
|
436
|
+
FAKE
|
|
437
|
+
chmod +x "$TMP/fakebin/curl"
|
|
438
|
+
rm -f "$TMP/curl-was-called"
|
|
439
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
440
|
+
echo '{"tool_name":"Edit","tool_input":{"file_path":"'"$PROJ"'/src/c.ts"}}' \
|
|
441
|
+
| FAKE_CURL_MARK="$TMP/curl-was-called" PATH="$TMP/fakebin:$PATH" bash "$ROOT/hooks/build/event-emitter.sh" >/dev/null 2>&1
|
|
442
|
+
[ ! -f "$TMP/curl-was-called" ] && echo "OK event-emitter no toca la red" || { echo "FAIL event-emitter llamo a curl"; fail=1; }
|
|
443
|
+
|
|
444
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
445
|
+
unset TRYCORE_HEARTBEAT_TICK_S TRYCORE_HEARTBEAT_INTERVAL_S
|
|
446
|
+
|
|
447
|
+
# --- load-build-state.sh en modo runtime ---
|
|
448
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
|
|
449
|
+
\"project\":{\"scaffold_confirmed\":true,\"harness_phase\":\"active\",\"design_source_applies\":false,\"design_source_confirmed\":false,\"project_kind\":\"greenfield\",\"foundation_done\":true},
|
|
450
|
+
\"active_slice\":{\"slice_id\":\"sl-11\",\"epic_code\":\"EP-009\",\"epic_title\":\"Cobros\",\"hus\":[\"HU-021\"],
|
|
451
|
+
\"phase\":\"green\",\"gates\":{\"dor\":true,\"tdd\":false,\"dod\":false},\"branch\":\"feature/cobros\",
|
|
452
|
+
\"openspec_change\":\"add-cobros\",\"wiring_failing\":[{\"item_id\":\"w1\",\"kind\":\"e2e\"}],
|
|
453
|
+
\"sub_slices_pending\":[\"ss-2\"],\"resume_hint\":\"cablear w1\",\"checkpoint\":null},
|
|
454
|
+
\"nudges\":[],\"context\":{\"manifest_hash\":\"sha256:v2\",\"version\":9},\"lease\":{\"expires_at\":null,\"ttl_s\":300}}"
|
|
455
|
+
|
|
456
|
+
out="$(echo '{"session_id":"s-render"}' | bash "$ROOT/hooks/build/load-build-state.sh" 2>&1)"
|
|
457
|
+
echo "$out" | grep -q 'EP-009' && echo "OK render runtime muestra la epica" || { echo "FAIL render epica ($out)"; fail=1; }
|
|
458
|
+
echo "$out" | grep -q 'tdd' && echo "OK render runtime lista gates abiertos" || { echo "FAIL render gates ($out)"; fail=1; }
|
|
459
|
+
echo "$out" | grep -q 'w1' && echo "OK render runtime muestra wiring failing" || { echo "FAIL render wiring ($out)"; fail=1; }
|
|
460
|
+
echo "$out" | grep -q 'cablear w1' && echo "OK render runtime muestra resume_hint" || { echo "FAIL render resume_hint ($out)"; fail=1; }
|
|
461
|
+
echo "$out" | grep -q 'ss-2' && echo "OK render runtime muestra sub-slices pendientes" || { echo "FAIL render sub-slices ($out)"; fail=1; }
|
|
462
|
+
|
|
463
|
+
# En runtime NO se toca build-state.json aunque exista (el fichero es fósil).
|
|
464
|
+
cat > "$PROJ/.claude/state/build-state.json" <<'JSON'
|
|
465
|
+
{"version":"1.0","harness_phase":"authoring","scaffold":{"confirmed":false},"active_slice":null,"history":[],"releases":[]}
|
|
466
|
+
JSON
|
|
467
|
+
before="$(md5 -q "$PROJ/.claude/state/build-state.json" 2>/dev/null || md5sum "$PROJ/.claude/state/build-state.json" | cut -d' ' -f1)"
|
|
468
|
+
echo '{"session_id":"s-render2"}' | bash "$ROOT/hooks/build/load-build-state.sh" >/dev/null 2>&1
|
|
469
|
+
after="$(md5 -q "$PROJ/.claude/state/build-state.json" 2>/dev/null || md5sum "$PROJ/.claude/state/build-state.json" | cut -d' ' -f1)"
|
|
470
|
+
[ "$before" = "$after" ] && echo "OK runtime no escribe build-state.json" || { echo "FAIL runtime escribio el fichero legacy"; fail=1; }
|
|
471
|
+
|
|
472
|
+
# Caché vieja: avisa de stale pero sigue rindiendo (guards offline, spec §8).
|
|
473
|
+
runtime_projection_write '{"fetched_at":"2000-01-01T00:00:00Z","project":{},"active_slice":null,"nudges":[]}'
|
|
474
|
+
out="$(echo '{"session_id":"s-render3"}' | bash "$ROOT/hooks/build/load-build-state.sh" 2>&1)"
|
|
475
|
+
echo "$out" | grep -q 'stale\|sin refrescar' && echo "OK render runtime avisa de cache vieja" || { echo "FAIL sin aviso de stale ($out)"; fail=1; }
|
|
476
|
+
echo "$out" | grep -q 'ninguno\|/build:claim' && echo "OK render runtime sin slice sugiere reclamar" || { echo "FAIL sin sugerencia de claim ($out)"; fail=1; }
|
|
477
|
+
|
|
478
|
+
# Regresión legacy: el camino v0.8.5 sigue intacto (lo cubre además test-context-monitor.sh).
|
|
479
|
+
out="$(echo '{"session_id":"s-legacy"}' | TRYCORE_RUNTIME_MODE=legacy bash "$ROOT/hooks/build/load-build-state.sh" 2>&1)"
|
|
480
|
+
echo "$out" | grep -q 'Arnés de construcción' && echo "OK load-build-state legacy sigue rindiendo" || { echo "FAIL legacy roto ($out)"; fail=1; }
|
|
481
|
+
rm -f "$PROJ/.claude/state/build-state.json"
|
|
482
|
+
|
|
483
|
+
# --- guards desde la caché de proyección (modo runtime) ---
|
|
484
|
+
HOOK_IN='{"tool_name":"Write","tool_input":{"file_path":"'"$PROJ"'/src/x.ts","content":"x"}}'
|
|
485
|
+
|
|
486
|
+
# Sin caché: auto-arme, no bloquea (nada que vigilar todavía).
|
|
487
|
+
rm -f "$PROJ/.claude/state/runtime-projection.json"
|
|
488
|
+
echo "$HOOK_IN" | bash "$ROOT/hooks/build/scaffold-guard.sh" >/dev/null 2>&1
|
|
489
|
+
[ $? -eq 0 ] && echo "OK scaffold-guard sin cache no bloquea" || { echo "FAIL scaffold-guard bloqueo sin cache"; fail=1; }
|
|
490
|
+
|
|
491
|
+
# Fase de código + scaffold NO confirmado -> bloquea.
|
|
492
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":false,\"design_source_applies\":true,\"design_source_confirmed\":false},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{\"fidelity\":false},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
493
|
+
out="$(echo "$HOOK_IN" | bash "$ROOT/hooks/build/scaffold-guard.sh" 2>&1)"
|
|
494
|
+
rc=$?
|
|
495
|
+
[ $rc -eq 2 ] && echo "OK scaffold-guard bloquea desde la cache" || { echo "FAIL scaffold-guard rc ($rc)"; fail=1; }
|
|
496
|
+
|
|
497
|
+
# Fase de planificación -> permitido.
|
|
498
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":false},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"dor\",\"gates\":{},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
499
|
+
echo "$HOOK_IN" | bash "$ROOT/hooks/build/scaffold-guard.sh" >/dev/null 2>&1
|
|
500
|
+
[ $? -eq 0 ] && echo "OK scaffold-guard permite en fase dor" || { echo "FAIL scaffold-guard bloqueo en dor"; fail=1; }
|
|
501
|
+
|
|
502
|
+
# Scaffold confirmado -> permitido.
|
|
503
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":true},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
504
|
+
echo "$HOOK_IN" | bash "$ROOT/hooks/build/scaffold-guard.sh" >/dev/null 2>&1
|
|
505
|
+
[ $? -eq 0 ] && echo "OK scaffold-guard permite con scaffold confirmado" || { echo "FAIL scaffold-guard bloqueo con scaffold ok"; fail=1; }
|
|
506
|
+
|
|
507
|
+
# Caché vieja: sigue bloqueando y lo dice (guards offline, spec §8).
|
|
508
|
+
runtime_projection_write '{"fetched_at":"2000-01-01T00:00:00Z","project":{"scaffold_confirmed":false},"active_slice":{"slice_id":"sl-11","phase":"green","gates":{},"wiring_failing":[]},"nudges":[]}'
|
|
509
|
+
out="$(echo "$HOOK_IN" | bash "$ROOT/hooks/build/scaffold-guard.sh" 2>&1)"
|
|
510
|
+
rc=$?
|
|
511
|
+
[ $rc -eq 2 ] && echo "$out" | grep -q 'stale\|sin refrescar' && echo "OK scaffold-guard bloquea offline y marca stale" || { echo "FAIL guard offline ($rc / $out)"; fail=1; }
|
|
512
|
+
|
|
513
|
+
# design-source-guard: UI pendiente + fuente sin confirmar -> bloquea; confirmada -> permite.
|
|
514
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":true,\"design_source_applies\":true,\"design_source_confirmed\":false},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{\"fidelity\":false},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
515
|
+
echo "$HOOK_IN" | bash "$ROOT/hooks/build/design-source-guard.sh" >/dev/null 2>&1
|
|
516
|
+
[ $? -eq 2 ] && echo "OK design-source-guard bloquea desde la cache" || { echo "FAIL design-source-guard no bloqueo"; fail=1; }
|
|
517
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":true,\"design_source_applies\":true,\"design_source_confirmed\":true},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{\"fidelity\":false},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
518
|
+
echo "$HOOK_IN" | bash "$ROOT/hooks/build/design-source-guard.sh" >/dev/null 2>&1
|
|
519
|
+
[ $? -eq 0 ] && echo "OK design-source-guard permite con fuente confirmada" || { echo "FAIL design-source-guard bloqueo con fuente ok"; fail=1; }
|
|
520
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":true,\"design_source_applies\":false,\"design_source_confirmed\":false},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{\"fidelity\":false},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
521
|
+
echo "$HOOK_IN" | bash "$ROOT/hooks/build/design-source-guard.sh" >/dev/null 2>&1
|
|
522
|
+
[ $? -eq 0 ] && echo "OK design-source-guard apagado sin UI" || { echo "FAIL design-source-guard bloqueo sin UI"; fail=1; }
|
|
523
|
+
|
|
524
|
+
# ── PreToolUse: prohibida la red (spec §4.2-B) ──
|
|
525
|
+
# Reutiliza el `curl` falso creado en el bloque de event-emitter (más arriba en este mismo
|
|
526
|
+
# fichero de test): si ese bloque se moviera, hay que recrear $TMP/fakebin/curl aquí.
|
|
527
|
+
# Entorno REALMENTE aislado: subshell bash -c con un `curl` falso al frente del PATH y
|
|
528
|
+
# credenciales apuntando a un puerto vivo. Si cualquiera de los cuatro guards abriera
|
|
529
|
+
# socket, el falso curl dejaría su marca. No se comprueba por grep del código (eso no
|
|
530
|
+
# demuestra comportamiento) sino ejecutando los guards de verdad.
|
|
531
|
+
runtime_credentials_merge '{"runtime_url":"http://127.0.0.1:9","project_token":"tok-x"}'
|
|
532
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":false},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{\"fidelity\":false},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
533
|
+
for g in gitflow-guard stack-guard scaffold-guard design-source-guard; do
|
|
534
|
+
rm -f "$TMP/curl-was-called"
|
|
535
|
+
bash -c '
|
|
536
|
+
echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git commit -m x\",\"file_path\":\"'"$PROJ"'/package.json\",\"content\":\"{}\"}}" \
|
|
537
|
+
| FAKE_CURL_MARK="'"$TMP"'/curl-was-called" PATH="'"$TMP"'/fakebin:$PATH" \
|
|
538
|
+
CLAUDE_PROJECT_DIR="'"$PROJ"'" BUILD_CONFIG_FILE="'"$BUILD_CONFIG_FILE"'" \
|
|
539
|
+
bash "'"$ROOT"'/hooks/build/'"$g"'.sh"' >/dev/null 2>&1
|
|
540
|
+
[ ! -f "$TMP/curl-was-called" ] && echo "OK $g no toca la red" || { echo "FAIL $g abrio socket"; fail=1; }
|
|
541
|
+
done
|
|
542
|
+
|
|
543
|
+
# --- gitflow-guard: repo del comando, no el de la sesión (spec §9) ---
|
|
544
|
+
OTHER="$TMP/otro-repo"
|
|
545
|
+
mkdir -p "$OTHER"
|
|
546
|
+
git -C "$OTHER" init -q 2>/dev/null
|
|
547
|
+
git -C "$OTHER" checkout -q -b main 2>/dev/null
|
|
548
|
+
git -C "$OTHER" commit -q --allow-empty -m init 2>/dev/null
|
|
549
|
+
|
|
550
|
+
SESSION_REPO="$TMP/sesion-repo"
|
|
551
|
+
mkdir -p "$SESSION_REPO"
|
|
552
|
+
git -C "$SESSION_REPO" init -q 2>/dev/null
|
|
553
|
+
git -C "$SESSION_REPO" checkout -q -b feature/ok 2>/dev/null
|
|
554
|
+
git -C "$SESSION_REPO" commit -q --allow-empty -m init 2>/dev/null
|
|
555
|
+
|
|
556
|
+
# La sesión está en feature/ok, pero el comando commitea en OTRO repo que está en main:
|
|
557
|
+
# el guard debe bloquear por la rama del repo del COMANDO.
|
|
558
|
+
out="$(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git -C '"$OTHER"' commit -m x"}}' \
|
|
559
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" 2>&1)"
|
|
560
|
+
rc=$?
|
|
561
|
+
[ $rc -eq 2 ] && echo "OK gitflow-guard resuelve el repo del comando (-C)" || { echo "FAIL gitflow-guard no vio el repo del comando ($rc / $out)"; fail=1; }
|
|
562
|
+
|
|
563
|
+
# Mismo caso con `cd <dir> && git commit`.
|
|
564
|
+
out="$(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"cd '"$OTHER"' && git commit -m x"}}' \
|
|
565
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" 2>&1)"
|
|
566
|
+
rc=$?
|
|
567
|
+
[ $rc -eq 2 ] && echo "OK gitflow-guard resuelve el repo tras un cd" || { echo "FAIL gitflow-guard ignoro el cd ($rc)"; fail=1; }
|
|
568
|
+
|
|
569
|
+
# Sin pistas: cae al repo de la sesión (feature/ok) -> permitido.
|
|
570
|
+
(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' \
|
|
571
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" >/dev/null 2>&1)
|
|
572
|
+
[ $? -eq 0 ] && echo "OK gitflow-guard permite en rama tipada del repo de sesion" || { echo "FAIL gitflow-guard bloqueo rama valida"; fail=1; }
|
|
573
|
+
|
|
574
|
+
# Política desde el asset sincronizado: con `release` como prefijo permitido y `develop`
|
|
575
|
+
# protegida, el guard cambia de veredicto sin tocar el código.
|
|
576
|
+
mkdir -p "$SESSION_REPO/.claude/config"
|
|
577
|
+
cat > "$SESSION_REPO/.claude/config/policy.json" <<'JSON'
|
|
578
|
+
{"branch_prefixes":["release"],"protected_branches":["main","develop"]}
|
|
579
|
+
JSON
|
|
580
|
+
(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' \
|
|
581
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" >/dev/null 2>&1)
|
|
582
|
+
[ $? -eq 2 ] && echo "OK gitflow-guard aplica los prefijos de la politica sincronizada" || { echo "FAIL politica sincronizada ignorada"; fail=1; }
|
|
583
|
+
git -C "$SESSION_REPO" checkout -q -b release/1.0 2>/dev/null
|
|
584
|
+
(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' \
|
|
585
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" >/dev/null 2>&1)
|
|
586
|
+
[ $? -eq 0 ] && echo "OK gitflow-guard permite el prefijo declarado en la politica" || { echo "FAIL prefijo de politica no permitido"; fail=1; }
|
|
587
|
+
|
|
588
|
+
# --- Revision Task 10 (hallazgo 1): varios `cd` encadenados antes del git — gana el
|
|
589
|
+
# ULTIMO (el directorio real en el que corre el commit), no el primero. En este punto la
|
|
590
|
+
# politica de SESSION_REPO exige prefijo "release" y protege "main"/"develop".
|
|
591
|
+
TYPED_REPO="$TMP/typed-repo"
|
|
592
|
+
mkdir -p "$TYPED_REPO"
|
|
593
|
+
git -C "$TYPED_REPO" init -q 2>/dev/null
|
|
594
|
+
git -C "$TYPED_REPO" checkout -q -b release/typed 2>/dev/null
|
|
595
|
+
git -C "$TYPED_REPO" commit -q --allow-empty -m init 2>/dev/null
|
|
596
|
+
# TYPED_REPO (primer cd) esta en una rama permitida por la politica; OTHER (ultimo cd)
|
|
597
|
+
# esta en "main", protegida. Si el guard tomara el PRIMER cd (el bug), esto pasaria
|
|
598
|
+
# (rc=0); si toma el ULTIMO (el fix), debe bloquear (rc=2).
|
|
599
|
+
out="$(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"cd '"$TYPED_REPO"' && cd '"$OTHER"' && git commit -m x"}}' \
|
|
600
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" 2>&1)"
|
|
601
|
+
rc=$?
|
|
602
|
+
[ $rc -eq 2 ] && echo "OK gitflow-guard usa el ultimo cd de una cadena, no el primero" || { echo "FAIL gitflow-guard tomo el primer cd de la cadena ($rc / $out)"; fail=1; }
|
|
603
|
+
|
|
604
|
+
# --- Revision Task 10 (hallazgo 2): `git commit -C <ref>` (flag del subcomando, reusa
|
|
605
|
+
# mensaje/autoria) no debe confundirse con el flag GLOBAL `git -C <dir> commit`. El guard
|
|
606
|
+
# debe seguir evaluando el repo real (SESSION_REPO) y su rama real.
|
|
607
|
+
git -C "$SESSION_REPO" checkout -q -b main 2>/dev/null
|
|
608
|
+
NOGIT_DIR="$TMP/nogit-dir"
|
|
609
|
+
mkdir -p "$NOGIT_DIR"
|
|
610
|
+
out="$(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git commit -C '"$NOGIT_DIR"' -m x"}}' \
|
|
611
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" 2>&1)"
|
|
612
|
+
rc=$?
|
|
613
|
+
[ $rc -eq 2 ] && echo "OK gitflow-guard no confunde 'commit -C <ref>' con el -C global" || { echo "FAIL gitflow-guard confundio commit -C con el flag global ($rc / $out)"; fail=1; }
|
|
614
|
+
|
|
615
|
+
# --- Revision Task 10 (hallazgo 3): un metacaracter (p.ej. un punto) en un prefijo o rama
|
|
616
|
+
# protegida de policy.json se trata como texto LITERAL, nunca como wildcard de regex
|
|
617
|
+
# (regresion sobre el re.escape del commit 0b7c053).
|
|
618
|
+
cat > "$SESSION_REPO/.claude/config/policy.json" <<'JSON'
|
|
619
|
+
{"branch_prefixes":["rel.ease"],"protected_branches":["main"]}
|
|
620
|
+
JSON
|
|
621
|
+
git -C "$SESSION_REPO" checkout -q -b relXease/1 2>/dev/null
|
|
622
|
+
(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' \
|
|
623
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" >/dev/null 2>&1)
|
|
624
|
+
[ $? -eq 2 ] && echo "OK gitflow-guard trata el punto del prefijo como literal, no wildcard" || { echo "FAIL gitflow-guard trato el punto de la politica como wildcard de regex"; fail=1; }
|
|
625
|
+
git -C "$SESSION_REPO" checkout -q -b rel.ease/1 2>/dev/null
|
|
626
|
+
(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}' \
|
|
627
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" >/dev/null 2>&1)
|
|
628
|
+
[ $? -eq 0 ] && echo "OK gitflow-guard permite el prefijo literal con punto de la politica" || { echo "FAIL gitflow-guard bloqueo el prefijo literal con punto"; fail=1; }
|
|
629
|
+
|
|
630
|
+
# --- Revision final de rama: dos bypasses de git_global_dash_c reproducidos en vivo tras
|
|
631
|
+
# el fix del hallazgo 2 — un prefijo de entorno o una flag global con valor ANTES de -C
|
|
632
|
+
# hacian que el walk de tokens nunca llegara a leer el -C real, cayendo al repo de sesion
|
|
633
|
+
# (feature/ok, permitido) en vez del repo real del comando (OTHER, en "main", protegida).
|
|
634
|
+
git -C "$SESSION_REPO" checkout -q -b feature/ok 2>/dev/null
|
|
635
|
+
|
|
636
|
+
# hallazgo A: `VAR=val git -C <dir> commit` — la asignacion de entorno antes de `git` hacia
|
|
637
|
+
# que toks[0] != "git" y el segmento se descartaba entero.
|
|
638
|
+
out="$(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"GIT_AUTHOR_NAME=x git -C '"$OTHER"' commit -m x"}}' \
|
|
639
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" 2>&1)"
|
|
640
|
+
rc=$?
|
|
641
|
+
[ $rc -eq 2 ] && echo "OK gitflow-guard resuelve -C tras un prefijo de variable de entorno" || { echo "FAIL gitflow-guard bypass via prefijo de entorno ($rc / $out)"; fail=1; }
|
|
642
|
+
|
|
643
|
+
# hallazgo B: `git -c clave=valor -C <dir> commit` — `-c` toma su valor en un token
|
|
644
|
+
# SEPARADO; sin saltar el par completo, el walk trataba ese valor como si fuera el
|
|
645
|
+
# subcomando y paraba ahi, sin llegar nunca al -C real.
|
|
646
|
+
out="$(cd "$SESSION_REPO" && echo '{"tool_name":"Bash","tool_input":{"command":"git -c user.name=x -C '"$OTHER"' commit -m x"}}' \
|
|
647
|
+
| CLAUDE_PROJECT_DIR="$SESSION_REPO" bash "$ROOT/hooks/build/gitflow-guard.sh" 2>&1)"
|
|
648
|
+
rc=$?
|
|
649
|
+
[ $rc -eq 2 ] && echo "OK gitflow-guard resuelve -C tras una flag global con valor (-c)" || { echo "FAIL gitflow-guard bypass via -c antes de -C ($rc / $out)"; fail=1; }
|
|
650
|
+
|
|
651
|
+
# stack-guard: bloquea igual que siempre y, con contexto stale, lo anota.
|
|
652
|
+
mkdir -p "$PROJ/.claude/config"
|
|
653
|
+
echo '{"allow":["react"]}' > "$PROJ/.claude/config/stack-allowlist.json"
|
|
654
|
+
runtime_projection_write '{"fetched_at":"2000-01-01T00:00:00Z","project":{},"active_slice":null,"nudges":[]}'
|
|
655
|
+
out="$(echo '{"tool_name":"Write","tool_input":{"file_path":"'"$PROJ"'/package.json","content":"{\"dependencies\":{\"lodash\":\"^4.0.0\"}}"}}' \
|
|
656
|
+
| bash "$ROOT/hooks/build/stack-guard.sh" 2>&1)"
|
|
657
|
+
rc=$?
|
|
658
|
+
[ $rc -eq 2 ] && echo "OK stack-guard sigue bloqueando deps fuera del allowlist" || { echo "FAIL stack-guard rc ($rc / $out)"; fail=1; }
|
|
659
|
+
echo "$out" | grep -q 'stale\|sin sincronizar' && echo "OK stack-guard anota contexto stale" || { echo "FAIL stack-guard sin nota de stale ($out)"; fail=1; }
|
|
660
|
+
|
|
661
|
+
# --- context-monitor.sh en modo runtime: handoff como evento, sin escritura local ---
|
|
662
|
+
export TMPDIR="$TMP"
|
|
663
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json 2>/dev/null
|
|
664
|
+
rm -f "$PROJ"/.claude/state/.handoff-* 2>/dev/null
|
|
665
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{},\"wiring_failing\":[{\"item_id\":\"w1\",\"kind\":\"e2e\"}],\"resume_hint\":null},\"nudges\":[],\"context\":{\"version\":9}}"
|
|
666
|
+
echo "{\"remaining_pct\":20,\"used_pct\":80,\"ts\":$(date +%s)}" > "$TMP/claude-ctx-cm1.json"
|
|
667
|
+
|
|
668
|
+
out="$(echo '{"session_id":"cm1","hook_event_name":"PostToolUse"}' | bash "$ROOT/hooks/build/context-monitor.sh" 2>&1)"
|
|
669
|
+
echo "$out" | grep -q 'additionalContext' && echo "OK context-monitor runtime inyecta aviso" || { echo "FAIL sin aviso runtime ($out)"; fail=1; }
|
|
670
|
+
ev="$(python3 -c "
|
|
671
|
+
import json,glob
|
|
672
|
+
files=[p for p in glob.glob('$PROJ/.claude/state/outbox/*.json') if not p.endswith('.dispatch-state.json')]
|
|
673
|
+
d=[json.load(open(p)) for p in files]
|
|
674
|
+
h=[x for x in d if x['type']=='handoff_recorded']
|
|
675
|
+
print(h[0]['slice_id'], 'w1' in (h[0]['payload'].get('resume_hint') or '')) if h else print('none')
|
|
676
|
+
" 2>/dev/null)"
|
|
677
|
+
[ "$ev" = "sl-11 True" ] && echo "OK context-monitor encola handoff_recorded con resume_hint" || { echo "FAIL handoff_recorded ($ev)"; fail=1; }
|
|
678
|
+
[ ! -f "$PROJ/.claude/state/build-state.json" ] && echo "OK context-monitor runtime no crea estado local" || { echo "FAIL estado local creado en runtime"; fail=1; }
|
|
679
|
+
|
|
680
|
+
# Once-per-sesión: un segundo disparo no duplica el evento.
|
|
681
|
+
n_before="$(find "$PROJ/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
682
|
+
echo '{"session_id":"cm1","hook_event_name":"PostToolUse"}' | bash "$ROOT/hooks/build/context-monitor.sh" >/dev/null 2>&1
|
|
683
|
+
n_after="$(find "$PROJ/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
684
|
+
[ "$n_before" = "$n_after" ] && echo "OK handoff_recorded no se duplica en la misma sesion" || { echo "FAIL handoff duplicado ($n_before -> $n_after)"; fail=1; }
|
|
685
|
+
|
|
686
|
+
# --- Regresión revisión final de rama (hallazgo 1): context-monitor.sh en modo `dual` SIN
|
|
687
|
+
# slice_id conocido NO debe encolar handoff_recorded sin la clave slice_id. `dual` no reclama
|
|
688
|
+
# vía runtime, así que /agent/context normalmente no trae active_slice y el slice_id sale
|
|
689
|
+
# vacío: es el caso NORMAL en dual, no el borde. Sin la guardia, el evento se encolaba sin
|
|
690
|
+
# slice_id (violando protocolo §1) y, al estar en RUNTIME_OUTBOX_PROTECTED, quedaba como
|
|
691
|
+
# poison pill si el servidor rechazaba el lote.
|
|
692
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json 2>/dev/null
|
|
693
|
+
rm -f "$PROJ"/.claude/state/.handoff-* 2>/dev/null
|
|
694
|
+
rm -f "$PROJ/.claude/state/build-state.json"
|
|
695
|
+
cat > "$PROJ/.claude/state/build-state.json" <<'JSON'
|
|
696
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
697
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"x","branch":"feature/x","phase":"tdd",
|
|
698
|
+
"gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t","progress_log":[]},
|
|
699
|
+
"history":[],"releases":[]}
|
|
700
|
+
JSON
|
|
701
|
+
# Caché de proyección de un cliente dual real: sin claim vía runtime, active_slice llega null.
|
|
702
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{},\"active_slice\":null,\"nudges\":[]}"
|
|
703
|
+
echo "{\"remaining_pct\":20,\"used_pct\":80,\"ts\":$(date +%s)}" > "$TMP/claude-ctx-cmdual.json"
|
|
704
|
+
|
|
705
|
+
out="$(echo '{"session_id":"cmdual","hook_event_name":"PostToolUse"}' | TRYCORE_RUNTIME_MODE=dual bash "$ROOT/hooks/build/context-monitor.sh" 2>&1)"
|
|
706
|
+
echo "$out" | grep -q 'additionalContext' && echo "OK context-monitor dual inyecta aviso" || { echo "FAIL sin aviso dual ($out)"; fail=1; }
|
|
707
|
+
sc="$(python3 -c "import json;print(json.load(open('$PROJ/.claude/state/build-state.json'))['active_slice'].get('session_continuity',{}).get('critical_recorded'))" 2>/dev/null)"
|
|
708
|
+
[ "$sc" = True ] && echo "OK context-monitor dual sigue escribiendo el fichero local" || { echo "FAIL dual no escribio build-state.json ($sc)"; fail=1; }
|
|
709
|
+
n_ev="$(find "$PROJ/.claude/state/outbox" -maxdepth 1 -name '*.json' ! -name '.dispatch-state.json' 2>/dev/null | wc -l | tr -d ' ')"
|
|
710
|
+
[ "$n_ev" = 0 ] && echo "OK context-monitor dual sin slice_id NO encola handoff_recorded (evita poison pill)" || { echo "FAIL dual encolo handoff_recorded sin slice_id (n=$n_ev)"; fail=1; }
|
|
711
|
+
|
|
712
|
+
rm -f "$PROJ/.claude/state/build-state.json"
|
|
713
|
+
rm -f "$PROJ"/.claude/state/outbox/*.json 2>/dev/null
|
|
714
|
+
rm -f "$PROJ"/.claude/state/.handoff-* 2>/dev/null
|
|
715
|
+
# Restaura la caché de proyección que el bloque siguiente (statusline-bridge) da por sentada.
|
|
716
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{},\"wiring_failing\":[{\"item_id\":\"w1\",\"kind\":\"e2e\"}],\"resume_hint\":null},\"nudges\":[],\"context\":{\"version\":9}}"
|
|
717
|
+
|
|
718
|
+
# --- statusline-bridge.sh: versión de contexto, stale y lease ---
|
|
719
|
+
out="$(echo '{"session_id":"sl1","context_window":{"remaining_percentage":40}}' | bash "$ROOT/hooks/build/statusline-bridge.sh" 2>/dev/null)"
|
|
720
|
+
echo "$out" | grep -q 'ctx 40%' && echo "OK statusline conserva el porcentaje de contexto" || { echo "FAIL statusline ctx% ($out)"; fail=1; }
|
|
721
|
+
echo "$out" | grep -q 'v9' && echo "OK statusline muestra la version de contexto" || { echo "FAIL statusline version ($out)"; fail=1; }
|
|
722
|
+
|
|
723
|
+
runtime_projection_write '{"fetched_at":"2000-01-01T00:00:00Z","project":{},"active_slice":null,"nudges":[],"context":{"version":9}}'
|
|
724
|
+
out="$(echo '{"session_id":"sl2","context_window":{"remaining_percentage":40}}' | bash "$ROOT/hooks/build/statusline-bridge.sh" 2>/dev/null)"
|
|
725
|
+
echo "$out" | grep -q 'stale' && echo "OK statusline marca stale" || { echo "FAIL statusline stale ($out)"; fail=1; }
|
|
726
|
+
|
|
727
|
+
echo '{"lease_lost": true}' > "$PROJ/.claude/state/heartbeat-status.json"
|
|
728
|
+
out="$(echo '{"session_id":"sl3","context_window":{"remaining_percentage":40}}' | bash "$ROOT/hooks/build/statusline-bridge.sh" 2>/dev/null)"
|
|
729
|
+
echo "$out" | grep -q 'lease' && echo "OK statusline marca lease perdido" || { echo "FAIL statusline lease ($out)"; fail=1; }
|
|
730
|
+
rm -f "$PROJ/.claude/state/heartbeat-status.json"
|
|
731
|
+
|
|
732
|
+
# legacy: la statusline vuelve a su forma v0.8.5 (sin marcas de runtime).
|
|
733
|
+
out="$(echo '{"session_id":"sl4","context_window":{"remaining_percentage":40}}' | TRYCORE_RUNTIME_MODE=legacy bash "$ROOT/hooks/build/statusline-bridge.sh" 2>/dev/null)"
|
|
734
|
+
[ "$out" = "🏗️ build · ctx 40%" ] && echo "OK statusline legacy intacta" || { echo "FAIL statusline legacy ($out)"; fail=1; }
|
|
735
|
+
unset TMPDIR
|
|
736
|
+
|
|
737
|
+
# --- Stop adelgazado: nudges del runtime ---
|
|
738
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{},
|
|
739
|
+
\"active_slice\":{\"slice_id\":\"sl-11\",\"epic_code\":\"EP-009\",\"hus\":[\"HU-021\"],\"phase\":\"green\",
|
|
740
|
+
\"gates\":{\"dor\":true,\"tdd\":false},\"wiring_failing\":[]},
|
|
741
|
+
\"nudges\":[{\"kind\":\"reflect\",\"message\":\"2 slices archivados sin reflexionar\"},
|
|
742
|
+
{\"kind\":\"release_gate\",\"message\":\"3 épicas sin auditar desde el último release\"},
|
|
743
|
+
{\"kind\":\"info\",\"message\":\"ruido que no debe salir por los nudges tipados\"}]}"
|
|
744
|
+
|
|
745
|
+
out="$(bash "$ROOT/hooks/build/build-gate-check.sh" 2>&1)"
|
|
746
|
+
echo "$out" | grep -q 'tdd' && echo "OK build-gate-check runtime lista gates abiertos" || { echo "FAIL build-gate-check runtime ($out)"; fail=1; }
|
|
747
|
+
out="$(bash "$ROOT/hooks/build/reflect-nudge.sh" 2>&1)"
|
|
748
|
+
echo "$out" | grep -q 'sin reflexionar' && echo "OK reflect-nudge renderiza el nudge del runtime" || { echo "FAIL reflect-nudge ($out)"; fail=1; }
|
|
749
|
+
echo "$out" | grep -q 'ruido' && { echo "FAIL reflect-nudge saca nudges de otro tipo"; fail=1; } || echo "OK reflect-nudge filtra por tipo"
|
|
750
|
+
out="$(bash "$ROOT/hooks/build/release-gate-nudge.sh" 2>&1)"
|
|
751
|
+
echo "$out" | grep -q 'sin auditar' && echo "OK release-gate-nudge renderiza el nudge del runtime" || { echo "FAIL release-gate-nudge ($out)"; fail=1; }
|
|
752
|
+
|
|
753
|
+
# Los tres son no-bloqueantes SIEMPRE (exit 0), pase lo que pase.
|
|
754
|
+
for h in build-gate-check reflect-nudge release-gate-nudge; do
|
|
755
|
+
bash "$ROOT/hooks/build/$h.sh" >/dev/null 2>&1
|
|
756
|
+
[ $? -eq 0 ] && echo "OK $h exit 0" || { echo "FAIL $h no salio 0"; fail=1; }
|
|
757
|
+
done
|
|
758
|
+
|
|
759
|
+
# Sin nudges y sin slice: silencio (nada que decir).
|
|
760
|
+
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{},\"active_slice\":null,\"nudges\":[]}"
|
|
761
|
+
out="$(bash "$ROOT/hooks/build/reflect-nudge.sh" 2>&1; bash "$ROOT/hooks/build/build-gate-check.sh" 2>&1)"
|
|
762
|
+
[ -z "$out" ] && echo "OK Stop silencioso sin nudges" || { echo "FAIL Stop ruidoso ($out)"; fail=1; }
|
|
763
|
+
|
|
764
|
+
# --- session-stop.sh: sentinela de flush, sin red ---
|
|
765
|
+
export TRYCORE_HEARTBEAT_TICK_S=1 TRYCORE_HEARTBEAT_INTERVAL_S=1
|
|
766
|
+
rm -f "$TMP/curl-was-called" "$PROJ/.claude/state/outbox/.flush-request"
|
|
767
|
+
mkdir -p "$TMP/fakebin"
|
|
768
|
+
echo '#!/bin/bash' > "$TMP/fakebin/curl" && echo 'touch "$FAKE_CURL_MARK"' >> "$TMP/fakebin/curl" && chmod +x "$TMP/fakebin/curl"
|
|
769
|
+
FAKE_CURL_MARK="$TMP/curl-was-called" PATH="$TMP/fakebin:$PATH" bash "$ROOT/hooks/build/session-stop.sh" >/dev/null 2>&1
|
|
770
|
+
rc=$?
|
|
771
|
+
[ $rc -eq 0 ] && echo "OK session-stop exit 0" || { echo "FAIL session-stop rc ($rc)"; fail=1; }
|
|
772
|
+
[ -f "$PROJ/.claude/state/outbox/.flush-request" ] && echo "OK session-stop deja el sentinela de flush" || { echo "FAIL sin sentinela"; fail=1; }
|
|
773
|
+
[ ! -f "$TMP/curl-was-called" ] && echo "OK session-stop no toca la red" || { echo "FAIL session-stop abrio socket"; fail=1; }
|
|
774
|
+
|
|
775
|
+
rm -f "$PROJ/.claude/state/outbox/.flush-request"
|
|
776
|
+
TRYCORE_RUNTIME_MODE=legacy bash "$ROOT/hooks/build/session-stop.sh" >/dev/null 2>&1
|
|
777
|
+
[ ! -f "$PROJ/.claude/state/outbox/.flush-request" ] && echo "OK session-stop no-op en legacy" || { echo "FAIL session-stop actuo en legacy"; fail=1; }
|
|
778
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
779
|
+
unset TRYCORE_HEARTBEAT_TICK_S TRYCORE_HEARTBEAT_INTERVAL_S
|
|
780
|
+
|
|
781
|
+
# --- registro en los tres canales, sincronizados entre sí ---
|
|
782
|
+
sync_check="$(ROOT="$ROOT" python3 <<'PY' 2>&1
|
|
783
|
+
import json,os,re,sys
|
|
784
|
+
root=os.environ["ROOT"]
|
|
785
|
+
ts=open(os.path.join(root,"src/lib/settings-merge.ts")).read()
|
|
786
|
+
block=re.search(r"HOOK_SPECS:\s*HookSpec\[\]\s*=\s*\[(.*?)\n\];", ts, re.S)
|
|
787
|
+
if not block:
|
|
788
|
+
print("FALLO: no se pudo leer HOOK_SPECS de settings-merge.ts"); sys.exit(0)
|
|
789
|
+
ts_specs={}
|
|
790
|
+
for m in re.finditer(r"\{\s*event:\s*'([^']+)',\s*matcher:\s*'([^']*)',\s*scripts:\s*\[([^\]]*)\]", block.group(1)):
|
|
791
|
+
ev,matcher,scripts=m.group(1),m.group(2),m.group(3)
|
|
792
|
+
ts_specs[(ev,matcher)]=[s.strip().strip("'") for s in scripts.split(",") if s.strip()]
|
|
793
|
+
|
|
794
|
+
def from_json(path):
|
|
795
|
+
d=json.load(open(os.path.join(root,path)))
|
|
796
|
+
out={}
|
|
797
|
+
for ev,groups in (d.get("hooks") or {}).items():
|
|
798
|
+
for g in groups:
|
|
799
|
+
key=(ev,g.get("matcher",""))
|
|
800
|
+
out.setdefault(key,[])
|
|
801
|
+
for h in (g.get("hooks") or []):
|
|
802
|
+
out[key].append(h["command"].rstrip('"').split("/hooks/build/")[-1])
|
|
803
|
+
return out
|
|
804
|
+
|
|
805
|
+
plugin=from_json("hooks/build-harness.json")
|
|
806
|
+
tmpl=from_json("templates/settings-hooks.template.json")
|
|
807
|
+
problemas=[]
|
|
808
|
+
for key in set(list(ts_specs.keys())+list(plugin.keys())+list(tmpl.keys())):
|
|
809
|
+
a,b,c=ts_specs.get(key),plugin.get(key),tmpl.get(key)
|
|
810
|
+
if not (a==b==c):
|
|
811
|
+
problemas.append(f"{key}: ts={a} plugin={b} template={c}")
|
|
812
|
+
esperado={
|
|
813
|
+
("SessionStart","startup|clear|compact"):["session-start.sh","load-build-state.sh"],
|
|
814
|
+
("PreToolUse","Bash"):["gitflow-guard.sh"],
|
|
815
|
+
("PreToolUse","Write|Edit|MultiEdit"):["stack-guard.sh","scaffold-guard.sh","design-source-guard.sh"],
|
|
816
|
+
("PostToolUse","Write|Edit|MultiEdit"):["lint-typecheck.sh","coherence-flag.sh"],
|
|
817
|
+
("PostToolUse","Bash|Edit|Write|MultiEdit|Task"):["context-monitor.sh","event-emitter.sh"],
|
|
818
|
+
("PreCompact",".*"):["context-monitor.sh"],
|
|
819
|
+
("Stop",".*"):["build-gate-check.sh","reflect-nudge.sh","release-gate-nudge.sh","dual-compare.sh","context-monitor.sh","session-stop.sh"],
|
|
820
|
+
}
|
|
821
|
+
if ts_specs!=esperado:
|
|
822
|
+
problemas.append(f"settings-merge.ts no coincide con el mapa objetivo: {ts_specs}")
|
|
823
|
+
print("SYNC-OK" if not problemas else "SYNC-FAIL: "+" | ".join(problemas))
|
|
824
|
+
PY
|
|
825
|
+
)"
|
|
826
|
+
echo "$sync_check" | grep -q '^SYNC-OK$' && echo "OK los tres canales registran los mismos hooks" || { echo "FAIL registro desincronizado: $sync_check"; fail=1; }
|
|
827
|
+
|
|
828
|
+
# Los scripts registrados existen y son ejecutables.
|
|
829
|
+
for s in session-start.sh load-build-state.sh gitflow-guard.sh stack-guard.sh scaffold-guard.sh \
|
|
830
|
+
design-source-guard.sh lint-typecheck.sh coherence-flag.sh context-monitor.sh \
|
|
831
|
+
event-emitter.sh build-gate-check.sh reflect-nudge.sh release-gate-nudge.sh dual-compare.sh session-stop.sh \
|
|
832
|
+
context-sync.sh heartbeat.sh; do
|
|
833
|
+
[ -x "$ROOT/hooks/build/$s" ] || { echo "FAIL $s no existe o no es ejecutable"; fail=1; }
|
|
834
|
+
done
|
|
835
|
+
echo "OK todos los hooks del mapa existen y son ejecutables"
|
|
836
|
+
|
|
837
|
+
# Checklist de aceptación v0.9.0 [EP-OR-08-E, plan-migracion-harness-v0.9.md §3]: "Los 4
|
|
838
|
+
# guards bloqueantes conservan latencia local (sin red en el camino de PreToolUse)". Ratchet
|
|
839
|
+
# estático: ninguno de los 4 debe llamar curl/runtime_get/runtime_post/runtime_put/
|
|
840
|
+
# _runtime_http/agent_context_fetch_and_cache — ni directo ni transitivo por lo que sourcean.
|
|
841
|
+
for g in gitflow-guard.sh stack-guard.sh scaffold-guard.sh design-source-guard.sh; do
|
|
842
|
+
if grep -qE '\b(curl|runtime_get|runtime_post|runtime_put|_runtime_http|agent_context_fetch_and_cache)\b' "$ROOT/hooks/build/$g"; then
|
|
843
|
+
echo "FAIL $g toca la red -- PreToolUse debe permanecer 100% local (spec §3 checklist v0.9.0)"
|
|
844
|
+
fail=1
|
|
845
|
+
fi
|
|
846
|
+
if grep -qE "^\s*source .*lib/runtime-client\.sh" "$ROOT/hooks/build/$g"; then
|
|
847
|
+
echo "FAIL $g sourcea runtime-client.sh -- solo config.sh/state-io.sh/projection.sh son válidos en PreToolUse"
|
|
848
|
+
fail=1
|
|
849
|
+
fi
|
|
850
|
+
done
|
|
851
|
+
echo "OK los 4 guards de PreToolUse no tocan la red (ni directo ni por runtime-client.sh)"
|
|
852
|
+
|
|
853
|
+
exit $fail
|