@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.
Files changed (113) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/GOVERNANCE.md +43 -5
  3. package/INSTALL.md +28 -6
  4. package/METODOLOGIA.md +65 -10
  5. package/README.md +41 -7
  6. package/VERSION +1 -1
  7. package/agents/build/build-orchestrator.md +33 -7
  8. package/agents/build/dor-dod-gatekeeper.md +17 -6
  9. package/agents/build/ux-fidelity-reviewer.md +4 -1
  10. package/agents/build/wiring-adversarial-verifier.md +52 -5
  11. package/commands/build/architect.md +1 -1
  12. package/commands/build/claim.md +46 -0
  13. package/commands/build/escalate.md +36 -0
  14. package/commands/build/front.md +9 -3
  15. package/commands/build/onboard.md +63 -14
  16. package/commands/build/prototype.md +23 -0
  17. package/commands/build/reflect.md +60 -40
  18. package/commands/build/release.md +10 -7
  19. package/commands/build/resume.md +33 -13
  20. package/commands/build/slice.md +32 -27
  21. package/commands/build/status.md +35 -0
  22. package/commands/build/work.md +11 -8
  23. package/config/build-config.template.json +4 -0
  24. package/dist/cli.js +22 -0
  25. package/dist/commands/doctor.js +42 -0
  26. package/dist/commands/init.js +84 -1
  27. package/dist/commands/migrate.js +48 -0
  28. package/dist/commands/status.js +34 -0
  29. package/dist/lib/normalize.js +276 -0
  30. package/dist/lib/paths.js +6 -0
  31. package/dist/lib/runtime-client.js +196 -0
  32. package/dist/lib/settings-merge.js +3 -3
  33. package/dist/lib/state-bundle.js +46 -0
  34. package/docs/commands.md +32 -9
  35. package/docs/getting-started.md +2 -1
  36. package/docs/hooks.md +114 -27
  37. package/docs/runtime/guia-modo-dual-y-migracion.md +136 -0
  38. package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
  39. package/docs/runtime/protocolo-cliente-runtime.md +109 -34
  40. package/hooks/build/build-gate-check.sh +21 -0
  41. package/hooks/build/context-monitor.sh +82 -15
  42. package/hooks/build/context-sync.sh +192 -0
  43. package/hooks/build/design-source-guard.sh +31 -3
  44. package/hooks/build/dual-compare.sh +92 -0
  45. package/hooks/build/event-emitter.sh +75 -0
  46. package/hooks/build/gitflow-guard.sh +164 -14
  47. package/hooks/build/heartbeat.sh +259 -0
  48. package/hooks/build/lib/agent-context.sh +139 -0
  49. package/hooks/build/lib/config.sh +27 -0
  50. package/hooks/build/lib/projection.sh +71 -0
  51. package/hooks/build/lib/runtime-client.sh +465 -0
  52. package/hooks/build/lib/runtime-ops.sh +221 -0
  53. package/hooks/build/lib/state-io.sh +5 -18
  54. package/hooks/build/load-build-state.sh +64 -2
  55. package/hooks/build/reflect-nudge.sh +15 -0
  56. package/hooks/build/release-gate-nudge.sh +15 -0
  57. package/hooks/build/release-ops.sh +164 -0
  58. package/hooks/build/scaffold-guard.sh +29 -2
  59. package/hooks/build/session-start.sh +103 -0
  60. package/hooks/build/session-stop.sh +22 -0
  61. package/hooks/build/slice-ops.sh +877 -0
  62. package/hooks/build/stack-guard.sh +8 -0
  63. package/hooks/build/statusline-bridge.sh +24 -3
  64. package/hooks/build-harness.json +16 -0
  65. package/package.json +3 -3
  66. package/scripts/check-agnostic.sh +3 -1
  67. package/scripts/check-pack-clean.sh +31 -0
  68. package/scripts/check-runtime-purity.sh +43 -0
  69. package/scripts/lib/front-plan.py +4 -0
  70. package/scripts/lib/graph-bundle.py +133 -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 +853 -0
  77. package/scripts/tests/test-install.sh +57 -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 +847 -0
  81. package/skills/building-a-micro-change/SKILL.md +22 -4
  82. package/skills/building-a-slice/SKILL.md +58 -23
  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 +7 -3
  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 +104 -0
  98. package/skills/prototyping-screens/assets/DESIGN.md.template +55 -0
  99. package/skills/prototyping-screens/assets/manifest.schema.json +70 -0
  100. package/skills/prototyping-screens/assets/screen.template.html +34 -0
  101. package/skills/prototyping-screens/references/aesthetic-directions.md +42 -0
  102. package/skills/prototyping-screens/references/extraction.md +57 -0
  103. package/skills/prototyping-screens/references/self-check.md +40 -0
  104. package/skills/releasing-a-version/SKILL.md +25 -16
  105. package/skills/releasing-a-version/references/release-dod.md +7 -5
  106. package/skills/releasing-a-version/workflows/README.md +2 -1
  107. package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
  108. package/skills/setup-architecture/SKILL.md +4 -2
  109. package/state/README.md +20 -3
  110. package/state/build-state.schema.json +3 -2
  111. package/templates/CLAUDE.md.template +17 -1
  112. package/templates/settings-hooks.template.json +8 -4
  113. package/internal/skills/auditar-arnes/SKILL.md +0 -29
