@trycore/spec-build-harness 0.8.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/GOVERNANCE.md +27 -4
- package/INSTALL.md +27 -5
- package/METODOLOGIA.md +55 -5
- package/README.md +39 -6
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +33 -7
- package/agents/build/dor-dod-gatekeeper.md +13 -5
- package/agents/build/wiring-adversarial-verifier.md +52 -5
- package/commands/build/architect.md +1 -1
- package/commands/build/claim.md +46 -0
- package/commands/build/escalate.md +36 -0
- package/commands/build/front.md +9 -3
- package/commands/build/onboard.md +75 -14
- package/commands/build/prototype.md +3 -2
- package/commands/build/reflect.md +60 -40
- package/commands/build/release.md +10 -7
- package/commands/build/resume.md +33 -13
- package/commands/build/slice.md +32 -27
- package/commands/build/status.md +35 -0
- package/commands/build/work.md +11 -8
- package/config/build-config.template.json +4 -0
- package/dist/cli.js +32 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/init.js +84 -1
- package/dist/commands/migrate.js +153 -0
- package/dist/commands/status.js +34 -0
- package/dist/lib/normalize.js +1123 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/runtime-client.js +196 -0
- package/dist/lib/settings-merge.js +3 -3
- package/dist/lib/state-bundle.js +150 -0
- package/docs/commands.md +25 -8
- package/docs/getting-started.md +1 -0
- package/docs/hooks.md +114 -27
- package/docs/runtime/guia-modo-dual-y-migracion.md +143 -0
- package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
- package/docs/runtime/protocolo-cliente-runtime.md +120 -35
- package/hooks/build/build-gate-check.sh +21 -0
- package/hooks/build/context-monitor.sh +82 -15
- package/hooks/build/context-sync.sh +192 -0
- package/hooks/build/design-source-guard.sh +30 -2
- package/hooks/build/dual-compare.sh +92 -0
- package/hooks/build/event-emitter.sh +32 -0
- package/hooks/build/gitflow-guard.sh +164 -14
- package/hooks/build/heartbeat.sh +259 -0
- package/hooks/build/lib/agent-context.sh +139 -0
- package/hooks/build/lib/config.sh +27 -0
- package/hooks/build/lib/projection.sh +71 -0
- package/hooks/build/lib/runtime-client.sh +625 -0
- package/hooks/build/lib/runtime-ops.sh +227 -0
- package/hooks/build/lib/state-io.sh +5 -18
- package/hooks/build/load-build-state.sh +64 -2
- package/hooks/build/reflect-nudge.sh +15 -0
- package/hooks/build/release-gate-nudge.sh +15 -0
- package/hooks/build/release-ops.sh +171 -0
- package/hooks/build/scaffold-guard.sh +29 -2
- package/hooks/build/session-start.sh +103 -0
- package/hooks/build/session-stop.sh +22 -0
- package/hooks/build/slice-ops.sh +948 -0
- package/hooks/build/stack-guard.sh +8 -0
- package/hooks/build/statusline-bridge.sh +24 -3
- package/hooks/build-harness.json +16 -0
- package/package.json +3 -3
- package/scripts/check-agnostic.sh +3 -1
- package/scripts/check-pack-clean.sh +31 -0
- package/scripts/check-runtime-purity.sh +43 -0
- package/scripts/denylist.txt +4 -0
- package/scripts/lib/front-plan.py +4 -0
- package/scripts/lib/graph-bundle.py +181 -0
- package/scripts/runtime-purity-allow.txt +5 -0
- package/scripts/smoke-test.sh +1 -1
- package/scripts/tests/lib/http-stub.py +46 -0
- package/scripts/tests/test-baseline-verdict.sh +92 -0
- package/scripts/tests/test-config.sh +25 -0
- package/scripts/tests/test-hooks-runtime.sh +828 -0
- package/scripts/tests/test-install.sh +103 -0
- package/scripts/tests/test-runtime-client.sh +298 -0
- package/scripts/tests/test-schema.sh +29 -1
- package/scripts/tests/test-skill-ops.sh +1367 -0
- package/skills/building-a-micro-change/SKILL.md +22 -4
- package/skills/building-a-slice/SKILL.md +55 -21
- package/skills/building-a-slice/assets/baseline-verdict.sh +172 -0
- package/skills/building-a-slice/references/dod.md +12 -3
- package/skills/building-a-slice/references/dor.md +3 -2
- package/skills/building-a-slice/references/evidence-budget.md +51 -0
- package/skills/building-a-slice/references/exploration-fanout.md +1 -1
- package/skills/building-a-slice/references/gitflow.md +1 -1
- package/skills/building-a-slice/references/regression-baseline.md +67 -0
- package/skills/building-a-slice/references/runtime-protocol.md +75 -0
- package/skills/building-a-slice/references/state-protocol.md +12 -1
- package/skills/building-a-slice/workflows/README.md +7 -3
- package/skills/building-a-slice/workflows/explore-fanout.workflow.js +3 -3
- package/skills/building-a-slice/workflows/wiring-verify.workflow.js +26 -4
- package/skills/managing-parallel-front/SKILL.md +32 -16
- package/skills/openspec-archive-change/SKILL.md +15 -0
- package/skills/prototyping-screens/SKILL.md +9 -5
- package/skills/releasing-a-version/SKILL.md +26 -16
- package/skills/releasing-a-version/references/release-dod.md +7 -5
- package/skills/releasing-a-version/workflows/README.md +2 -1
- package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
- package/skills/setup-architecture/SKILL.md +4 -2
- package/state/README.md +16 -1
- package/state/build-state.schema.json +2 -1
- package/templates/CLAUDE.md.template +16 -0
- package/templates/settings-hooks.template.json +8 -4
- package/internal/skills/auditar-arnes/SKILL.md +0 -29
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agent-context.sh — hidratación de la caché de proyección desde GET /agent/context
|
|
3
|
+
# [EP-OR-08-B]. Es la ÚNICA pieza que conoce la forma cruda de la respuesta del servidor:
|
|
4
|
+
# la normaliza al esquema documentado del plan (fetched_at/project/active_slice/nudges/
|
|
5
|
+
# context/lease) para que guards, renders y nudges dependan solo de ese esquema. Si el
|
|
6
|
+
# servidor añade o renombra campos, se toca este fichero y nada más.
|
|
7
|
+
# Diseño fail-open: ante red caída o respuesta ilegible, la caché ANTERIOR queda intacta.
|
|
8
|
+
|
|
9
|
+
source "$(dirname "${BASH_SOURCE[0]}")/runtime-client.sh"
|
|
10
|
+
|
|
11
|
+
AGENT_CONTEXT_PATH="/agent/context"
|
|
12
|
+
|
|
13
|
+
# agent_context_normalize <fichero-con-el-json-crudo> — imprime el JSON normalizado
|
|
14
|
+
# (o "{}" si es ilegible, con rc 1). El crudo se pasa por FICHERO, no por argv: la
|
|
15
|
+
# respuesta puede traer nudges y wiring largos y argv está acotado por ARG_MAX.
|
|
16
|
+
agent_context_normalize() {
|
|
17
|
+
local raw_file="$1"
|
|
18
|
+
local out rc
|
|
19
|
+
command -v python3 >/dev/null 2>&1 || { echo "{}"; return 1; }
|
|
20
|
+
# La salida de python3 se captura en vez de dejarla fluir directo a stdout:
|
|
21
|
+
# si el script ya imprimió "{}" y salió con rc 1 (JSON ilegible), no queremos
|
|
22
|
+
# que el manejo de fallo de abajo imprima un segundo "{}" encima (doble salida).
|
|
23
|
+
out="$(python3 - "$raw_file" <<'PY' 2>/dev/null
|
|
24
|
+
import json,sys,datetime
|
|
25
|
+
|
|
26
|
+
def as_dict(x):
|
|
27
|
+
return x if isinstance(x, dict) else {}
|
|
28
|
+
|
|
29
|
+
def as_list(x):
|
|
30
|
+
return x if isinstance(x, list) else []
|
|
31
|
+
|
|
32
|
+
def first(*vals):
|
|
33
|
+
for v in vals:
|
|
34
|
+
if v is not None:
|
|
35
|
+
return v
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
raw = as_dict(json.load(open(sys.argv[1])))
|
|
40
|
+
except Exception:
|
|
41
|
+
print("{}")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
proj = as_dict(first(raw.get("project"), raw.get("project_facts"), {}))
|
|
45
|
+
scaffold = as_dict(proj.get("scaffold"))
|
|
46
|
+
ds = as_dict(proj.get("design_source"))
|
|
47
|
+
sl = as_dict(first(raw.get("active_slice"), raw.get("slice"), {}))
|
|
48
|
+
|
|
49
|
+
wiring = []
|
|
50
|
+
for w in as_list(first(sl.get("wiring_checklist"), sl.get("wiring"), sl.get("wiring_failing"), [])):
|
|
51
|
+
w = as_dict(w)
|
|
52
|
+
# Sin `status` se asume failing: la lista `wiring_failing` del servidor ya viene filtrada.
|
|
53
|
+
if w.get("status") in (None, "failing"):
|
|
54
|
+
wiring.append({"item_id": first(w.get("item_id"), w.get("id")), "kind": w.get("kind")})
|
|
55
|
+
|
|
56
|
+
subs = []
|
|
57
|
+
for s in as_list(sl.get("sub_slices")):
|
|
58
|
+
s = as_dict(s)
|
|
59
|
+
if s.get("status") != "done":
|
|
60
|
+
sid = first(s.get("id"), s.get("sub_slice_id"))
|
|
61
|
+
if sid:
|
|
62
|
+
subs.append(sid)
|
|
63
|
+
|
|
64
|
+
nudges = []
|
|
65
|
+
for n in as_list(raw.get("nudges")):
|
|
66
|
+
if isinstance(n, str):
|
|
67
|
+
nudges.append({"kind": "info", "message": n})
|
|
68
|
+
else:
|
|
69
|
+
n = as_dict(n)
|
|
70
|
+
nudges.append({"kind": first(n.get("kind"), n.get("type"), "info"),
|
|
71
|
+
"message": first(n.get("message"), n.get("text"), "")})
|
|
72
|
+
|
|
73
|
+
hus = [h for h in as_list(first(sl.get("stories"), sl.get("hus"), [])) if isinstance(h, str)]
|
|
74
|
+
ctx = as_dict(raw.get("context"))
|
|
75
|
+
lease = as_dict(raw.get("lease"))
|
|
76
|
+
checkpoint = as_dict(sl.get("checkpoint")) or None
|
|
77
|
+
|
|
78
|
+
out = {
|
|
79
|
+
"fetched_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
80
|
+
"source": "agent_context",
|
|
81
|
+
"project": {
|
|
82
|
+
"scaffold_confirmed": bool(first(proj.get("scaffold_confirmed"), scaffold.get("confirmed"), False)),
|
|
83
|
+
"design_source_applies": bool(first(proj.get("design_source_applies"), ds.get("applies"), False)),
|
|
84
|
+
"design_source_confirmed": bool(first(proj.get("design_source_confirmed"), ds.get("confirmed"), False)),
|
|
85
|
+
"project_kind": first(proj.get("project_kind"), raw.get("project_kind")),
|
|
86
|
+
"harness_phase": first(proj.get("harness_phase"), raw.get("harness_phase")),
|
|
87
|
+
"foundation_done": bool(first(proj.get("foundation_done"), as_dict(proj.get("foundation")).get("done"), False)),
|
|
88
|
+
},
|
|
89
|
+
"active_slice": None if not sl else {
|
|
90
|
+
"slice_id": first(sl.get("slice_id"), sl.get("id")),
|
|
91
|
+
"epic_code": first(sl.get("epic_code"), sl.get("epica")),
|
|
92
|
+
"epic_title": sl.get("epic_title"),
|
|
93
|
+
"hus": hus,
|
|
94
|
+
"phase": sl.get("phase"),
|
|
95
|
+
"gates": as_dict(sl.get("gates")),
|
|
96
|
+
"branch": first(sl.get("branch"), sl.get("branch_base")),
|
|
97
|
+
"openspec_change": sl.get("openspec_change"),
|
|
98
|
+
"docs_ref": sl.get("docs_ref"),
|
|
99
|
+
"wiring_failing": wiring,
|
|
100
|
+
"sub_slices_pending": subs,
|
|
101
|
+
"resume_hint": first(sl.get("resume_hint"), as_dict(sl.get("session_continuity")).get("resume_hint")),
|
|
102
|
+
"checkpoint": checkpoint,
|
|
103
|
+
},
|
|
104
|
+
"nudges": nudges,
|
|
105
|
+
"context": {"manifest_hash": first(ctx.get("manifest_hash"), raw.get("manifest_hash")),
|
|
106
|
+
"version": first(ctx.get("version"), raw.get("context_version"))},
|
|
107
|
+
"lease": {"expires_at": first(lease.get("expires_at"), raw.get("lease_expires_at")),
|
|
108
|
+
"ttl_s": first(lease.get("ttl_s"), raw.get("lease_ttl_s"))},
|
|
109
|
+
}
|
|
110
|
+
print(json.dumps(out, ensure_ascii=False))
|
|
111
|
+
PY
|
|
112
|
+
)"
|
|
113
|
+
rc=$?
|
|
114
|
+
if [ $rc -ne 0 ]; then
|
|
115
|
+
echo "{}"
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
printf '%s\n' "$out"
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# agent_context_fetch_and_cache — GET /agent/context, normaliza y escribe la caché.
|
|
123
|
+
# rc 0 = caché actualizada · 1 = no se pudo (red, status != 200, respuesta ilegible):
|
|
124
|
+
# en ese caso la caché anterior NO se toca, y los guards siguen operando con ella.
|
|
125
|
+
agent_context_fetch_and_cache() {
|
|
126
|
+
local body status tmp norm rc
|
|
127
|
+
body="$(runtime_get "$AGENT_CONTEXT_PATH")"
|
|
128
|
+
status="$(runtime_http_status)"
|
|
129
|
+
[ "$status" = "200" ] || return 1
|
|
130
|
+
tmp="$(mktemp)" || return 1
|
|
131
|
+
printf '%s' "$body" > "$tmp"
|
|
132
|
+
norm="$(agent_context_normalize "$tmp")"
|
|
133
|
+
rc=$?
|
|
134
|
+
rm -f "$tmp"
|
|
135
|
+
[ $rc -eq 0 ] || return 1
|
|
136
|
+
[ -n "$norm" ] && [ "$norm" != "{}" ] || return 1
|
|
137
|
+
runtime_projection_write "$norm" || return 1
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# config.sh — helpers de lectura de config/build-config.json. Extraído de state-io.sh
|
|
3
|
+
# [EP-OR-08-A] para que sea infraestructura común a legacy/dual/runtime: retirar
|
|
4
|
+
# state-io.sh en modo runtime puro no debe romper context-monitor, que solo necesita
|
|
5
|
+
# config_get. Diseño: fail-open, igual que state-io.sh — nunca lanza.
|
|
6
|
+
|
|
7
|
+
# config_root — raíz del proyecto consumidor (misma señal que state_path en state-io.sh).
|
|
8
|
+
config_root() {
|
|
9
|
+
echo "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# config_get <clave.punteada> <default>
|
|
13
|
+
config_get() {
|
|
14
|
+
local key="$1" def="$2" file
|
|
15
|
+
file="${BUILD_CONFIG_FILE:-$(config_root)/.claude/config/build-config.json}"
|
|
16
|
+
command -v python3 >/dev/null 2>&1 || { echo "$def"; return; }
|
|
17
|
+
python3 - "$file" "$key" "$def" <<'PY' 2>/dev/null || echo "$def"
|
|
18
|
+
import json,sys
|
|
19
|
+
file,key,default=sys.argv[1],sys.argv[2],sys.argv[3]
|
|
20
|
+
try:
|
|
21
|
+
d=json.load(open(file))
|
|
22
|
+
for part in key.split("."): d=d[part]
|
|
23
|
+
print(d if not isinstance(d,bool) else str(d).lower())
|
|
24
|
+
except Exception:
|
|
25
|
+
print(default)
|
|
26
|
+
PY
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# projection.sh — lectores de la caché de proyección local [EP-OR-08-B].
|
|
3
|
+
# La caché la ESCRIBE session-start.sh (vía lib/agent-context.sh) desde GET /agent/context;
|
|
4
|
+
# aquí solo se LEE: ninguna función de este fichero toca la red, y de esa garantía dependen
|
|
5
|
+
# los cuatro guards de PreToolUse (spec §4.2-B: «PreToolUse: prohibida la red»).
|
|
6
|
+
# Diseño fail-open, igual que config.sh/state-io.sh: sin caché, sin python3 o con JSON
|
|
7
|
+
# corrupto se devuelve el default y jamás se lanza.
|
|
8
|
+
|
|
9
|
+
source "$(dirname "${BASH_SOURCE[0]}")/runtime-client.sh"
|
|
10
|
+
|
|
11
|
+
# projection_get <clave.punteada> <default>
|
|
12
|
+
# Escalares tal cual; booleanos en minúsculas (true/false, comparables desde bash);
|
|
13
|
+
# dict/list como JSON compacto para encadenar con python3 en el llamador.
|
|
14
|
+
projection_get() {
|
|
15
|
+
local key="$1" def="$2" file
|
|
16
|
+
file="$(runtime_projection_path)"
|
|
17
|
+
[ -f "$file" ] || { echo "$def"; return; }
|
|
18
|
+
command -v python3 >/dev/null 2>&1 || { echo "$def"; return; }
|
|
19
|
+
python3 - "$file" "$key" "$def" <<'PY' 2>/dev/null || echo "$def"
|
|
20
|
+
import json,sys
|
|
21
|
+
path,key,default=sys.argv[1],sys.argv[2],sys.argv[3]
|
|
22
|
+
try:
|
|
23
|
+
d=json.load(open(path))
|
|
24
|
+
for part in key.split("."):
|
|
25
|
+
if isinstance(d,dict) and part in d:
|
|
26
|
+
d=d[part]
|
|
27
|
+
else:
|
|
28
|
+
raise KeyError(part)
|
|
29
|
+
if d is None:
|
|
30
|
+
print(default)
|
|
31
|
+
elif isinstance(d,bool):
|
|
32
|
+
print(str(d).lower())
|
|
33
|
+
elif isinstance(d,(dict,list)):
|
|
34
|
+
print(json.dumps(d,ensure_ascii=False))
|
|
35
|
+
else:
|
|
36
|
+
print(d)
|
|
37
|
+
except Exception:
|
|
38
|
+
print(default)
|
|
39
|
+
PY
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# projection_age_seconds — segundos desde `fetched_at`. 999999 si no hay caché, no hay
|
|
43
|
+
# python3 o la fecha es ilegible (equivale a «infinitamente vieja»: el llamador avisará
|
|
44
|
+
# de stale en vez de fingir frescura).
|
|
45
|
+
projection_age_seconds() {
|
|
46
|
+
local file; file="$(runtime_projection_path)"
|
|
47
|
+
[ -f "$file" ] || { echo 999999; return; }
|
|
48
|
+
command -v python3 >/dev/null 2>&1 || { echo 999999; return; }
|
|
49
|
+
python3 - "$file" <<'PY' 2>/dev/null || echo 999999
|
|
50
|
+
import json,sys,datetime
|
|
51
|
+
try:
|
|
52
|
+
d=json.load(open(sys.argv[1]))
|
|
53
|
+
t=datetime.datetime.strptime(d.get("fetched_at") or "","%Y-%m-%dT%H:%M:%SZ")
|
|
54
|
+
t=t.replace(tzinfo=datetime.timezone.utc)
|
|
55
|
+
now=datetime.datetime.now(datetime.timezone.utc)
|
|
56
|
+
print(max(0,int((now-t).total_seconds())))
|
|
57
|
+
except Exception:
|
|
58
|
+
print(999999)
|
|
59
|
+
PY
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# projection_is_stale — rc 0 si la caché supera runtime.stale_seconds (default 900 s).
|
|
63
|
+
# rc, no stdout: se usa como condición (`if projection_is_stale; then …`).
|
|
64
|
+
projection_is_stale() {
|
|
65
|
+
local age max
|
|
66
|
+
age="$(projection_age_seconds)"
|
|
67
|
+
max="$(config_get runtime.stale_seconds 900)"
|
|
68
|
+
case "$max" in ''|*[!0-9]*) max=900 ;; esac
|
|
69
|
+
case "$age" in ''|*[!0-9]*) age=999999 ;; esac
|
|
70
|
+
[ "$age" -gt "$max" ]
|
|
71
|
+
}
|