@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.
Files changed (107) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/GOVERNANCE.md +27 -4
  3. package/INSTALL.md +27 -5
  4. package/METODOLOGIA.md +55 -5
  5. package/README.md +39 -6
  6. package/VERSION +1 -1
  7. package/agents/build/build-orchestrator.md +33 -7
  8. package/agents/build/dor-dod-gatekeeper.md +13 -5
  9. package/agents/build/wiring-adversarial-verifier.md +52 -5
  10. package/commands/build/architect.md +1 -1
  11. package/commands/build/claim.md +46 -0
  12. package/commands/build/escalate.md +36 -0
  13. package/commands/build/front.md +9 -3
  14. package/commands/build/onboard.md +75 -14
  15. package/commands/build/prototype.md +3 -2
  16. package/commands/build/reflect.md +60 -40
  17. package/commands/build/release.md +10 -7
  18. package/commands/build/resume.md +33 -13
  19. package/commands/build/slice.md +32 -27
  20. package/commands/build/status.md +35 -0
  21. package/commands/build/work.md +11 -8
  22. package/config/build-config.template.json +4 -0
  23. package/dist/cli.js +32 -0
  24. package/dist/commands/doctor.js +42 -0
  25. package/dist/commands/init.js +84 -1
  26. package/dist/commands/migrate.js +153 -0
  27. package/dist/commands/status.js +34 -0
  28. package/dist/lib/normalize.js +1123 -0
  29. package/dist/lib/paths.js +6 -0
  30. package/dist/lib/runtime-client.js +196 -0
  31. package/dist/lib/settings-merge.js +3 -3
  32. package/dist/lib/state-bundle.js +150 -0
  33. package/docs/commands.md +25 -8
  34. package/docs/getting-started.md +1 -0
  35. package/docs/hooks.md +114 -27
  36. package/docs/runtime/guia-modo-dual-y-migracion.md +143 -0
  37. package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
  38. package/docs/runtime/protocolo-cliente-runtime.md +120 -35
  39. package/hooks/build/build-gate-check.sh +21 -0
  40. package/hooks/build/context-monitor.sh +82 -15
  41. package/hooks/build/context-sync.sh +192 -0
  42. package/hooks/build/design-source-guard.sh +30 -2
  43. package/hooks/build/dual-compare.sh +92 -0
  44. package/hooks/build/event-emitter.sh +32 -0
  45. package/hooks/build/gitflow-guard.sh +164 -14
  46. package/hooks/build/heartbeat.sh +259 -0
  47. package/hooks/build/lib/agent-context.sh +139 -0
  48. package/hooks/build/lib/config.sh +27 -0
  49. package/hooks/build/lib/projection.sh +71 -0
  50. package/hooks/build/lib/runtime-client.sh +625 -0
  51. package/hooks/build/lib/runtime-ops.sh +227 -0
  52. package/hooks/build/lib/state-io.sh +5 -18
  53. package/hooks/build/load-build-state.sh +64 -2
  54. package/hooks/build/reflect-nudge.sh +15 -0
  55. package/hooks/build/release-gate-nudge.sh +15 -0
  56. package/hooks/build/release-ops.sh +171 -0
  57. package/hooks/build/scaffold-guard.sh +29 -2
  58. package/hooks/build/session-start.sh +103 -0
  59. package/hooks/build/session-stop.sh +22 -0
  60. package/hooks/build/slice-ops.sh +948 -0
  61. package/hooks/build/stack-guard.sh +8 -0
  62. package/hooks/build/statusline-bridge.sh +24 -3
  63. package/hooks/build-harness.json +16 -0
  64. package/package.json +3 -3
  65. package/scripts/check-agnostic.sh +3 -1
  66. package/scripts/check-pack-clean.sh +31 -0
  67. package/scripts/check-runtime-purity.sh +43 -0
  68. package/scripts/denylist.txt +4 -0
  69. package/scripts/lib/front-plan.py +4 -0
  70. package/scripts/lib/graph-bundle.py +181 -0
  71. package/scripts/runtime-purity-allow.txt +5 -0
  72. package/scripts/smoke-test.sh +1 -1
  73. package/scripts/tests/lib/http-stub.py +46 -0
  74. package/scripts/tests/test-baseline-verdict.sh +92 -0
  75. package/scripts/tests/test-config.sh +25 -0
  76. package/scripts/tests/test-hooks-runtime.sh +828 -0
  77. package/scripts/tests/test-install.sh +103 -0
  78. package/scripts/tests/test-runtime-client.sh +298 -0
  79. package/scripts/tests/test-schema.sh +29 -1
  80. package/scripts/tests/test-skill-ops.sh +1367 -0
  81. package/skills/building-a-micro-change/SKILL.md +22 -4
  82. package/skills/building-a-slice/SKILL.md +55 -21
  83. package/skills/building-a-slice/assets/baseline-verdict.sh +172 -0
  84. package/skills/building-a-slice/references/dod.md +12 -3
  85. package/skills/building-a-slice/references/dor.md +3 -2
  86. package/skills/building-a-slice/references/evidence-budget.md +51 -0
  87. package/skills/building-a-slice/references/exploration-fanout.md +1 -1
  88. package/skills/building-a-slice/references/gitflow.md +1 -1
  89. package/skills/building-a-slice/references/regression-baseline.md +67 -0
  90. package/skills/building-a-slice/references/runtime-protocol.md +75 -0
  91. package/skills/building-a-slice/references/state-protocol.md +12 -1
  92. package/skills/building-a-slice/workflows/README.md +7 -3
  93. package/skills/building-a-slice/workflows/explore-fanout.workflow.js +3 -3
  94. package/skills/building-a-slice/workflows/wiring-verify.workflow.js +26 -4
  95. package/skills/managing-parallel-front/SKILL.md +32 -16
  96. package/skills/openspec-archive-change/SKILL.md +15 -0
  97. package/skills/prototyping-screens/SKILL.md +9 -5
  98. package/skills/releasing-a-version/SKILL.md +26 -16
  99. package/skills/releasing-a-version/references/release-dod.md +7 -5
  100. package/skills/releasing-a-version/workflows/README.md +2 -1
  101. package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
  102. package/skills/setup-architecture/SKILL.md +4 -2
  103. package/state/README.md +16 -1
  104. package/state/build-state.schema.json +2 -1
  105. package/templates/CLAUDE.md.template +16 -0
  106. package/templates/settings-hooks.template.json +8 -4
  107. 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
+ }