@@ -6,6 +6,11 @@
6
6
  set -uo pipefail
7
7
 
8
8
  ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
9
+ # [EP-OR-08-B] El allowlist deja de ser un fichero que edita un humano: en dual/runtime lo
10
+ # escribe context-sync.sh desde el asset gobernado del runtime. El guard sigue leyéndolo de
11
+ # disco (cero red en PreToolUse); solo avisa si el contexto local está sin sincronizar.
12
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ source "$HERE/lib/projection.sh"
9
14
  ALLOW="$ROOT/.claude/config/stack-allowlist.json"
10
15
  [ -f "$ALLOW" ] || exit 0
11
16
 
@@ -80,6 +85,9 @@ if [ -n "$VIOL" ]; then
80
85
  echo "⛔ stack-guard: dependencias fuera del stack declarado del PRD (allowlist): $VIOL" >&2
81
86
  echo " Si es intencional, actualiza .claude/config/stack-allowlist.json y deja nota en GOVERNANCE.md," >&2
82
87
  echo " o consulta al agente stack-guardian. Allowlist: .claude/config/stack-allowlist.json" >&2
88
+ if [ "$(runtime_mode)" != "legacy" ] && projection_is_stale; then
89
+ echo " ⚠ Contexto local sin sincronizar (stale): el allowlist puede estar desactualizado respecto al runtime." >&2
90
+ fi
83
91
  exit 2
84
92
  fi
85
93
  exit 0
@@ -1,15 +1,36 @@
1
1
  #!/usr/bin/env bash
2
2
  # statusline-bridge.sh — statusLine command (canal CLI). Imprime la línea de estado
3
3
  # y escribe el puente de contexto que lee context-monitor.sh. Fail-open siempre.
4
+ # [EP-OR-08-B] En dual/runtime añade el plano de contexto: versión sincronizada (✓ vN),
5
+ # proyección sin refrescar (⚠ stale) y lease perdido (⚠ lease). Cero red: todo sale de
6
+ # ficheros locales (caché de proyección y estado del daemon).
4
7
  set -uo pipefail
8
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
9
  payload="$(cat)"
6
10
  command -v python3 >/dev/null 2>&1 || { echo "🏗️ build"; exit 0; }
7
- echo "$payload" | python3 -c '
11
+
12
+ source "$HERE/lib/runtime-client.sh"
13
+ source "$HERE/lib/projection.sh"
14
+
15
+ CTXTAG=""
16
+ if [ "$(runtime_mode)" != "legacy" ]; then
17
+ if projection_is_stale; then
18
+ CTXTAG=" · ⚠ stale"
19
+ else
20
+ ver="$(projection_get context.version "")"
21
+ [ -n "$ver" ] && CTXTAG=" · ✓ v$ver"
22
+ fi
23
+ if grep -q '"lease_lost": true' "$(config_root)/.claude/state/heartbeat-status.json" 2>/dev/null; then
24
+ CTXTAG="$CTXTAG · ⚠ lease"
25
+ fi
26
+ fi
27
+
28
+ echo "$payload" | CTXTAG="$CTXTAG" python3 -c '
8
29
  import json,sys,os,time,re,tempfile
9
30
  try:
10
31
  p=json.load(sys.stdin)
11
32
  except Exception:
12
- print("🏗️ build"); sys.exit(0)
33
+ print("🏗️ build"+os.environ.get("CTXTAG","")); sys.exit(0)
13
34
  sid=str(p.get("session_id","default"))
14
35
  if re.search(r"[\\/]|\.\.", sid): sid=re.sub(r"[^A-Za-z0-9_-]","_",sid) # sanitiza path traversal
