@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
|
@@ -53,9 +53,15 @@ check() { # <ruta relativa a .claude> <descripción>
|
|
|
53
53
|
|
|
54
54
|
check "hooks/build/reconcile-build-state.py"
|
|
55
55
|
check "hooks/build/lib/state-io.sh"
|
|
56
|
+
check "hooks/build/lib/config.sh"
|
|
56
57
|
check "hooks/build/context-monitor.sh"
|
|
57
58
|
check "hooks/build/statusline-bridge.sh"
|
|
58
59
|
check "scripts/lib/front-plan.py"
|
|
60
|
+
check "hooks/build/slice-ops.sh"
|
|
61
|
+
check "hooks/build/release-ops.sh"
|
|
62
|
+
check "hooks/build/lib/runtime-ops.sh"
|
|
63
|
+
check "hooks/build/dual-compare.sh"
|
|
64
|
+
check "scripts/lib/graph-bundle.py"
|
|
59
65
|
|
|
60
66
|
# 3) Bit ejecutable en los .py/.sh copiados (modo copy no preserva +x por defecto — el
|
|
61
67
|
# instalador debe forzarlo con chmodExec tras copiar).
|
|
@@ -71,7 +77,12 @@ checkexec() {
|
|
|
71
77
|
}
|
|
72
78
|
checkexec "hooks/build/reconcile-build-state.py"
|
|
73
79
|
checkexec "hooks/build/lib/state-io.sh"
|
|
80
|
+
checkexec "hooks/build/lib/config.sh"
|
|
74
81
|
checkexec "scripts/lib/front-plan.py"
|
|
82
|
+
checkexec "hooks/build/slice-ops.sh"
|
|
83
|
+
checkexec "hooks/build/release-ops.sh"
|
|
84
|
+
checkexec "hooks/build/dual-compare.sh"
|
|
85
|
+
checkexec "scripts/lib/graph-bundle.py"
|
|
75
86
|
|
|
76
87
|
# 4) `update` (canal de refresco tras `npm i` del paquete) también debe dejarlos — re-correr
|
|
77
88
|
# sobre la misma instalación no debe romper nada ni perder archivos.
|
|
@@ -84,7 +95,12 @@ if [ $rc -ne 0 ]; then
|
|
|
84
95
|
fi
|
|
85
96
|
check "hooks/build/reconcile-build-state.py"
|
|
86
97
|
check "hooks/build/lib/state-io.sh"
|
|
98
|
+
check "hooks/build/lib/config.sh"
|
|
87
99
|
check "scripts/lib/front-plan.py"
|
|
100
|
+
check "hooks/build/slice-ops.sh"
|
|
101
|
+
check "hooks/build/release-ops.sh"
|
|
102
|
+
check "hooks/build/dual-compare.sh"
|
|
103
|
+
check "scripts/lib/graph-bundle.py"
|
|
88
104
|
|
|
89
105
|
# project_kind: el TMP de init es un directorio vacío → greenfield detectado (auto)
|
|
90
106
|
if python3 -c "
|
|
@@ -138,6 +154,25 @@ else
|
|
|
138
154
|
echo "FAIL install: project_kind no detectado como brownfield con historial git"
|
|
139
155
|
fail=1
|
|
140
156
|
fi
|
|
157
|
+
|
|
158
|
+
# asset-types.json [EP-OR-08-D]: versionado como schema/README, sembrado en .claude/.
|
|
159
|
+
[ -f "$TMP_BF/.claude/asset-types.json" ] \
|
|
160
|
+
&& echo "OK install: .claude/asset-types.json sembrado" \
|
|
161
|
+
|| { echo "FAIL install: .claude/asset-types.json NO sembrado"; fail=1; }
|
|
162
|
+
|
|
163
|
+
# .gitignore [EP-OR-08-D]: las credenciales del runtime y sus derivados nunca deben
|
|
164
|
+
# poder llegar a un \`git add\` descuidado (repo git real: TMP_BF ya tiene uno).
|
|
165
|
+
GI="$TMP_BF/.gitignore"
|
|
166
|
+
if [ -f "$GI" ] \
|
|
167
|
+
&& grep -q '^\.claude/state/runtime\.credentials$' "$GI" \
|
|
168
|
+
&& grep -q '^\.claude/state/outbox/$' "$GI" \
|
|
169
|
+
&& grep -q '^\.claude/state/context\.lock$' "$GI" \
|
|
170
|
+
&& grep -q '^\.claude/state/runtime-projection\.json$' "$GI"; then
|
|
171
|
+
echo "OK install: .gitignore protege runtime.credentials/outbox/lock/projection"
|
|
172
|
+
else
|
|
173
|
+
echo "FAIL install: .gitignore no protege los artefactos del runtime"
|
|
174
|
+
fail=1
|
|
175
|
+
fi
|
|
141
176
|
rm -rf "$TMP_BF"
|
|
142
177
|
|
|
143
178
|
# 6) Ambiguo: manifiesto + código pero SIN repo git (sin historial) → project_kind null.
|
|
@@ -174,4 +209,26 @@ else
|
|
|
174
209
|
fi
|
|
175
210
|
rm -rf "$TMP_AMB"
|
|
176
211
|
|
|
212
|
+
# 7) `migrate` [EP-OR-08-D]: prepara el bundle de estado sin tocar la red (TMP ya tiene
|
|
213
|
+
# build-state.json vacío sembrado por el init de la sección 2/3).
|
|
214
|
+
node "$ROOT/dist/cli.js" migrate "$TMP" --project-ref acme >"$TMP/.migrate.log" 2>&1
|
|
215
|
+
rc=$?
|
|
216
|
+
BUNDLE="$TMP/.claude/state/migration-bundle.json"
|
|
217
|
+
if [ $rc -eq 0 ] && [ -f "$BUNDLE" ] \
|
|
218
|
+
&& python3 -c "
|
|
219
|
+
import json
|
|
220
|
+
d=json.load(open('$BUNDLE'))
|
|
221
|
+
assert d['kind']=='state', d
|
|
222
|
+
assert d['bundle_version']==2, d
|
|
223
|
+
assert d['project_ref']=='acme', d
|
|
224
|
+
assert d['history']==[], d
|
|
225
|
+
assert d['releases']==[], d
|
|
226
|
+
" 2>/dev/null; then
|
|
227
|
+
echo "OK install: migrate prepara el bundle de estado (kind/version/project_ref/history/releases)"
|
|
228
|
+
else
|
|
229
|
+
echo "FAIL install: migrate no preparó el bundle de estado esperado (rc=$rc)"
|
|
230
|
+
cat "$TMP/.migrate.log"
|
|
231
|
+
fail=1
|
|
232
|
+
fi
|
|
233
|
+
|
|
177
234
|
exit $fail
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
4
|
+
source "$ROOT/hooks/build/lib/runtime-client.sh" 2>/dev/null || { echo "FAIL no existe lib/runtime-client.sh"; exit 1; }
|
|
5
|
+
fail=0
|
|
6
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
|
|
8
|
+
# --- runtime_mode ---
|
|
9
|
+
echo '{}' > "$TMP/cfg.json"
|
|
10
|
+
got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" runtime_mode)"
|
|
11
|
+
[ "$got" = legacy ] && echo "OK runtime_mode default legacy" || { echo "FAIL runtime_mode default ($got)"; fail=1; }
|
|
12
|
+
|
|
13
|
+
echo '{"runtime":{"mode":"dual"}}' > "$TMP/cfg.json"
|
|
14
|
+
got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" runtime_mode)"
|
|
15
|
+
[ "$got" = dual ] && echo "OK runtime_mode desde config" || { echo "FAIL runtime_mode config ($got)"; fail=1; }
|
|
16
|
+
|
|
17
|
+
got="$(TRYCORE_RUNTIME_MODE=runtime BUILD_CONFIG_FILE="$TMP/cfg.json" runtime_mode)"
|
|
18
|
+
[ "$got" = runtime ] && echo "OK runtime_mode env gana a config" || { echo "FAIL runtime_mode env ($got)"; fail=1; }
|
|
19
|
+
|
|
20
|
+
# --- credenciales ---
|
|
21
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj"
|
|
22
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
23
|
+
|
|
24
|
+
got="$(runtime_field project_token)"
|
|
25
|
+
[ "$got" = "" ] && echo "OK runtime_field sin fichero" || { echo "FAIL runtime_field sin fichero ($got)"; fail=1; }
|
|
26
|
+
|
|
27
|
+
runtime_credentials_merge '{"runtime_url":"http://127.0.0.1:9","project_token":"tok-abc"}'
|
|
28
|
+
perm="$(stat -f '%Lp' "$CLAUDE_PROJECT_DIR/.claude/state/runtime.credentials" 2>/dev/null || stat -c '%a' "$CLAUDE_PROJECT_DIR/.claude/state/runtime.credentials")"
|
|
29
|
+
[ "$perm" = 600 ] && echo "OK credenciales 0600" || { echo "FAIL permisos ($perm)"; fail=1; }
|
|
30
|
+
got="$(runtime_field project_token)"
|
|
31
|
+
[ "$got" = "tok-abc" ] && echo "OK runtime_field lee tras merge" || { echo "FAIL runtime_field tras merge ($got)"; fail=1; }
|
|
32
|
+
|
|
33
|
+
runtime_credentials_merge '{"agent_key":"ak-1"}'
|
|
34
|
+
got="$(runtime_field project_token)"
|
|
35
|
+
[ "$got" = "tok-abc" ] && echo "OK merge preserva claves previas" || { echo "FAIL merge no preserva ($got)"; fail=1; }
|
|
36
|
+
got="$(runtime_field agent_key)"
|
|
37
|
+
[ "$got" = "ak-1" ] && echo "OK merge añade clave nueva" || { echo "FAIL merge no añade ($got)"; fail=1; }
|
|
38
|
+
|
|
39
|
+
unset CLAUDE_PROJECT_DIR
|
|
40
|
+
|
|
41
|
+
# --- HTTP contra stub local ---
|
|
42
|
+
PORT="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
43
|
+
cat > "$TMP/routes.json" <<JSON
|
|
44
|
+
{"GET /ping": {"status": 200, "body": {"ok": true}},
|
|
45
|
+
"POST /echo": {"status": 200, "body": {"received": true}}}
|
|
46
|
+
JSON
|
|
47
|
+
: > "$TMP/calls.jsonl"
|
|
48
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT" "$TMP/routes.json" "$TMP/calls.jsonl" &
|
|
49
|
+
STUB_PID=$!
|
|
50
|
+
trap 'kill $STUB_PID 2>/dev/null; rm -rf "$TMP"' EXIT
|
|
51
|
+
sleep 0.3
|
|
52
|
+
|
|
53
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj2"
|
|
54
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
55
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT\",\"project_token\":\"tok-xyz\"}"
|
|
56
|
+
|
|
57
|
+
body="$(runtime_get /ping)"
|
|
58
|
+
echo "$body" | grep -q '"ok": true' && echo "OK runtime_get body" || { echo "FAIL runtime_get body ($body)"; fail=1; }
|
|
59
|
+
st="$(runtime_http_status)"
|
|
60
|
+
[ "$st" = 200 ] && echo "OK runtime_get status" || { echo "FAIL runtime_get status ($st)"; fail=1; }
|
|
61
|
+
|
|
62
|
+
body="$(runtime_post /echo '{"a":1}')"
|
|
63
|
+
echo "$body" | grep -q '"received": true' && echo "OK runtime_post body" || { echo "FAIL runtime_post body ($body)"; fail=1; }
|
|
64
|
+
|
|
65
|
+
auth_sent="$(tail -1 "$TMP/calls.jsonl" | python3 -c 'import json,sys;print(json.load(sys.stdin)["auth"])')"
|
|
66
|
+
[ "$auth_sent" = "Bearer tok-xyz" ] && echo "OK Authorization Bearer" || { echo "FAIL Authorization ($auth_sent)"; fail=1; }
|
|
67
|
+
|
|
68
|
+
kill "$STUB_PID" 2>/dev/null; wait "$STUB_PID" 2>/dev/null
|
|
69
|
+
|
|
70
|
+
# offline: puerto cerrado, nunca debe colgar ni lanzar
|
|
71
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:1\"}"
|
|
72
|
+
body="$(runtime_get /ping)"
|
|
73
|
+
st="$(runtime_http_status)"
|
|
74
|
+
[ "$st" = 000 ] && echo "OK offline status 000" || { echo "FAIL offline status ($st)"; fail=1; }
|
|
75
|
+
|
|
76
|
+
unset CLAUDE_PROJECT_DIR
|
|
77
|
+
|
|
78
|
+
# --- runtime_register ---
|
|
79
|
+
PORT2="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
80
|
+
cat > "$TMP/routes2.json" <<JSON
|
|
81
|
+
{"POST /agents/register": {"status": 200, "body": {"agent_key": "ak-9", "project_id": "proj-9", "poll_interval_s": 30, "lease_ttl_s": 300, "manifest_hash": "sha256:abc"}}}
|
|
82
|
+
JSON
|
|
83
|
+
: > "$TMP/calls2.jsonl"
|
|
84
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT2" "$TMP/routes2.json" "$TMP/calls2.jsonl" &
|
|
85
|
+
STUB_PID2=$!
|
|
86
|
+
trap 'kill $STUB_PID 2>/dev/null; kill $STUB_PID2 2>/dev/null; rm -rf "$TMP"' EXIT
|
|
87
|
+
sleep 0.3
|
|
88
|
+
|
|
89
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj3"
|
|
90
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
91
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT2\",\"project_token\":\"tok-r\"}"
|
|
92
|
+
|
|
93
|
+
runtime_register "0.9.0"
|
|
94
|
+
rc=$?
|
|
95
|
+
[ $rc -eq 0 ] && echo "OK runtime_register rc 0" || { echo "FAIL runtime_register rc ($rc)"; fail=1; }
|
|
96
|
+
got="$(runtime_field agent_key)"
|
|
97
|
+
[ "$got" = "ak-9" ] && echo "OK runtime_register persiste agent_key" || { echo "FAIL agent_key ($got)"; fail=1; }
|
|
98
|
+
got="$(runtime_field lease_ttl_s)"
|
|
99
|
+
[ "$got" = 300 ] && echo "OK runtime_register persiste lease_ttl_s" || { echo "FAIL lease_ttl_s ($got)"; fail=1; }
|
|
100
|
+
got="$(runtime_field project_token)"
|
|
101
|
+
[ "$got" = "tok-r" ] && echo "OK runtime_register preserva project_token" || { echo "FAIL project_token pisado ($got)"; fail=1; }
|
|
102
|
+
|
|
103
|
+
hv_sent="$(tail -1 "$TMP/calls2.jsonl" | python3 -c 'import json,sys;print(json.loads(json.load(sys.stdin)["body"])["harness_version"])')"
|
|
104
|
+
[ "$hv_sent" = "0.9.0" ] && echo "OK register envia harness_version" || { echo "FAIL harness_version no enviado ($hv_sent)"; fail=1; }
|
|
105
|
+
|
|
106
|
+
# 409 incompatible_version: no debe corromper credenciales existentes, rc distinguible
|
|
107
|
+
cat > "$TMP/routes2.json" <<JSON
|
|
108
|
+
{"POST /agents/register": {"status": 409, "body": {"error": "incompatible_version", "min_version": "0.9.0"}}}
|
|
109
|
+
JSON
|
|
110
|
+
runtime_register "0.8.5"
|
|
111
|
+
rc=$?
|
|
112
|
+
[ $rc -eq 2 ] && echo "OK runtime_register 409 rc 2" || { echo "FAIL 409 rc ($rc)"; fail=1; }
|
|
113
|
+
got="$(runtime_field agent_key)"
|
|
114
|
+
[ "$got" = "ak-9" ] && echo "OK 409 no corrompe credenciales" || { echo "FAIL 409 corrompio credenciales ($got)"; fail=1; }
|
|
115
|
+
|
|
116
|
+
kill "$STUB_PID2" 2>/dev/null; wait "$STUB_PID2" 2>/dev/null
|
|
117
|
+
unset CLAUDE_PROJECT_DIR
|
|
118
|
+
|
|
119
|
+
# --- context.lock ---
|
|
120
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj4"
|
|
121
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
122
|
+
|
|
123
|
+
got="$(runtime_lock_manifest_hash)"
|
|
124
|
+
[ "$got" = "" ] && echo "OK lock vacio sin fichero" || { echo "FAIL lock vacio ($got)"; fail=1; }
|
|
125
|
+
|
|
126
|
+
runtime_lock_write "sha256:deadbeef" '[{"path":"config/stack-allowlist.json","sha256":"aa"}]'
|
|
127
|
+
got="$(runtime_lock_manifest_hash)"
|
|
128
|
+
[ "$got" = "sha256:deadbeef" ] && echo "OK lock_write + manifest_hash" || { echo "FAIL lock_write ($got)"; fail=1; }
|
|
129
|
+
got="$(python3 -c "import json;print(len(json.load(open('$CLAUDE_PROJECT_DIR/.claude/state/context.lock'))['files']))")"
|
|
130
|
+
[ "$got" = 1 ] && echo "OK lock_write persiste files" || { echo "FAIL lock files ($got)"; fail=1; }
|
|
131
|
+
|
|
132
|
+
unset CLAUDE_PROJECT_DIR
|
|
133
|
+
|
|
134
|
+
# --- caché de proyección ---
|
|
135
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj5"
|
|
136
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
137
|
+
|
|
138
|
+
got="$(runtime_projection_read)"
|
|
139
|
+
[ "$got" = "{}" ] && echo "OK projection_read vacio" || { echo "FAIL projection_read vacio ($got)"; fail=1; }
|
|
140
|
+
|
|
141
|
+
runtime_projection_write '{"phase":"tdd","gates":{"dor":true}}'
|
|
142
|
+
got="$(runtime_projection_read | python3 -c 'import json,sys;print(json.load(sys.stdin)["phase"])')"
|
|
143
|
+
[ "$got" = "tdd" ] && echo "OK projection_write + read" || { echo "FAIL projection_write ($got)"; fail=1; }
|
|
144
|
+
|
|
145
|
+
# fichero JSON corrupto: debe degradar a {}
|
|
146
|
+
echo "not json at all" > "$CLAUDE_PROJECT_DIR/.claude/state/runtime-projection.json"
|
|
147
|
+
got="$(runtime_projection_read)"
|
|
148
|
+
[ "$got" = "{}" ] && echo "OK projection_read JSON corrupto" || { echo "FAIL projection_read corrupto ($got)"; fail=1; }
|
|
149
|
+
|
|
150
|
+
unset CLAUDE_PROJECT_DIR
|
|
151
|
+
|
|
152
|
+
# --- outbox: enqueue + cota ---
|
|
153
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj6"
|
|
154
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
155
|
+
|
|
156
|
+
runtime_enqueue_event "checkpoint_created" '{"branch":"feature/x"}'
|
|
157
|
+
n="$(find "$CLAUDE_PROJECT_DIR/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
158
|
+
[ "$n" = 1 ] && echo "OK enqueue crea archivo" || { echo "FAIL enqueue ($n)"; fail=1; }
|
|
159
|
+
cid1="$(python3 -c "import json,glob;print(json.load(open(glob.glob('$CLAUDE_PROJECT_DIR/.claude/state/outbox/*.json')[0]))['client_event_id'])")"
|
|
160
|
+
[ -n "$cid1" ] && echo "OK enqueue asigna client_event_id" || { echo "FAIL client_event_id vacio"; fail=1; }
|
|
161
|
+
|
|
162
|
+
runtime_enqueue_event "checkpoint_created" '{"branch":"feature/y"}'
|
|
163
|
+
n="$(find "$CLAUDE_PROJECT_DIR/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
164
|
+
[ "$n" = 2 ] && echo "OK enqueue no pisa eventos previos" || { echo "FAIL enqueue pisa ($n)"; fail=1; }
|
|
165
|
+
|
|
166
|
+
# cota por edad: un evento evictable (tipo genérico) viejo se descarta; uno protegido sobrevive
|
|
167
|
+
old_file="$CLAUDE_PROJECT_DIR/.claude/state/outbox/00000000000000-old.json"
|
|
168
|
+
python3 -c "import json;json.dump({'client_event_id':'old-1','type':'tool_use_recorded','payload':{},'enqueued_at':'2020-01-01T00:00:00Z'},open('$old_file','w'))"
|
|
169
|
+
touch -t 202001010000 "$old_file"
|
|
170
|
+
dropped="$(runtime_outbox_enforce_cap | tail -1)"
|
|
171
|
+
[ "$dropped" -ge 1 ] && echo "OK enforce_cap descarta evento viejo evictable" || { echo "FAIL enforce_cap no descarto ($dropped)"; fail=1; }
|
|
172
|
+
[ ! -f "$old_file" ] && echo "OK evento viejo evictable eliminado" || { echo "FAIL evento viejo sigue ($old_file)"; fail=1; }
|
|
173
|
+
gap_found="$(grep -l '"type": "telemetry_gap"' "$CLAUDE_PROJECT_DIR"/.claude/state/outbox/*.json 2>/dev/null | wc -l | tr -d ' ')"
|
|
174
|
+
[ "$gap_found" -ge 1 ] && echo "OK descarte emite telemetry_gap" || { echo "FAIL sin telemetry_gap"; fail=1; }
|
|
175
|
+
still_protected="$(grep -l '"type": "checkpoint_created"' "$CLAUDE_PROJECT_DIR"/.claude/state/outbox/*.json 2>/dev/null | wc -l | tr -d ' ')"
|
|
176
|
+
[ "$still_protected" = 2 ] && echo "OK eventos protegidos sobreviven la cota" || { echo "FAIL protegidos descartados ($still_protected)"; fail=1; }
|
|
177
|
+
|
|
178
|
+
# cota alcanza MÁS ALLÁ de un protegido: el protegido es el más viejo (primero en orden
|
|
179
|
+
# por mtime) pero sobrevive por `continue`; el siguiente evictable, más joven, sí se
|
|
180
|
+
# descarta — no basta con mirar/parar en el primer archivo de la cola
|
|
181
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj6b"
|
|
182
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state/outbox"
|
|
183
|
+
protected_old="$CLAUDE_PROJECT_DIR/.claude/state/outbox/00000000000001-protected.json"
|
|
184
|
+
python3 -c "import json;json.dump({'client_event_id':'prot-1','type':'checkpoint_created','payload':{},'enqueued_at':'2019-01-01T00:00:00Z'},open('$protected_old','w'))"
|
|
185
|
+
touch -t 201901010000 "$protected_old"
|
|
186
|
+
evictable_younger="$CLAUDE_PROJECT_DIR/.claude/state/outbox/00000000000002-evictable.json"
|
|
187
|
+
python3 -c "import json;json.dump({'client_event_id':'evict-1','type':'tool_use_recorded','payload':{},'enqueued_at':'2021-01-01T00:00:00Z'},open('$evictable_younger','w'))"
|
|
188
|
+
touch -t 202101010000 "$evictable_younger"
|
|
189
|
+
runtime_outbox_enforce_cap >/dev/null
|
|
190
|
+
[ -f "$protected_old" ] && echo "OK protegido mas viejo sobrevive" || { echo "FAIL protegido mas viejo fue descartado"; fail=1; }
|
|
191
|
+
[ ! -f "$evictable_younger" ] && echo "OK eviccion alcanza mas alla del protegido" || { echo "FAIL eviccion no alcanzo al evictable mas joven"; fail=1; }
|
|
192
|
+
unset CLAUDE_PROJECT_DIR
|
|
193
|
+
|
|
194
|
+
# --- outbox: despacho con backoff ---
|
|
195
|
+
PORT3="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
196
|
+
: > "$TMP/calls3.jsonl"
|
|
197
|
+
|
|
198
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj7"
|
|
199
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
200
|
+
BUILD_CONFIG_FILE="$TMP/proj7-cfg.json"
|
|
201
|
+
echo '{"runtime":{"mode":"dual"}}' > "$BUILD_CONFIG_FILE"
|
|
202
|
+
export BUILD_CONFIG_FILE
|
|
203
|
+
|
|
204
|
+
# legacy: dispatch es no-op aunque haya cola
|
|
205
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT3\",\"project_token\":\"tok-d\"}"
|
|
206
|
+
runtime_enqueue_event "checkpoint_created" '{"n":1}'
|
|
207
|
+
TRYCORE_RUNTIME_MODE=legacy runtime_dispatch_outbox
|
|
208
|
+
n="$(find "$CLAUDE_PROJECT_DIR/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
209
|
+
[ "$n" -ge 1 ] && echo "OK dispatch no-op en legacy" || { echo "FAIL dispatch corrio en legacy"; fail=1; }
|
|
210
|
+
|
|
211
|
+
# runtime caido: reintenta sin bloquear, agenda backoff, no borra la cola
|
|
212
|
+
runtime_dispatch_outbox
|
|
213
|
+
rc=$?
|
|
214
|
+
[ $rc -eq 1 ] && echo "OK dispatch rc 1 sin servidor" || { echo "FAIL dispatch rc sin servidor ($rc)"; fail=1; }
|
|
215
|
+
n="$(find "$CLAUDE_PROJECT_DIR/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
216
|
+
[ "$n" -ge 1 ] && echo "OK dispatch preserva cola si falla" || { echo "FAIL dispatch borro cola en fallo"; fail=1; }
|
|
217
|
+
[ -f "$CLAUDE_PROJECT_DIR/.claude/state/outbox/.dispatch-state.json" ] && echo "OK dispatch agenda backoff" || { echo "FAIL sin estado de backoff"; fail=1; }
|
|
218
|
+
|
|
219
|
+
# segundo intento inmediato: respeta el backoff, no vuelve a intentar (no golpea la red)
|
|
220
|
+
before="$(find "$CLAUDE_PROJECT_DIR/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
221
|
+
runtime_dispatch_outbox
|
|
222
|
+
after="$(find "$CLAUDE_PROJECT_DIR/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
223
|
+
[ "$before" = "$after" ] && echo "OK dispatch respeta backoff" || { echo "FAIL dispatch ignoro backoff"; fail=1; }
|
|
224
|
+
|
|
225
|
+
# servidor arriba: despacha, borra SOLO lo confirmado por el server, preserva lo no
|
|
226
|
+
# confirmado (ack parcial en el mismo lote) y limpia el estado de backoff. El stub carga
|
|
227
|
+
# routes3.json UNA vez al arrancar, así que el client_event_id que va a confirmar debe
|
|
228
|
+
# conocerse ANTES de lanzarlo — se lee del primer archivo ya encolado.
|
|
229
|
+
runtime_enqueue_event "checkpoint_created" '{"n":2}'
|
|
230
|
+
cid="$(python3 -c "
|
|
231
|
+
import json,glob
|
|
232
|
+
files=sorted(p for p in glob.glob('$CLAUDE_PROJECT_DIR/.claude/state/outbox/*.json') if not p.endswith('.dispatch-state.json'))
|
|
233
|
+
print(json.load(open(files[0]))['client_event_id'])
|
|
234
|
+
")"
|
|
235
|
+
cat > "$TMP/routes3.json" <<JSON
|
|
236
|
+
{"POST /events": {"status": 200, "body": {"results": [{"client_event_id": "$cid", "status": "accepted"}]}}}
|
|
237
|
+
JSON
|
|
238
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT3" "$TMP/routes3.json" "$TMP/calls3.jsonl" &
|
|
239
|
+
STUB_PID3=$!
|
|
240
|
+
trap 'kill $STUB_PID 2>/dev/null; kill $STUB_PID2 2>/dev/null; kill $STUB_PID3 2>/dev/null; rm -rf "$TMP"' EXIT
|
|
241
|
+
sleep 0.3
|
|
242
|
+
python3 -c "import json;json.dump({'next_attempt_epoch':0,'attempt':0}, open('$CLAUDE_PROJECT_DIR/.claude/state/outbox/.dispatch-state.json','w'))"
|
|
243
|
+
|
|
244
|
+
runtime_dispatch_outbox
|
|
245
|
+
rc=$?
|
|
246
|
+
[ $rc -eq 0 ] && echo "OK dispatch rc 0 con servidor arriba" || { echo "FAIL dispatch rc con servidor ($rc)"; fail=1; }
|
|
247
|
+
n="$(find "$CLAUDE_PROJECT_DIR/.claude/state/outbox" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ')"
|
|
248
|
+
[ "$n" = 1 ] && echo "OK dispatch borra lo confirmado, preserva lo no confirmado" || { echo "FAIL cola tras ack parcial ($n)"; fail=1; }
|
|
249
|
+
[ ! -f "$CLAUDE_PROJECT_DIR/.claude/state/outbox/.dispatch-state.json" ] && echo "OK dispatch limpia estado de backoff" || { echo "FAIL estado de backoff persiste"; fail=1; }
|
|
250
|
+
|
|
251
|
+
kill "$STUB_PID3" 2>/dev/null; wait "$STUB_PID3" 2>/dev/null
|
|
252
|
+
unset CLAUDE_PROJECT_DIR BUILD_CONFIG_FILE
|
|
253
|
+
|
|
254
|
+
# --- [EP-OR-08-B] runtime_put + slice_id opcional en runtime_enqueue_event ---
|
|
255
|
+
PORT4="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
256
|
+
cat > "$TMP/routes4.json" <<JSON
|
|
257
|
+
{"PUT /leases/renew": {"status": 200, "body": {"expires_at": "2026-08-19T12:00:00Z"}}}
|
|
258
|
+
JSON
|
|
259
|
+
: > "$TMP/calls4.jsonl"
|
|
260
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT4" "$TMP/routes4.json" "$TMP/calls4.jsonl" &
|
|
261
|
+
STUB_PID4=$!
|
|
262
|
+
trap 'kill $STUB_PID 2>/dev/null; kill $STUB_PID2 2>/dev/null; kill $STUB_PID3 2>/dev/null; kill $STUB_PID4 2>/dev/null; rm -rf "$TMP"' EXIT
|
|
263
|
+
sleep 0.3
|
|
264
|
+
|
|
265
|
+
export CLAUDE_PROJECT_DIR="$TMP/proj8"
|
|
266
|
+
mkdir -p "$CLAUDE_PROJECT_DIR/.claude/state"
|
|
267
|
+
runtime_credentials_merge "{\"runtime_url\":\"http://127.0.0.1:$PORT4\",\"project_token\":\"tok-p\"}"
|
|
268
|
+
|
|
269
|
+
body="$(runtime_put /leases/renew '{"slice_id":"sl-1"}')"
|
|
270
|
+
echo "$body" | grep -q '"expires_at"' && echo "OK runtime_put body" || { echo "FAIL runtime_put body ($body)"; fail=1; }
|
|
271
|
+
[ "$(runtime_http_status)" = 200 ] && echo "OK runtime_put status" || { echo "FAIL runtime_put status"; fail=1; }
|
|
272
|
+
m_sent="$(tail -1 "$TMP/calls4.jsonl" | python3 -c 'import json,sys;print(json.load(sys.stdin)["method"])')"
|
|
273
|
+
[ "$m_sent" = PUT ] && echo "OK runtime_put usa metodo PUT" || { echo "FAIL metodo ($m_sent)"; fail=1; }
|
|
274
|
+
|
|
275
|
+
# slice_id como TERCER argumento opcional: clave de primer nivel del evento encolado.
|
|
276
|
+
runtime_enqueue_event "tool_use_recorded" '{"tool":"Edit"}' "sl-42"
|
|
277
|
+
sid_written="$(python3 -c "
|
|
278
|
+
import json,glob
|
|
279
|
+
files=sorted(p for p in glob.glob('$CLAUDE_PROJECT_DIR/.claude/state/outbox/*.json') if not p.endswith('.dispatch-state.json'))
|
|
280
|
+
print(json.load(open(files[0])).get('slice_id',''))
|
|
281
|
+
")"
|
|
282
|
+
[ "$sid_written" = "sl-42" ] && echo "OK enqueue escribe slice_id de primer nivel" || { echo "FAIL slice_id ($sid_written)"; fail=1; }
|
|
283
|
+
|
|
284
|
+
# Regresión de firma: con DOS argumentos el evento NO lleva slice_id (los llamadores del
|
|
285
|
+
# sub-slice A siguen funcionando sin cambios).
|
|
286
|
+
rm -f "$CLAUDE_PROJECT_DIR"/.claude/state/outbox/*.json
|
|
287
|
+
runtime_enqueue_event "checkpoint_created" '{"n":1}'
|
|
288
|
+
has_sid="$(python3 -c "
|
|
289
|
+
import json,glob
|
|
290
|
+
files=sorted(p for p in glob.glob('$CLAUDE_PROJECT_DIR/.claude/state/outbox/*.json') if not p.endswith('.dispatch-state.json'))
|
|
291
|
+
print('slice_id' in json.load(open(files[0])))
|
|
292
|
+
")"
|
|
293
|
+
[ "$has_sid" = False ] && echo "OK enqueue de 2 args no inventa slice_id" || { echo "FAIL enqueue 2 args ($has_sid)"; fail=1; }
|
|
294
|
+
|
|
295
|
+
kill "$STUB_PID4" 2>/dev/null; wait "$STUB_PID4" 2>/dev/null
|
|
296
|
+
unset CLAUDE_PROJECT_DIR
|
|
297
|
+
|
|
298
|
+
exit $fail
|
|
@@ -61,8 +61,31 @@ cat > "$TMP/bad-foundation-item.json" <<'JSON'
|
|
|
61
61
|
"checklist":[{"applies":true}]},
|
|
62
62
|
"active_slice":null,"history":[],"releases":[]}
|
|
63
63
|
JSON
|
|
64
|
+
cat > "$TMP/valid-wiring-sha.json" <<'JSON'
|
|
65
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
66
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"add-login",
|
|
67
|
+
"branch":"feature/login","phase":"dod","layer":"business",
|
|
68
|
+
"gates":{"dor":true,"tdd":true,"dod":false},"updated_at":"2026-08-27T00:00:00Z","updated_by":"test",
|
|
69
|
+
"wiring_checklist":[
|
|
70
|
+
{"id":"HU-001-AC1","kind":"hu_ac","ref":"HU-001#happy","status":"passing",
|
|
71
|
+
"evidence":"npm test -- auth.spec (3 passed)","verified_at_sha":"a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0"},
|
|
72
|
+
{"id":"INT-spa-gateway","kind":"integration_point","ref":"spa→gateway","status":"failing","evidence":""}]},
|
|
73
|
+
"history":[],"releases":[]}
|
|
74
|
+
JSON
|
|
75
|
+
cat > "$TMP/bad-wiring-sha-type.json" <<'JSON'
|
|
76
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
77
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"add-login",
|
|
78
|
+
"branch":"feature/login","phase":"dod","layer":"business",
|
|
79
|
+
"gates":{"dor":true,"tdd":true,"dod":false},"updated_at":"2026-08-27T00:00:00Z","updated_by":"test",
|
|
80
|
+
"wiring_checklist":[
|
|
81
|
+
{"id":"HU-001-AC1","kind":"hu_ac","ref":"HU-001#happy","status":"passing",
|
|
82
|
+
"evidence":"npm test -- auth.spec (3 passed)","verified_at_sha":12345}]},
|
|
83
|
+
"history":[],"releases":[]}
|
|
84
|
+
JSON
|
|
64
85
|
check "estado válido con campos nuevos" pass "$TMP/valid.json"
|
|
65
86
|
check "layer inválido rechazado" fail "$TMP/bad-layer.json"
|
|
87
|
+
check "wiring_checklist con verified_at_sha válido" pass "$TMP/valid-wiring-sha.json"
|
|
88
|
+
check "verified_at_sha no-string rechazado" fail "$TMP/bad-wiring-sha-type.json"
|
|
66
89
|
check "foundation + project_kind válidos" pass "$TMP/valid-foundation.json"
|
|
67
90
|
check "project_kind inválido rechazado" fail "$TMP/bad-project-kind.json"
|
|
68
91
|
check "checklist item sin 'item' rechazado" fail "$TMP/bad-foundation-item.json"
|
|
@@ -73,7 +96,12 @@ if [ -f "$CFG" ] && python3 -c "import json,sys; d=json.load(open('$CFG'))['cont
|
|
|
73
96
|
else echo "FAIL build-config.template.json con defaults"; fail=1; fi
|
|
74
97
|
|
|
75
98
|
CMD="$ROOT/commands/build/resume.md"
|
|
76
|
-
|
|
99
|
+
# [EP-OR-08-C] resume.md ya no menciona session_continuity literal: en dual/runtime la
|
|
100
|
+
# continuidad la trae el checkpoint del runtime (slice-ops.sh), no ese campo del fichero.
|
|
101
|
+
# El campo sigue existiendo para el camino legacy (reconcile-build-state.py lo reconcilia
|
|
102
|
+
# sin que resume.md necesite nombrarlo); aceptamos cualquiera de los dos como señal de que
|
|
103
|
+
# el comando sigue cubriendo continuidad en algún modo.
|
|
104
|
+
{ [ -f "$CMD" ] && grep -q 'reconcile-build-state.py' "$CMD" && { grep -q 'session_continuity' "$CMD" || grep -q 'checkpoint' "$CMD"; }; } \
|
|
77
105
|
&& echo "OK comando resume" || { echo "FAIL comando resume"; fail=1; }
|
|
78
106
|
|
|
79
107
|
exit $fail
|