@trycore/spec-build-harness 0.7.1 → 0.8.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 +3 -3
- package/METODOLOGIA.md +44 -0
- package/README.md +59 -5
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +6 -0
- package/commands/build/front.md +15 -0
- package/commands/build/resume.md +29 -0
- package/config/build-config.template.json +8 -0
- package/dist/commands/init.js +15 -5
- package/dist/commands/status.js +1 -0
- package/dist/commands/uninstall.js +2 -1
- package/dist/lib/paths.js +7 -0
- package/dist/lib/settings-merge.js +29 -2
- package/dist/lib/state-seed.js +14 -0
- package/docs/commands.md +15 -3
- package/docs/decisiones/2026-07-03-gsd-vs-openspec-fork-vs-rama.md +157 -0
- package/docs/flujo-harness-funcional.md +42 -0
- package/docs/flujo-harness.md +192 -0
- package/docs/hooks.md +22 -5
- package/hooks/build/build-gate-check.sh +1 -1
- package/hooks/build/context-monitor.sh +74 -0
- package/hooks/build/design-source-guard.sh +1 -1
- package/hooks/build/lib/state-io.sh +55 -0
- package/hooks/build/lint-typecheck.sh +1 -1
- package/hooks/build/load-build-state.sh +46 -2
- package/hooks/build/reconcile-build-state.py +70 -0
- package/hooks/build/reflect-nudge.sh +1 -1
- package/hooks/build/release-gate-nudge.sh +1 -1
- package/hooks/build/scaffold-guard.sh +1 -1
- package/hooks/build/stack-guard.sh +1 -1
- package/hooks/build/statusline-bridge.sh +32 -0
- package/hooks/build-harness.json +24 -0
- package/package.json +1 -1
- package/scripts/lib/front-plan.py +47 -0
- package/scripts/smoke-test.sh +12 -0
- package/scripts/tests/test-context-monitor.sh +70 -0
- package/scripts/tests/test-front-plan.sh +38 -0
- package/scripts/tests/test-install.sh +89 -0
- package/scripts/tests/test-reconciler.sh +54 -0
- package/scripts/tests/test-schema.sh +58 -0
- package/skills/building-a-slice/references/dor.md +2 -0
- package/skills/managing-parallel-front/SKILL.md +36 -0
- package/state/build-state.schema.json +50 -0
|
@@ -3,14 +3,53 @@
|
|
|
3
3
|
# Inyecta al contexto: rama actual, harness_phase, slice activo y gates abiertos.
|
|
4
4
|
# Además sincroniza harness_phase (authoring -> active) según exista package.json.
|
|
5
5
|
set -uo pipefail
|
|
6
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/state-io.sh"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
# Lee el payload de SessionStart (JSON en stdin) para extraer session_id. Fail-open:
|
|
9
|
+
# si no hay stdin o no trae session_id, seguimos sin tocar session_continuity.
|
|
10
|
+
payload="$(cat 2>/dev/null || true)"
|
|
11
|
+
|
|
12
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
13
|
+
STATE="$(state_path)"
|
|
9
14
|
BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'desconocida')"
|
|
10
15
|
|
|
11
16
|
# Determina la fase real del arnés.
|
|
12
17
|
if [ -f "$ROOT/package.json" ]; then PHASE="active"; else PHASE="authoring"; fi
|
|
13
18
|
|
|
19
|
+
# Ancla el estado a disco antes de leerlo (fail-open; nunca rompe SessionStart).
|
|
20
|
+
if [ -f "$STATE" ] && command -v python3 >/dev/null 2>&1; then
|
|
21
|
+
python3 "$(dirname "${BASH_SOURCE[0]}")/reconcile-build-state.py" "$STATE" 2>/dev/null || true
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Reset once-per-SESSION del guard de auto-handoff (A1): si esta sesión (session_id del
|
|
25
|
+
# payload de SessionStart) es distinta de la última registrada en session_continuity,
|
|
26
|
+
# reabrimos critical_recorded para que context-monitor.sh pueda volver a escribir handoff
|
|
27
|
+
# fresco en ESTA sesión. Fail-open: sin stdin/session_id/state, no se toca nada.
|
|
28
|
+
if [ -f "$STATE" ] && command -v python3 >/dev/null 2>&1; then
|
|
29
|
+
SESSION_ID="$(printf '%s' "$payload" | python3 -c '
|
|
30
|
+
import json,sys
|
|
31
|
+
try:
|
|
32
|
+
p=json.load(sys.stdin)
|
|
33
|
+
sid=p.get("session_id","")
|
|
34
|
+
print(sid if isinstance(sid,str) else "")
|
|
35
|
+
except Exception:
|
|
36
|
+
print("")
|
|
37
|
+
' 2>/dev/null || true)"
|
|
38
|
+
if [ -n "$SESSION_ID" ]; then
|
|
39
|
+
SID_JSON="$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$SESSION_ID" 2>/dev/null || true)"
|
|
40
|
+
if [ -n "$SID_JSON" ]; then
|
|
41
|
+
state_atomic_patch "$STATE" "
|
|
42
|
+
s = d.get('active_slice')
|
|
43
|
+
if isinstance(s, dict):
|
|
44
|
+
sc = s.setdefault('session_continuity', {})
|
|
45
|
+
if sc.get('last_session') != $SID_JSON:
|
|
46
|
+
sc['last_session'] = $SID_JSON
|
|
47
|
+
sc['critical_recorded'] = False
|
|
48
|
+
"
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
14
53
|
# Sincroniza harness_phase en el estado (si python3 disponible y el archivo existe).
|
|
15
54
|
# Escritura ATÓMICA + validada: este hook corre en CADA SessionStart (alta frecuencia,
|
|
16
55
|
# headless incluido); una escritura no atómica que se interrumpa truncaría la ÚNICA
|
|
@@ -79,6 +118,11 @@ else:
|
|
|
79
118
|
if log:
|
|
80
119
|
last=log[-1]
|
|
81
120
|
print(f" Última bitácora: [{last.get('by')}] {last.get('note')}")
|
|
121
|
+
sc=s.get("session_continuity") or {}
|
|
122
|
+
if sc.get("resume_hint"):
|
|
123
|
+
print(f" ▶️ Retomar: {sc.get('resume_hint')}" + (" [auto-continue]" if sc.get("auto_continue") else ""))
|
|
124
|
+
if s.get("branch_drift"):
|
|
125
|
+
print(f" ⚠️ Rama real ({s.get('branch_drift')}) != branch del slice ({s.get('branch')}).")
|
|
82
126
|
PY
|
|
83
127
|
fi
|
|
84
128
|
exit 0
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""reconcile-build-state.py — ancla el estado a la realidad (git + tests). Fail-open.
|
|
3
|
+
|
|
4
|
+
Deriva/reconcilia, nunca lanza:
|
|
5
|
+
- wiring_checklist: un item 'passing' sin 'evidence' se degrada a 'failing' (self-heal).
|
|
6
|
+
- branch_drift: si la rama git real != active_slice.branch, lo anota (no corrige).
|
|
7
|
+
- ratchet: no revierte gates booleanos true->false (solo señal explícita lo haría).
|
|
8
|
+
Uso: reconcile-build-state.py [<state-file>]
|
|
9
|
+
"""
|
|
10
|
+
import json, os, sys, subprocess, tempfile
|
|
11
|
+
|
|
12
|
+
def git_branch(root):
|
|
13
|
+
try:
|
|
14
|
+
return subprocess.run(["git","-C",root,"rev-parse","--abbrev-ref","HEAD"],
|
|
15
|
+
capture_output=True,text=True,timeout=5).stdout.strip() or None
|
|
16
|
+
except Exception:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
|
|
21
|
+
os.environ.get("CLAUDE_PROJECT_DIR",os.getcwd()), ".claude","state","build-state.json")
|
|
22
|
+
try:
|
|
23
|
+
with open(path) as fh:
|
|
24
|
+
d = json.load(fh)
|
|
25
|
+
except Exception:
|
|
26
|
+
return 0 # fail-open
|
|
27
|
+
if not isinstance(d, dict):
|
|
28
|
+
return 0
|
|
29
|
+
changed = False
|
|
30
|
+
s = d.get("active_slice")
|
|
31
|
+
if isinstance(s, dict):
|
|
32
|
+
try:
|
|
33
|
+
# 1) wiring: passing sin evidencia -> failing
|
|
34
|
+
wc = s.get("wiring_checklist")
|
|
35
|
+
wc = wc if isinstance(wc, list) else []
|
|
36
|
+
for w in wc:
|
|
37
|
+
if not isinstance(w, dict):
|
|
38
|
+
continue
|
|
39
|
+
if w.get("status") == "passing" and not (w.get("evidence") or "").strip():
|
|
40
|
+
w["status"] = "failing"; changed = True
|
|
41
|
+
# 2) branch drift
|
|
42
|
+
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(path))))
|
|
43
|
+
rb = git_branch(root)
|
|
44
|
+
if rb and s.get("branch") and rb != s["branch"] and rb not in ("HEAD",):
|
|
45
|
+
if s.get("branch_drift") != rb:
|
|
46
|
+
s["branch_drift"] = rb; changed = True
|
|
47
|
+
elif s.get("branch_drift") and rb == s.get("branch"):
|
|
48
|
+
s.pop("branch_drift", None); changed = True
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
if changed:
|
|
52
|
+
try:
|
|
53
|
+
dirn = os.path.dirname(path) or "."
|
|
54
|
+
fd, tmp = tempfile.mkstemp(dir=dirn, prefix=".build-state.", suffix=".tmp")
|
|
55
|
+
except Exception:
|
|
56
|
+
return 0
|
|
57
|
+
try:
|
|
58
|
+
with os.fdopen(fd,"w") as o:
|
|
59
|
+
json.dump(d,o,indent=2,ensure_ascii=False); o.flush(); os.fsync(o.fileno())
|
|
60
|
+
os.replace(tmp, path)
|
|
61
|
+
sys.stderr.write("reconcile: estado re-anclado a disco\n")
|
|
62
|
+
except Exception:
|
|
63
|
+
try:
|
|
64
|
+
os.unlink(tmp)
|
|
65
|
+
except OSError:
|
|
66
|
+
pass
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Determinista y barato: el razonamiento (qué se aprendió) lo hace el MODELO en /build:reflect.
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
|
|
8
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
8
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
9
9
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
10
10
|
[ -f "$STATE" ] || exit 0
|
|
11
11
|
command -v python3 >/dev/null 2>&1 || exit 0 # fail-open: jamás impide cerrar sesión
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# (lo computa la skill building-a-slice en la fase 8).
|
|
9
9
|
set -uo pipefail
|
|
10
10
|
|
|
11
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
11
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
12
12
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
13
13
|
[ -f "$STATE" ] || exit 0
|
|
14
14
|
command -v python3 >/dev/null 2>&1 || exit 0 # fail-open: jamás impide cerrar sesión
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# AUTO-ARME: si no existe build-state.json, no hay nada que vigilar -> exit 0.
|
|
8
8
|
set -uo pipefail
|
|
9
9
|
|
|
10
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
10
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
11
11
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
12
12
|
[ -f "$STATE" ] || exit 0
|
|
13
13
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# dependencias fuera de .claude/config/stack-allowlist.json (el contrato de stack del PRD).
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
|
|
8
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
8
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
9
9
|
ALLOW="$ROOT/.claude/config/stack-allowlist.json"
|
|
10
10
|
[ -f "$ALLOW" ] || exit 0
|
|
11
11
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# statusline-bridge.sh — statusLine command (canal CLI). Imprime la línea de estado
|
|
3
|
+
# y escribe el puente de contexto que lee context-monitor.sh. Fail-open siempre.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
payload="$(cat)"
|
|
6
|
+
command -v python3 >/dev/null 2>&1 || { echo "🏗️ build"; exit 0; }
|
|
7
|
+
echo "$payload" | python3 -c '
|
|
8
|
+
import json,sys,os,time,re,tempfile
|
|
9
|
+
try:
|
|
10
|
+
p=json.load(sys.stdin)
|
|
11
|
+
except Exception:
|
|
12
|
+
print("🏗️ build"); sys.exit(0)
|
|
13
|
+
sid=str(p.get("session_id","default"))
|
|
14
|
+
if re.search(r"[\\/]|\.\.", sid): sid=re.sub(r"[^A-Za-z0-9_-]","_",sid) # sanitiza path traversal
|
|
15
|
+
rem=None
|
|
16
|
+
try: rem=int(p.get("context_window",{}).get("remaining_percentage"))
|
|
17
|
+
except Exception: rem=None
|
|
18
|
+
if rem is not None:
|
|
19
|
+
d={"remaining_pct":rem,"used_pct":100-rem,"ts":int(time.time())}
|
|
20
|
+
path=os.path.join(tempfile.gettempdir(), f"claude-ctx-{sid}.json")
|
|
21
|
+
try:
|
|
22
|
+
fd,tmp=tempfile.mkstemp(dir=tempfile.gettempdir(),prefix=".ctx-",suffix=".tmp")
|
|
23
|
+
try:
|
|
24
|
+
with os.fdopen(fd,"w") as f: json.dump(d,f)
|
|
25
|
+
os.replace(tmp,path)
|
|
26
|
+
except Exception:
|
|
27
|
+
try: os.unlink(tmp)
|
|
28
|
+
except OSError: pass
|
|
29
|
+
except Exception: pass
|
|
30
|
+
tag = f"🏗️ build · ctx {rem}%" if rem is not None else "🏗️ build"
|
|
31
|
+
print(tag)
|
|
32
|
+
' 2>/dev/null || echo "🏗️ build"
|
package/hooks/build-harness.json
CHANGED
|
@@ -52,6 +52,26 @@
|
|
|
52
52
|
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/coherence-flag.sh\""
|
|
53
53
|
}
|
|
54
54
|
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"matcher": "Bash|Edit|Write|MultiEdit|Task",
|
|
58
|
+
"hooks": [
|
|
59
|
+
{
|
|
60
|
+
"type": "command",
|
|
61
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/context-monitor.sh\""
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"PreCompact": [
|
|
67
|
+
{
|
|
68
|
+
"matcher": ".*",
|
|
69
|
+
"hooks": [
|
|
70
|
+
{
|
|
71
|
+
"type": "command",
|
|
72
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/context-monitor.sh\""
|
|
73
|
+
}
|
|
74
|
+
]
|
|
55
75
|
}
|
|
56
76
|
],
|
|
57
77
|
"Stop": [
|
|
@@ -69,6 +89,10 @@
|
|
|
69
89
|
{
|
|
70
90
|
"type": "command",
|
|
71
91
|
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/release-gate-nudge.sh\""
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"type": "command",
|
|
95
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/context-monitor.sh\""
|
|
72
96
|
}
|
|
73
97
|
]
|
|
74
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trycore/spec-build-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Arnés agéntico de construcción de Trycore para Claude Code: pipeline de dos loops (slice por épica + release gate) con gates de calidad, estado compartido y OpenSpec. Compañero de @trycore/spec-product-flow. Agnóstico al proyecto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""front-plan.py — selecciona el conjunto disjunto máximo de épicas no fundacionales.
|
|
3
|
+
|
|
4
|
+
stdin: JSON [{"epica","layer","files_scope":[glob]}]
|
|
5
|
+
stdout: {"selected":[...],"serialized":[{"epica","reason"}],"excluded_foundational":[...]}
|
|
6
|
+
Determinista: orden por 'epica'. Solape de globs por prefijo de directorio o glob idéntico.
|
|
7
|
+
"""
|
|
8
|
+
import json, sys, fnmatch
|
|
9
|
+
|
|
10
|
+
def overlap(a, b):
|
|
11
|
+
for ga in a:
|
|
12
|
+
for gb in b:
|
|
13
|
+
if ga == gb or fnmatch.fnmatch(ga, gb) or fnmatch.fnmatch(gb, ga):
|
|
14
|
+
return True
|
|
15
|
+
pa, pb = ga.split("*", 1)[0], gb.split("*", 1)[0]
|
|
16
|
+
# Fail closed: a leading/empty-prefix wildcard could match anything → assume overlap.
|
|
17
|
+
if not pa or not pb:
|
|
18
|
+
return True
|
|
19
|
+
if pa.startswith(pb) or pb.startswith(pa):
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def _empty_result():
|
|
24
|
+
return {"selected": [], "serialized": [], "excluded_foundational": []}
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
try:
|
|
28
|
+
cands = json.load(sys.stdin)
|
|
29
|
+
if not isinstance(cands, list) or not all(isinstance(c, dict) for c in cands):
|
|
30
|
+
raise ValueError("entrada inválida: se esperaba una lista de objetos")
|
|
31
|
+
cands = sorted(cands, key=lambda c: c.get("epica",""))
|
|
32
|
+
excluded = [c["epica"] for c in cands if c.get("layer") == "foundational"]
|
|
33
|
+
pool = [c for c in cands if c.get("layer") != "foundational"]
|
|
34
|
+
selected, sel_scopes, serialized = [], [], []
|
|
35
|
+
for c in pool:
|
|
36
|
+
sc = c.get("files_scope") or []
|
|
37
|
+
if any(overlap(sc, s) for s in sel_scopes):
|
|
38
|
+
serialized.append({"epica": c["epica"], "reason": "solape de files_scope con épica seleccionada"})
|
|
39
|
+
else:
|
|
40
|
+
selected.append(c["epica"]); sel_scopes.append(sc)
|
|
41
|
+
print(json.dumps({"selected":selected,"serialized":serialized,"excluded_foundational":excluded}))
|
|
42
|
+
except Exception:
|
|
43
|
+
print(json.dumps(_empty_result()))
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
sys.exit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-test.sh — corre los tests de humo del motor de contexto y front.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
FAILED=0
|
|
6
|
+
|
|
7
|
+
echo "── Tests del motor de contexto y front ──"
|
|
8
|
+
for t in test-schema.sh test-reconciler.sh test-context-monitor.sh test-front-plan.sh test-install.sh; do
|
|
9
|
+
if bash "$DIR/tests/$t"; then echo "smoke: $t OK"; else echo "smoke: $t FALLÓ"; FAILED=1; fi
|
|
10
|
+
done
|
|
11
|
+
|
|
12
|
+
exit $FAILED
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
4
|
+
fail=0
|
|
5
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
6
|
+
export TMPDIR="$TMP"
|
|
7
|
+
|
|
8
|
+
# bridge: escribe puente sanitizando session_id
|
|
9
|
+
echo '{"session_id":"ab/../cd","context_window":{"remaining_percentage":22}}' \
|
|
10
|
+
| bash "$ROOT/hooks/build/statusline-bridge.sh" >/dev/null
|
|
11
|
+
if ls "$TMP"/claude-ctx-*.json >/dev/null 2>&1; then
|
|
12
|
+
b="$(ls "$TMP"/claude-ctx-*.json)"
|
|
13
|
+
case "$b" in *..*|*/claude-ctx-ab/*) echo "FAIL bridge no sanitiza"; fail=1;; *) echo "OK bridge sanitiza";; esac
|
|
14
|
+
rp="$(python3 -c "import json;print(json.load(open('$b'))['remaining_pct'])")"
|
|
15
|
+
[ "$rp" = 22 ] && echo "OK bridge remaining_pct" || { echo "FAIL bridge remaining_pct ($rp)"; fail=1; }
|
|
16
|
+
else echo "FAIL bridge no escribió puente"; fail=1; fi
|
|
17
|
+
|
|
18
|
+
# monitor: a 20% (critical<25) inyecta additionalContext y escribe handoff
|
|
19
|
+
sid="sess1"; echo "{\"remaining_pct\":20,\"used_pct\":80,\"ts\":$(date +%s)}" > "$TMP/claude-ctx-$sid.json"
|
|
20
|
+
mkdir -p "$TMP/proj/.claude/state" "$TMP/proj/.claude/config"
|
|
21
|
+
cat > "$TMP/proj/.claude/state/build-state.json" <<'JSON'
|
|
22
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
23
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"x","branch":"feature/x","phase":"tdd",
|
|
24
|
+
"gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t","progress_log":[]},
|
|
25
|
+
"history":[],"releases":[]}
|
|
26
|
+
JSON
|
|
27
|
+
echo '{"context":{"critical_pct":25,"warning_pct":35,"auto_checkpoint":false}}' > "$TMP/proj/.claude/config/build-config.json"
|
|
28
|
+
out="$(echo "{\"session_id\":\"$sid\",\"hook_event_name\":\"PostToolUse\"}" \
|
|
29
|
+
| CLAUDE_PROJECT_DIR="$TMP/proj" bash "$ROOT/hooks/build/context-monitor.sh")"
|
|
30
|
+
echo "$out" | grep -q 'additionalContext' && echo "OK monitor inyecta aviso" || { echo "FAIL monitor sin aviso"; fail=1; }
|
|
31
|
+
sc="$(python3 -c "import json;print(json.load(open('$TMP/proj/.claude/state/build-state.json'))['active_slice'].get('session_continuity',{}).get('critical_recorded'))" 2>/dev/null)"
|
|
32
|
+
[ "$sc" = True ] && echo "OK monitor handoff" || { echo "FAIL monitor handoff ($sc)"; fail=1; }
|
|
33
|
+
|
|
34
|
+
# load-build-state (SessionStart): reset once-per-SESSION del guard critical_recorded.
|
|
35
|
+
# Sesión NUEVA (last_session distinto del session_id del payload) -> reabre el guard y
|
|
36
|
+
# persiste el session_id actual.
|
|
37
|
+
PROJ="$TMP/proj-lbs"; mkdir -p "$PROJ/.claude/state"
|
|
38
|
+
cat > "$PROJ/.claude/state/build-state.json" <<'JSON'
|
|
39
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
40
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"x","branch":"feature/x","phase":"tdd",
|
|
41
|
+
"gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t",
|
|
42
|
+
"progress_log":[],"session_continuity":{"critical_recorded":true,"last_session":"old"}},
|
|
43
|
+
"history":[],"releases":[]}
|
|
44
|
+
JSON
|
|
45
|
+
echo '{"session_id":"new"}' | CLAUDE_PROJECT_DIR="$PROJ" bash "$ROOT/hooks/build/load-build-state.sh" >/dev/null
|
|
46
|
+
cr="$(python3 -c "import json;print(json.load(open('$PROJ/.claude/state/build-state.json'))['active_slice']['session_continuity']['critical_recorded'])")"
|
|
47
|
+
ls_="$(python3 -c "import json;print(json.load(open('$PROJ/.claude/state/build-state.json'))['active_slice']['session_continuity']['last_session'])")"
|
|
48
|
+
[ "$cr" = False ] && echo "OK load-build-state resetea critical_recorded en sesión nueva" || { echo "FAIL critical_recorded no se reseteó ($cr)"; fail=1; }
|
|
49
|
+
[ "$ls_" = new ] && echo "OK load-build-state persiste last_session nuevo" || { echo "FAIL last_session no persistió ($ls_)"; fail=1; }
|
|
50
|
+
|
|
51
|
+
# Sesión IDÉNTICA (last_session == session_id del payload) -> NO toca critical_recorded.
|
|
52
|
+
PROJ2="$TMP/proj-lbs-same"; mkdir -p "$PROJ2/.claude/state"
|
|
53
|
+
cat > "$PROJ2/.claude/state/build-state.json" <<'JSON'
|
|
54
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
55
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"x","branch":"feature/x","phase":"tdd",
|
|
56
|
+
"gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t",
|
|
57
|
+
"progress_log":[],"session_continuity":{"critical_recorded":true,"last_session":"s1"}},
|
|
58
|
+
"history":[],"releases":[]}
|
|
59
|
+
JSON
|
|
60
|
+
echo '{"session_id":"s1"}' | CLAUDE_PROJECT_DIR="$PROJ2" bash "$ROOT/hooks/build/load-build-state.sh" >/dev/null
|
|
61
|
+
cr2="$(python3 -c "import json;print(json.load(open('$PROJ2/.claude/state/build-state.json'))['active_slice']['session_continuity']['critical_recorded'])")"
|
|
62
|
+
[ "$cr2" = True ] && echo "OK load-build-state NO toca critical_recorded en misma sesión" || { echo "FAIL critical_recorded se tocó en misma sesión ($cr2)"; fail=1; }
|
|
63
|
+
|
|
64
|
+
# wiring: ambos canales cablean context-monitor en PostToolUse/PreCompact/Stop
|
|
65
|
+
grep -q 'PreCompact' "$ROOT/hooks/build-harness.json" && echo "OK plugin PreCompact" || { echo "FAIL plugin PreCompact"; fail=1; }
|
|
66
|
+
grep -q 'context-monitor.sh' "$ROOT/hooks/build-harness.json" && echo "OK plugin monitor" || { echo "FAIL plugin monitor"; fail=1; }
|
|
67
|
+
grep -q "PreCompact" "$ROOT/src/lib/settings-merge.ts" && echo "OK cli PreCompact" || { echo "FAIL cli PreCompact"; fail=1; }
|
|
68
|
+
grep -q "context-monitor.sh" "$ROOT/src/lib/settings-merge.ts" && echo "OK cli monitor" || { echo "FAIL cli monitor"; fail=1; }
|
|
69
|
+
|
|
70
|
+
exit $fail
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
ROOT="$(git rev-parse --show-toplevel)"; fail=0
|
|
4
|
+
run() { echo "$1" | python3 "$ROOT/scripts/lib/front-plan.py"; }
|
|
5
|
+
|
|
6
|
+
out="$(run '[{"epica":"EP-001","layer":"foundational","files_scope":["src/**"]},
|
|
7
|
+
{"epica":"EP-002","layer":"business","files_scope":["src/a/**"]},
|
|
8
|
+
{"epica":"EP-003","layer":"business","files_scope":["src/b/**"]},
|
|
9
|
+
{"epica":"EP-004","layer":"business","files_scope":["src/a/x.ts"]}]')"
|
|
10
|
+
echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
11
|
+
assert d['excluded_foundational']==['EP-001'], d
|
|
12
|
+
assert set(d['selected'])=={'EP-002','EP-003'}, d
|
|
13
|
+
assert [m['epica'] for m in d['serialized']]==['EP-004'], d
|
|
14
|
+
print('OK front-plan disjunción')" || { echo "FAIL front-plan disjunción"; fail=1; }
|
|
15
|
+
|
|
16
|
+
for bad in 'null' '[1,2,3]' '"abc"' '{"a":1}'; do
|
|
17
|
+
if out="$(echo "$bad" | python3 "$ROOT/scripts/lib/front-plan.py" 2>/dev/null)" \
|
|
18
|
+
&& echo "$out" | python3 -c "import json,sys;assert json.load(sys.stdin)['selected']==[]" 2>/dev/null; then
|
|
19
|
+
echo "OK front-plan fail-open ($bad)"; else echo "FAIL front-plan fail-open ($bad)"; fail=1; fi
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
out="$(echo '[{"epica":"EP-010","layer":"business","files_scope":["*a*.ts"]},
|
|
23
|
+
{"epica":"EP-011","layer":"business","files_scope":["*b*.ts"]}]' | python3 "$ROOT/scripts/lib/front-plan.py")"
|
|
24
|
+
echo "$out" | python3 -c "import json,sys;d=json.load(sys.stdin);
|
|
25
|
+
assert d['selected']==['EP-010'] and [m['epica'] for m in d['serialized']]==['EP-011'], d
|
|
26
|
+
print('OK front-plan mid-wildcard overlap serializado')" || { echo "FAIL mid-wildcard overlap"; fail=1; }
|
|
27
|
+
|
|
28
|
+
SK="$ROOT/skills/managing-parallel-front/SKILL.md"
|
|
29
|
+
{ [ -f "$SK" ] && grep -q 'git worktree add' "$SK" && grep -q 'front-plan.py' "$SK" \
|
|
30
|
+
&& grep -q 'merge_order' "$SK" && grep -q 'foundational' "$SK"; } \
|
|
31
|
+
&& echo "OK skill front" || { echo "FAIL skill front"; fail=1; }
|
|
32
|
+
|
|
33
|
+
{ [ -f "$ROOT/commands/build/front.md" ] && grep -q 'managing-parallel-front' "$ROOT/commands/build/front.md"; } \
|
|
34
|
+
&& echo "OK comando front" || { echo "FAIL comando front"; fail=1; }
|
|
35
|
+
grep -q 'files_scope' "$ROOT/skills/building-a-slice/references/dor.md" && echo "OK dor files_scope" || { echo "FAIL dor files_scope"; fail=1; }
|
|
36
|
+
grep -q 'parallel_front' "$ROOT/agents/build/build-orchestrator.md" && echo "OK orquestador front" || { echo "FAIL orquestador front"; fail=1; }
|
|
37
|
+
|
|
38
|
+
exit $fail
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-install.sh — smoke test del CANAL CLI de instalación (init/update).
|
|
3
|
+
# Reproduce el bug real: el instalador solo copiaba *.sh de hooks/build/ (sin
|
|
4
|
+
# recursar en lib/) y nunca creaba .claude/scripts/, dejando muertos en un
|
|
5
|
+
# install real los 3 archivos v0.8 (reconcile-build-state.py, state-io.sh,
|
|
6
|
+
# front-plan.py) aunque el smoke suite "en el repo" no lo detectara (los
|
|
7
|
+
# archivos ya existen ahí, fuera del flujo de instalación).
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
10
|
+
fail=0
|
|
11
|
+
|
|
12
|
+
# 1) Build fresco del CLI compilado (dist/cli.js) — el test corre contra el artefacto publicado.
|
|
13
|
+
if [ ! -f "$ROOT/dist/cli.js" ]; then
|
|
14
|
+
echo "ℹ dist/cli.js no existe — corriendo npm run build"
|
|
15
|
+
(cd "$ROOT" && npm run build) >/dev/null 2>&1
|
|
16
|
+
fi
|
|
17
|
+
if [ ! -f "$ROOT/dist/cli.js" ]; then
|
|
18
|
+
echo "FAIL install: npm run build no produjo dist/cli.js"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
TMP="$(mktemp -d)"
|
|
23
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
24
|
+
|
|
25
|
+
# 2) `init` no interactivo, modo copy (para no depender de symlinks al paquete),
|
|
26
|
+
# sin exigir openspec/python3/git en el runner (--skip-doctor).
|
|
27
|
+
node "$ROOT/dist/cli.js" init "$TMP" \
|
|
28
|
+
--copy \
|
|
29
|
+
--skip-doctor \
|
|
30
|
+
--yes \
|
|
31
|
+
--stack "" \
|
|
32
|
+
--pkg-manager npm \
|
|
33
|
+
--runtime ">=18.18" \
|
|
34
|
+
--prd-path "docs/01-prd/x.md#req" \
|
|
35
|
+
>"$TMP/.init.log" 2>&1
|
|
36
|
+
rc=$?
|
|
37
|
+
if [ $rc -ne 0 ]; then
|
|
38
|
+
echo "FAIL install: \`trycore-build init\` salió con código $rc"
|
|
39
|
+
cat "$TMP/.init.log"
|
|
40
|
+
fail=1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
check() { # <ruta relativa a .claude> <descripción>
|
|
44
|
+
local rel="$1"
|
|
45
|
+
local p="$TMP/.claude/$rel"
|
|
46
|
+
if [ -f "$p" ]; then
|
|
47
|
+
echo "OK install: .claude/$rel"
|
|
48
|
+
else
|
|
49
|
+
echo "FAIL install: .claude/$rel NO existe"
|
|
50
|
+
fail=1
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
check "hooks/build/reconcile-build-state.py"
|
|
55
|
+
check "hooks/build/lib/state-io.sh"
|
|
56
|
+
check "hooks/build/context-monitor.sh"
|
|
57
|
+
check "hooks/build/statusline-bridge.sh"
|
|
58
|
+
check "scripts/lib/front-plan.py"
|
|
59
|
+
|
|
60
|
+
# 3) Bit ejecutable en los .py/.sh copiados (modo copy no preserva +x por defecto — el
|
|
61
|
+
# instalador debe forzarlo con chmodExec tras copiar).
|
|
62
|
+
checkexec() {
|
|
63
|
+
local rel="$1"
|
|
64
|
+
local p="$TMP/.claude/$rel"
|
|
65
|
+
if [ -f "$p" ] && [ -x "$p" ]; then
|
|
66
|
+
echo "OK install: .claude/$rel es ejecutable"
|
|
67
|
+
else
|
|
68
|
+
echo "FAIL install: .claude/$rel NO es ejecutable"
|
|
69
|
+
fail=1
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
checkexec "hooks/build/reconcile-build-state.py"
|
|
73
|
+
checkexec "hooks/build/lib/state-io.sh"
|
|
74
|
+
checkexec "scripts/lib/front-plan.py"
|
|
75
|
+
|
|
76
|
+
# 4) `update` (canal de refresco tras `npm i` del paquete) también debe dejarlos — re-correr
|
|
77
|
+
# sobre la misma instalación no debe romper nada ni perder archivos.
|
|
78
|
+
node "$ROOT/dist/cli.js" update "$TMP" --copy --skip-doctor >"$TMP/.update.log" 2>&1
|
|
79
|
+
rc=$?
|
|
80
|
+
if [ $rc -ne 0 ]; then
|
|
81
|
+
echo "FAIL install: \`trycore-build update\` salió con código $rc"
|
|
82
|
+
cat "$TMP/.update.log"
|
|
83
|
+
fail=1
|
|
84
|
+
fi
|
|
85
|
+
check "hooks/build/reconcile-build-state.py"
|
|
86
|
+
check "hooks/build/lib/state-io.sh"
|
|
87
|
+
check "scripts/lib/front-plan.py"
|
|
88
|
+
|
|
89
|
+
exit $fail
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
4
|
+
source "$ROOT/hooks/build/lib/state-io.sh" 2>/dev/null || { echo "FAIL no existe state-io.sh"; exit 1; }
|
|
5
|
+
fail=0
|
|
6
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
|
|
8
|
+
# config_get lee clave punteada con default
|
|
9
|
+
echo '{"context":{"warning_pct":40}}' > "$TMP/cfg.json"
|
|
10
|
+
got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get context.warning_pct 35)"
|
|
11
|
+
[ "$got" = 40 ] && echo "OK config_get valor" || { echo "FAIL config_get valor ($got)"; fail=1; }
|
|
12
|
+
got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get context.critical_pct 25)"
|
|
13
|
+
[ "$got" = 25 ] && echo "OK config_get default" || { echo "FAIL config_get default ($got)"; fail=1; }
|
|
14
|
+
|
|
15
|
+
# state_atomic_patch muta y deja JSON válido
|
|
16
|
+
echo '{"version":"1.0","n":1}' > "$TMP/s.json"
|
|
17
|
+
state_atomic_patch "$TMP/s.json" 'd["n"]=2'
|
|
18
|
+
got="$(python3 -c "import json;print(json.load(open('$TMP/s.json'))['n'])")"
|
|
19
|
+
[ "$got" = 2 ] && echo "OK atomic_patch" || { echo "FAIL atomic_patch ($got)"; fail=1; }
|
|
20
|
+
|
|
21
|
+
# reconcile-build-state: degrada passing sin evidencia, conserva con evidencia, fail-open
|
|
22
|
+
cat > "$TMP/state.json" <<'JSON'
|
|
23
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
24
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"x","branch":"feature/x","phase":"tdd",
|
|
25
|
+
"gates":{"dor":true,"tdd":true,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t",
|
|
26
|
+
"wiring_checklist":[{"id":"HU-001-AC1","kind":"hu_ac","ref":"HU-001#1","status":"passing","evidence":""},
|
|
27
|
+
{"id":"HU-001-AC2","kind":"hu_ac","ref":"HU-001#2","status":"passing","evidence":"pytest ok"}]},
|
|
28
|
+
"history":[],"releases":[]}
|
|
29
|
+
JSON
|
|
30
|
+
python3 "$ROOT/hooks/build/reconcile-build-state.py" "$TMP/state.json" 2>/dev/null
|
|
31
|
+
st1="$(python3 -c "import json;d=json.load(open('$TMP/state.json'));print(d['active_slice']['wiring_checklist'][0]['status'])")"
|
|
32
|
+
st2="$(python3 -c "import json;d=json.load(open('$TMP/state.json'));print(d['active_slice']['wiring_checklist'][1]['status'])")"
|
|
33
|
+
[ "$st1" = failing ] && echo "OK reconcile degrada sin evidencia" || { echo "FAIL degrada ($st1)"; fail=1; }
|
|
34
|
+
[ "$st2" = passing ] && echo "OK reconcile conserva con evidencia" || { echo "FAIL conserva ($st2)"; fail=1; }
|
|
35
|
+
# fail-open: archivo corrupto no rompe
|
|
36
|
+
echo 'no json' > "$TMP/bad.json"
|
|
37
|
+
python3 "$ROOT/hooks/build/reconcile-build-state.py" "$TMP/bad.json" 2>/dev/null; echo "OK reconcile fail-open (rc=$?)"
|
|
38
|
+
|
|
39
|
+
# fail-open: wiring_checklist malformado (item no-dict) no debe crashear ni corromper el archivo
|
|
40
|
+
cat > "$TMP/mal.json" <<'JSON'
|
|
41
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
42
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"x","branch":"feature/x","phase":"tdd",
|
|
43
|
+
"gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t",
|
|
44
|
+
"wiring_checklist":["not-a-dict"]},
|
|
45
|
+
"history":[],"releases":[]}
|
|
46
|
+
JSON
|
|
47
|
+
if python3 "$ROOT/hooks/build/reconcile-build-state.py" "$TMP/mal.json" 2>/dev/null; then
|
|
48
|
+
python3 -c "import json;json.load(open('$TMP/mal.json'))" 2>/dev/null && echo "OK reconcile fail-open wiring malformado" || { echo "FAIL mal json corrupto"; fail=1; }
|
|
49
|
+
else echo "FAIL reconcile crashed on malformed wiring"; fail=1; fi
|
|
50
|
+
|
|
51
|
+
# SessionStart integra reconciliador
|
|
52
|
+
grep -q 'reconcile-build-state.py' "$ROOT/hooks/build/load-build-state.sh" && echo "OK sessionstart reconcilia" || { echo "FAIL sessionstart no reconcilia"; fail=1; }
|
|
53
|
+
grep -q 'resume_hint' "$ROOT/hooks/build/load-build-state.sh" && echo "OK sessionstart imprime hint" || { echo "FAIL sessionstart sin hint"; fail=1; }
|
|
54
|
+
exit $fail
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-schema.sh — valida build-state.schema.json contra ejemplos válidos/ inválidos.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
5
|
+
SCHEMA="$ROOT/state/build-state.schema.json"
|
|
6
|
+
fail=0
|
|
7
|
+
|
|
8
|
+
check() { # <descripción> <esperado: pass|fail> <json-file>
|
|
9
|
+
python3 - "$SCHEMA" "$3" <<'PY'
|
|
10
|
+
import json,sys
|
|
11
|
+
try:
|
|
12
|
+
import jsonschema
|
|
13
|
+
except ImportError:
|
|
14
|
+
print("SKIP: jsonschema no instalado"); sys.exit(2)
|
|
15
|
+
schema=json.load(open(sys.argv[1])); data=json.load(open(sys.argv[2]))
|
|
16
|
+
from jsonschema import Draft202012Validator
|
|
17
|
+
errs=sorted(Draft202012Validator(schema).iter_errors(data), key=str)
|
|
18
|
+
sys.exit(1 if errs else 0)
|
|
19
|
+
PY
|
|
20
|
+
local rc=$?
|
|
21
|
+
if [ "$rc" = 2 ]; then echo "SKIP $1"; return; fi
|
|
22
|
+
if { [ "$2" = pass ] && [ "$rc" = 0 ]; } || { [ "$2" = fail ] && [ "$rc" = 1 ]; }; then
|
|
23
|
+
echo "OK $1"
|
|
24
|
+
else echo "FAIL $1 (rc=$rc)"; fail=1; fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
28
|
+
cat > "$TMP/valid.json" <<'JSON'
|
|
29
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
30
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"add-login",
|
|
31
|
+
"branch":"feature/login","phase":"change","layer":"business","files_scope":["src/auth/**"],
|
|
32
|
+
"gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"test",
|
|
33
|
+
"session_continuity":{"last_session":"s1","stopped_at":null,"resume_hint":"cablear AC2","critical_recorded":false,"auto_continue":false}},
|
|
34
|
+
"history":[],"releases":[],
|
|
35
|
+
"parallel_front":{"status":"active","opened_at":"2026-07-03T00:00:00Z","updated_by":"test",
|
|
36
|
+
"members":[{"epica":"EP-002","worktree":{"path":".wt/ep-002","branch":"feature/ep-002","created_at":"2026-07-03T00:00:00Z"},
|
|
37
|
+
"files_scope":["src/reports/**"],"journey_smoke":false,"merge_status":"pending"}],
|
|
38
|
+
"merge_order":["EP-002"]}}
|
|
39
|
+
JSON
|
|
40
|
+
cat > "$TMP/bad-layer.json" <<'JSON'
|
|
41
|
+
{"version":"1.0","harness_phase":"active","scaffold":{"confirmed":true},
|
|
42
|
+
"active_slice":{"epica":"EP-001","hus":["HU-001"],"openspec_change":"x","branch":"feature/x","phase":"change",
|
|
43
|
+
"layer":"WRONG","gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t"},
|
|
44
|
+
"history":[],"releases":[]}
|
|
45
|
+
JSON
|
|
46
|
+
check "estado válido con campos nuevos" pass "$TMP/valid.json"
|
|
47
|
+
check "layer inválido rechazado" fail "$TMP/bad-layer.json"
|
|
48
|
+
|
|
49
|
+
CFG="$ROOT/config/build-config.template.json"
|
|
50
|
+
if [ -f "$CFG" ] && python3 -c "import json,sys; d=json.load(open('$CFG'))['context']; assert d['warning_pct']==35 and d['critical_pct']==25 and d['auto_checkpoint'] is False" 2>/dev/null; then
|
|
51
|
+
echo "OK build-config.template.json con defaults"
|
|
52
|
+
else echo "FAIL build-config.template.json con defaults"; fail=1; fi
|
|
53
|
+
|
|
54
|
+
CMD="$ROOT/commands/build/resume.md"
|
|
55
|
+
{ [ -f "$CMD" ] && grep -q 'reconcile-build-state.py' "$CMD" && grep -q 'session_continuity' "$CMD"; } \
|
|
56
|
+
&& echo "OK comando resume" || { echo "FAIL comando resume"; fail=1; }
|
|
57
|
+
|
|
58
|
+
exit $fail
|