15
36
  rem=None
@@ -28,5 +49,5 @@ if rem is not None:
28
49
  except OSError: pass
29
50
  except Exception: pass
30
51
  tag = f"🏗️ build · ctx {rem}%" if rem is not None else "🏗️ build"
31
- print(tag)
52
+ print(tag+os.environ.get("CTXTAG",""))
32
53
  ' 2>/dev/null || echo "🏗️ build"
@@ -4,6 +4,10 @@
4
4
  {
5
5
  "matcher": "startup|clear|compact",
6
6
  "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/session-start.sh\""
10
+ },
7
11
  {
8
12
  "type": "command",
9
13
  "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/load-build-state.sh\""
@@ -59,6 +63,10 @@
59
63
  {
60
64
  "type": "command",
61
65
  "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/context-monitor.sh\""
66
+ },
67
+ {
68
+ "type": "command",
69
+ "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/event-emitter.sh\""
62
70
  }
63
71
  ]
64
72
  }
@@ -90,9 +98,17 @@
90
98
  "type": "command",
91
99
  "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/release-gate-nudge.sh\""
92
100
  },
101
+ {
102
+ "type": "command",
103
+ "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/dual-compare.sh\""
104
+ },
93
105
  {
94
106
  "type": "command",
95
107
  "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/context-monitor.sh\""
108
+ },
109
+ {
110
+ "type": "command",
111
+ "command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/session-stop.sh\""
96
112
  }
97
113
  ]
98
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trycore/spec-build-harness",
3
- "version": "0.8.4",
3
+ "version": "0.10.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": {
@@ -17,7 +17,6 @@
17
17
  "state/",
18
18
  "templates/",
19
19
  "asset-types.json",
20
- "internal/",
21
20
  "docs/",
22
21
  "!docs/superpowers",
23
22
  "scripts/",
@@ -32,7 +31,8 @@
32
31
  },
33
32
  "scripts": {
34
33
  "build": "tsc",
35
- "prepublishOnly": "npm run build && bash scripts/check-version-sync.sh && bash scripts/check-agnostic.sh && bash scripts/check-state-clean.sh",
34
+ "test:unit": "tsc -p tsconfig.test.json && find dist-test -name '*.test.js' -print0 | xargs -0 node --test",
35
+ "prepublishOnly": "npm run build && npm run test:unit && bash scripts/check-version-sync.sh && bash scripts/check-agnostic.sh && bash scripts/check-runtime-purity.sh && bash scripts/check-state-clean.sh && bash scripts/check-pack-clean.sh",
36
36
  "test:smoke": "bash scripts/smoke-test.sh"
37
37
  },
38
38
  "keywords": [
@@ -47,6 +47,8 @@ TARGETS=(
47
47
  "$REPO_ROOT/config"
48
48
  "$REPO_ROOT/state"
49
49
  "$REPO_ROOT/templates"
50
+ "$REPO_ROOT/src"
51
+ "$REPO_ROOT/asset-types.json"
50
52
  "$REPO_ROOT/internal"
51
53
  "$REPO_ROOT/docs/customization"
52
54
  "$REPO_ROOT/METODOLOGIA.md"
@@ -59,7 +61,7 @@ TARGETS=(
59
61
  for target in "${TARGETS[@]}"; do
60
62
  [[ -e "$target" ]] || continue
61
63
  hits=$(grep -riEn -- "$PATTERN" "$target" 2>/dev/null \
62
- --include='*.md' --include='*.json' --include='*.sh' --include='*.js' --include='*.mjs' \
64
+ --include='*.md' --include='*.json' --include='*.sh' --include='*.js' --include='*.mjs' --include='*.ts' \
63
65
  --exclude-dir='node_modules' || true)
64
66
  if [[ -n "$hits" ]]; then
65
67
  VIOLATIONS+=("$hits")
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # check-pack-clean: verifica que el tarball de npm NO incluya contenido no distribuible.
3
+ # Regla del repo (CLAUDE.md): MAINTAINERS.md, internal/ y docs/superpowers/ no se distribuyen en npm.
4
+ # Corre en prepublishOnly: si algo de eso entra al pack, bloquea la publicación.
5
+ set -euo pipefail
6
+
7
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ cd "$REPO_ROOT"
9
+
10
+ echo "▶ check-pack-clean: verificando el contenido del tarball (npm pack --dry-run)…"
11
+
12
+ # Lista de rutas prohibidas en el paquete publicado (relativas a la raíz del tarball).
13
+ FORBIDDEN='^(internal/|MAINTAINERS\.md$|docs/superpowers/)'
14
+
15
+ FILES="$(npm pack --dry-run --json 2>/dev/null | python3 -c '
16
+ import json, sys
17
+ data = json.load(sys.stdin)
18
+ for f in data[0]["files"]:
19
+ print(f["path"])
20
+ ')"
21
+
22
+ LEAKS="$(printf '%s\n' "$FILES" | grep -E "$FORBIDDEN" || true)"
23
+
24
+ if [ -n "$LEAKS" ]; then
25
+ echo "✗ Contenido NO distribuible dentro del tarball:" >&2
26
+ printf '%s\n' "$LEAKS" | sed 's/^/ /' >&2
27
+ echo " Revisa package.json#files (regla: MAINTAINERS.md, internal/ y docs/superpowers/ no se publican)." >&2
28
+ exit 1
29
+ fi
30
+
31
+ echo "✓ Tarball limpio: sin internal/, MAINTAINERS.md ni docs/superpowers/."
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # check-runtime-purity.sh — ratchet de la migración v0.9 [EP-OR-08-C].
3
+ # Prohíbe que skills/, commands/ y agents/ instruyan el protocolo del FICHERO local
4
+ # (.claude/state/build-state.json) fuera de dos excepciones declaradas:
5
+ # (a) los ficheros listados en scripts/runtime-purity-allow.txt — documentación del
6
+ # modo `legacy`, que vive mientras viva legacy (spec §4.2-E);
7
+ # (b) líneas que declaran explícitamente el modo `legacy` (una tabla de modos, una nota
8
+ # «en legacy … »): ahí la mención es correcta y necesaria.
9
+ # Salida 0 si limpio, 1 si hay menciones fuera de esas excepciones. Corre en prepublishOnly.
10
+ set -uo pipefail
11
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
12
+ ALLOW="$ROOT/scripts/runtime-purity-allow.txt"
13
+ PATTERN='build-state\.json|build-state\.schema\.json|build_state'
14
+
15
+ hits="$(grep -rEn -- "$PATTERN" "$ROOT/skills" "$ROOT/commands" "$ROOT/agents" \
16
+ --include='*.md' --include='*.js' --include='*.json' 2>/dev/null || true)"
17
+
18
+ fail=0
19
+ violaciones=""
20
+ while IFS= read -r linea; do
21
+ [ -n "$linea" ] || continue
22
+ archivo="${linea%%:*}"
23
+ rel="${archivo#$ROOT/}"
24
+ if [ -f "$ALLOW" ] && grep -qxF "$rel" "$ALLOW" 2>/dev/null; then continue; fi
25
+ # (b) la línea declara el modo legacy
26
+ case "$linea" in
27
+ *legacy*|*LEGACY*|*Legacy*) continue ;;
28
+ esac
29
+ violaciones="$violaciones
30
+ $linea"
31
+ fail=1
32
+ done <<< "$hits"
33
+
34
+ if [ "$fail" = 0 ]; then
35
+ echo "✓ check-runtime-purity: skills/commands/agents no instruyen el fichero de estado fuera de legacy."
36
+ exit 0
37
+ fi
38
+ echo "✗ check-runtime-purity: menciones del fichero de estado fuera de las excepciones declaradas:"
39
+ echo "$violaciones"
40
+ echo ""
41
+ echo "Migra el protocolo a slice-ops.sh/release-ops.sh, o declara la línea como modo legacy,"
42
+ echo "o añade el fichero a scripts/runtime-purity-allow.txt si es documentación de legacy."
43
+ exit 1
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
  """front-plan.py — selecciona el conjunto disjunto máximo de épicas no fundacionales.
3
3
 
4
+ [EP-OR-08-C] Este script SIGUE SIENDO LOCAL (spec §4.2-C rev. 4): es el generador del plan
5
+ (conjunto disjunto + merge_order) que un humano aprueba al abrir el front en la consola del
6
+ hub. No llama al runtime ni lee el estado: entra JSON por stdin, sale JSON por stdout.
7
+
4
8
  stdin: JSON [{"epica","layer","files_scope":[glob]}]
5
9
  stdout: {"selected":[...],"serialized":[{"epica","reason"}],"excluded_foundational":[...]}
6
10
  Determinista: orden por 'epica'. Solape de globs por prefijo de directorio o glob idéntico.
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """graph-bundle.py — prepara el BUNDLE DE GRAFO que un ADMIN sube en la consola del hub.
3
+
4
+ [EP-OR-08-C] El import del grafo es un acto de GOBIERNO (spec §4.1-D rev. 4): la superficie
5
+ `POST …/graph/import` exige sesión de ADMIN y rechaza el token de agente. Por eso el arnés
6
+ **prepara y valida** el bundle en local — nunca lo sube — y `/build:onboard` se lo entrega a
7
+ una persona.
8
+
9
+ Uso:
10
+ python3 graph-bundle.py < epics.json > bundle.json # avisos por stderr
11
+
12
+ Entrada (stdin, JSON):
13
+ {"project_ref": "…",
14
+ "epics": [{"code","title","layer","files_scope":[],"depends_on":[],
15
+ "stories":[{"id","title"}]}],
16
+ "release_lines": [{"id","epics":[]}]}
17
+
18
+ Salida (stdout, JSON): el bundle normalizado y DETERMINISTA (orden estable por código);
19
+ los problemas no bloquean: se listan como avisos en stderr y en `warnings` del bundle, con
20
+ el criterio del arnés — nada se pierde en silencio, nada bloquea la preparación.
21
+
22
+ Códigos de salida: 0 = bundle generado (con o sin avisos) · 1 = entrada ilegible.
23
+ """
24
+ import json
25
+ import sys
26
+ import datetime
27
+
28
+ BUNDLE_VERSION = 1
29
+
30
+
31
+ def norm_list(v):
32
+ return [x for x in v if isinstance(x, str)] if isinstance(v, list) else []
33
+
34
+
35
+ def main():
36
+ avisos = []
37
+ try:
38
+ raw = json.load(sys.stdin)
39
+ except Exception as e:
40
+ print("graph-bundle: entrada ilegible (se esperaba JSON por stdin): %s" % e,
41
+ file=sys.stderr)
42
+ return 1
43
+ if not isinstance(raw, dict):
44
+ print("graph-bundle: la entrada debe ser un objeto JSON", file=sys.stderr)
45
+ return 1
46
+
47
+ epicas = {}
48
+ for e in raw.get("epics") or []:
49
+ if not isinstance(e, dict):
50
+ continue
51
+ code = e.get("code")
52
+ if not isinstance(code, str) or not code:
53
+ avisos.append("épica sin `code`: descartada")
54
+ continue
55
+ if code in epicas:
56
+ avisos.append("código duplicado %s: se conserva la PRIMERA aparición" % code)
57
+ continue
58
+ if not e.get("layer"):
59
+ avisos.append("%s sin `layer`: el DoR no podrá aplicar la regla de cimiento" % code)
60
+ historias = []
61
+ for h in e.get("stories") or []:
62
+ if isinstance(h, dict) and isinstance(h.get("id"), str):
63
+ historias.append({"id": h["id"], "title": h.get("title")})
64
+ epicas[code] = {
65
+ "code": code,
66
+ "title": e.get("title"),
67
+ "layer": e.get("layer"),
68
+ "files_scope": sorted(norm_list(e.get("files_scope"))),
69
+ "depends_on": sorted(norm_list(e.get("depends_on"))),
70
+ "stories": sorted(historias, key=lambda h: h["id"]),
71
+ }
72
+
73
+ for code, e in epicas.items():
74
+ for dep in e["depends_on"]:
75
+ if dep not in epicas:
76
+ avisos.append("%s depende de %s, que no está en el grafo" % (code, dep))
77
+
78
+ # Ciclos: DFS iterativa (nada de recursión: un grafo hostil no debe reventar la pila).
79
+ estado = {}
80
+ for raiz in sorted(epicas):
81
+ if estado.get(raiz):
82
+ continue
83
+ pila = [(raiz, iter(epicas[raiz]["depends_on"]))]
84
+ estado[raiz] = 1
85
+ while pila:
86
+ nodo, it = pila[-1]
87
+ avanzo = False
88
+ for dep in it:
89
+ if dep not in epicas:
90
+ continue
91
+ if estado.get(dep) == 1:
92
+ avisos.append("ciclo de dependencias detectado: %s → %s" % (nodo, dep))
93
+ continue
94
+ if not estado.get(dep):
95
+ estado[dep] = 1
96
+ pila.append((dep, iter(epicas[dep]["depends_on"])))
97
+ avanzo = True
98
+ break
99
+ if not avanzo:
100
+ estado[nodo] = 2
101
+ pila.pop()
102
+
103
+ lineas = []
104
+ for r in raw.get("release_lines") or []:
105
+ if not isinstance(r, dict) or not isinstance(r.get("id"), str):
106
+ continue
107
+ miembros = [c for c in norm_list(r.get("epics"))]
108
+ for c in miembros:
109
+ if c not in epicas:
110
+ avisos.append("la línea %s referencia %s, que no está en el grafo" % (r["id"], c))
111
+ lineas.append({"id": r["id"], "epics": miembros})
112
+ lineas.sort(key=lambda r: r["id"])
113
+
114
+ bundle = {
115
+ "bundle_version": BUNDLE_VERSION,
116
+ "kind": "graph",
117
+ "project_ref": raw.get("project_ref"),
118
+ "generated_at": datetime.datetime.now(datetime.timezone.utc)
119
+ .strftime("%Y-%m-%dT%H:%M:%SZ"),
120
+ "generated_by": "trycore-build-harness/graph-bundle.py",
121
+ "epics": [epicas[c] for c in sorted(epicas)],
122
+ "release_lines": lineas,
123
+ "warnings": avisos,
124
+ }
125
+ json.dump(bundle, sys.stdout, indent=2, ensure_ascii=False, sort_keys=False)
126
+ sys.stdout.write("\n")
127
+ for a in avisos:
128
+ print("aviso: %s" % a, file=sys.stderr)
129
+ return 0
130
+
131
+
132
+ if __name__ == "__main__":
133
+ sys.exit(main())
@@ -0,0 +1,5 @@
1
+ # Ficheros que TODAVÍA instruyen el protocolo del fichero de estado local.
2
+ # Es un ratchet: cada tarea de EP-OR-08-C quita los suyos y el check impide que vuelvan.
3
+ # Al cerrar el sub-slice debe quedar SOLO el protocolo legacy, que vive mientras viva
4
+ # legacy (spec §4.2-E).
5
+ skills/building-a-slice/references/state-protocol.md
@@ -5,7 +5,7 @@ DIR="$(cd "$(dirname "$0")" && pwd)"
5
5
  FAILED=0
6
6
 
7
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
8
+ for t in test-schema.sh test-config.sh test-runtime-client.sh test-hooks-runtime.sh test-skill-ops.sh test-reconciler.sh test-context-monitor.sh test-front-plan.sh test-install.sh; do
9
9
  if bash "$DIR/tests/$t"; then echo "smoke: $t OK"; else echo "smoke: $t FALLÓ"; FAILED=1; fi
10
10
  done
11
11
 
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """Stub HTTP mínimo del runtime para tests de runtime-client.sh, sin depender del hub
3
+ real (que se construye en otro repo/sesión — EP-OR-13). Uso:
4
+ python3 http-stub.py <port> <routes.json> <calls-log.jsonl>
5
+ routes.json: {"METHOD /path": {"status": 200, "body": {...}}, ...} — se relee EN CADA
6
+ request (no solo al arrancar), así un test puede reescribir routes.json a mitad de
7
+ ejecución para simular una segunda respuesta del mismo endpoint.
8
+ Cada request recibida se apendiza como una línea JSON en calls-log.jsonl (método, path,
9
+ header Authorization, body) para que el test verifique lo que el cliente envió.
10
+ """
11
+ import sys, json, http.server
12
+
13
+ PORT = int(sys.argv[1])
14
+ ROUTES_PATH = sys.argv[2]
15
+ CALLS_LOG = sys.argv[3]
16
+
17
+
18
+ class Handler(http.server.BaseHTTPRequestHandler):
19
+ def _handle(self, method):
20
+ length = int(self.headers.get('Content-Length', 0) or 0)
21
+ body = self.rfile.read(length) if length else b''
22
+ with open(CALLS_LOG, 'a') as f:
23
+ f.write(json.dumps({
24
+ "method": method, "path": self.path,
25
+ "auth": self.headers.get('Authorization', ''),
26
+ "body": body.decode('utf-8', 'replace'),
27
+ }) + "\n")
28
+ try:
29
+ routes = json.loads(open(ROUTES_PATH).read())
30
+ except Exception:
31
+ routes = {}
32
+ key = f"{method} {self.path}"
33
+ route = routes.get(key, {"status": 404, "body": {"error": "not_found"}})
34
+ self.send_response(route["status"])
35
+ self.send_header('Content-Type', 'application/json')
36
+ self.end_headers()
37
+ self.wfile.write(json.dumps(route["body"]).encode('utf-8'))
38
+
39
+ def do_GET(self): self._handle("GET")
40
+ def do_POST(self): self._handle("POST")
41
+ def do_PUT(self): self._handle("PUT")
42
+ def log_message(self, *a): pass
43
+
44
+
45
+ if __name__ == "__main__":
46
+ http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # test-baseline-verdict.sh — prueba baseline-verdict.sh (skill building-a-slice) contra
3
+ # un repo git temporal con remoto local y una suite fake de rojos controlados.
4
+ set -uo pipefail
5
+ ROOT="$(git rev-parse --show-toplevel)"
6
+ SCRIPT="$ROOT/skills/building-a-slice/assets/baseline-verdict.sh"
7
+ [ -f "$SCRIPT" ] || { echo "FAIL no existe baseline-verdict.sh"; exit 1; }
8
+ fail=0
9
+ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
10
+
11
+ # ── Fixture: remoto bare + clon con suite fake ───────────────────────────────
12
+ git init --quiet --bare "$TMP/remote.git"
13
+ git clone --quiet "$TMP/remote.git" "$TMP/proj" 2>/dev/null
14
+ cd "$TMP/proj"
15
+ git config user.email t@t.local && git config user.name t
16
+ git symbolic-ref HEAD refs/heads/main
17
+
18
+ # Suite fake agnóstica: cada tests/*.txt con contenido "red" pinta "FAIL <fichero>".
19
+ cat > suite.sh <<'SH'
20
+ #!/usr/bin/env bash
21
+ rc=0
22
+ for f in tests/*.txt; do
23
+ if grep -q red "$f"; then echo "FAIL $f"; rc=1; else echo "PASS $f"; fi
24
+ done
25
+ exit $rc
26
+ SH
27
+ mkdir tests
28
+ echo red > tests/heredado.txt # rojo ambiental heredado
29
+ echo green > tests/sano.txt
30
+ git add -A && git commit --quiet -m base && git push --quiet -u origin main 2>/dev/null
31
+
32
+ export BASELINE_SUITE_CMD="bash suite.sh"
33
+ export BASELINE_LINK_DIRS=""
34
+
35
+ # ── capture contra origin/main escribe el JSON con el rojo heredado ──────────
36
+ SHA="$(git rev-parse origin/main)"
37
+ bash "$SCRIPT" capture origin/main >/dev/null 2>&1
38
+ [ $? -eq 0 ] && echo "OK capture rc=0" || { echo "FAIL capture rc"; fail=1; }
39
+ CACHE=".baseline/baseline-verdict.${SHA}.json"
40
+ [ -f "$CACHE" ] && echo "OK capture escribe JSON por sha" || { echo "FAIL falta $CACHE"; fail=1; }
41
+ python3 - "$CACHE" "$SHA" <<'PY' && echo "OK contenido del veredicto" || { echo "FAIL contenido del veredicto"; fail=1; }
42
+ import json, sys
43
+ d = json.load(open(sys.argv[1]))
44
+ assert d["sha"] == sys.argv[2], d["sha"]
45
+ assert d["red_files"] == ["tests/heredado.txt"], d["red_files"]
46
+ assert d["captured_at"] and d["suite_cmd"], d
47
+ PY
48
+
49
+ # ── compare sin cambios: heredado no cuenta como regresión ───────────────────
50
+ out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
51
+ [ $rc -eq 0 ] && echo "OK compare limpio rc=0" || { echo "FAIL compare limpio (rc=$rc)"; fail=1; }
52
+ echo "$out" | grep -q "heredados sin cambio: 1" && echo "OK reporta heredado" || { echo "FAIL no reporta heredado"; fail=1; }
53
+
54
+ # ── regresión nueva en el working tree → rc=1 y la nombra ────────────────────
55
+ echo red > tests/sano.txt
56
+ out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
57
+ [ $rc -eq 1 ] && echo "OK compare detecta regresión (rc=1)" || { echo "FAIL rc regresión (rc=$rc)"; fail=1; }
58
+ echo "$out" | grep -q "tests/sano.txt" && echo "OK nombra el fichero regresado" || { echo "FAIL no nombra la regresión"; fail=1; }
59
+
60
+ # ── arreglo del heredado → rc=0 y cuenta 1 arreglado ─────────────────────────
61
+ echo green > tests/sano.txt
62
+ echo green > tests/heredado.txt
63
+ out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
64
+ [ $rc -eq 0 ] && echo "OK compare tras arreglo rc=0" || { echo "FAIL rc tras arreglo (rc=$rc)"; fail=1; }
65
+ echo "$out" | grep -q "arreglados : 1" && echo "OK cuenta arreglado" || { echo "FAIL no cuenta arreglado"; fail=1; }
66
+ git checkout --quiet -- tests
67
+
68
+ # ── GUARD: ref local rechazado con rc=2 ──────────────────────────────────────
69
+ out="$(bash "$SCRIPT" capture main 2>&1)"; rc=$?
70
+ [ $rc -eq 2 ] && echo "OK guard rechaza ref local (rc=2)" || { echo "FAIL guard ref local (rc=$rc)"; fail=1; }
71
+ echo "$out" | grep -qi "origin/<destino>" && echo "OK guard explica el porqué" || { echo "FAIL guard sin mensaje"; fail=1; }
72
+
73
+ # ── GUARD: sha solo-local (no pusheado) rechazado ────────────────────────────
74
+ git checkout --quiet -b feature/local
75
+ echo x > local.txt && git add local.txt && git commit --quiet -m local
76
+ LOCAL_SHA="$(git rev-parse HEAD)"
77
+ out="$(bash "$SCRIPT" capture "$LOCAL_SHA" 2>&1)"; rc=$?
78
+ [ $rc -eq 2 ] && echo "OK guard rechaza sha no remoto (rc=2)" || { echo "FAIL guard sha local (rc=$rc)"; fail=1; }
79
+ git checkout --quiet main
80
+
81
+ # ── sha remoto verificado sí pasa el guard (capture por sha) ─────────────────
82
+ bash "$SCRIPT" capture "$SHA" >/dev/null 2>&1
83
+ [ $? -eq 0 ] && echo "OK acepta sha contenido en rama remota" || { echo "FAIL sha remoto rechazado"; fail=1; }
84
+
85
+ # ── invalidación: el remoto avanza → compare pide re-capturar (rc=3) ─────────
86
+ echo green > tests/nuevo.txt && git add tests/nuevo.txt && git commit --quiet -m avanza
87
+ git push --quiet origin main 2>/dev/null
88
+ out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
89
+ [ $rc -eq 3 ] && echo "OK cache invalidado al avanzar el remoto (rc=3)" || { echo "FAIL invalidación (rc=$rc)"; fail=1; }
90
+ echo "$out" | grep -q "re-captura" && echo "OK pide re-capturar" || { echo "FAIL no pide re-capturar"; fail=1; }
91
+
92
+ exit $fail
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ ROOT="$(git rev-parse --show-toplevel)"
4
+ source "$ROOT/hooks/build/lib/config.sh" 2>/dev/null || { echo "FAIL no existe lib/config.sh"; exit 1; }
5
+ fail=0
6
+ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
7
+
8
+ echo '{"context":{"warning_pct":40},"runtime":{"mode":"dual"}}' > "$TMP/cfg.json"
9
+ got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get context.warning_pct 35)"
10
+ [ "$got" = 40 ] && echo "OK config_get valor" || { echo "FAIL config_get valor ($got)"; fail=1; }
11
+ got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get context.critical_pct 25)"
12
+ [ "$got" = 25 ] && echo "OK config_get default" || { echo "FAIL config_get default ($got)"; fail=1; }
13
+ got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get runtime.mode legacy)"
14
+ [ "$got" = dual ] && echo "OK config_get anidado" || { echo "FAIL config_get anidado ($got)"; fail=1; }
15
+
16
+ # Regresión: state-io.sh sigue exponiendo config_get vía sourcing, cero cambio de
17
+ # comportamiento. Subshell AISLADO que solo sourcea state-io.sh (nunca config.sh
18
+ # directamente) — si state-io.sh dejara de sourcear config.sh, config_get no existiría
19
+ # aquí y el subshell fallaría. Sourcear ambos en el shell del test (como antes) no
20
+ # probaba nada: config_get ya estaba definida por el primer source, pasara lo que
21
+ # pasara con el segundo.
22
+ got="$(bash -c 'source "$1/hooks/build/lib/state-io.sh" 2>/dev/null && BUILD_CONFIG_FILE="$2" config_get context.warning_pct 35' _ "$ROOT" "$TMP/cfg.json" 2>&1)"
23
+ [ "$got" = 40 ] && echo "OK state-io re-exporta config_get" || { echo "FAIL state-io no re-exporta ($got)"; fail=1; }
24
+
25
+ exit $fail