#!/usr/bin/env bash set -u RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' ok() { printf "${GREEN}[OK]${NC} %s\n" "$1"; } warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1"; } fail() { printf "${RED}[FAIL]${NC} %s\n" "$1"; } EXIT_CODE=0 cd "$(dirname "$0")/.." || exit 1 echo "── 1) Verificando estructura base ─────────────────────" required=( "AGENTS.md" "CHECKPOINTS.md" "README.md" "HOWTO.md" "TEMPLATE.md" "docs/repository-layout.md" "docs/scripts-reference.md" "harness/agents.matrix.yml" "harness/workflow.stages.yml" "harness/policies/governance.md" "harness/policies/security.md" "harness/policies/quality.md" "harness/policies/language.md" "harness/policies/model-routing.md" "harness/models.profiles.yml" "harness/contracts/handoff.md" "harness/contracts/evidence.schema.json" "spec/product.md" "spec/tech.md" "spec/acceptance.md" "spec/bdd/README.md" "spec/bdd/features/README.md" "spec/sdd/README.md" "spec/sdd/components/README.md" "spec/sdd/decisions/README.md" "features/README.md" "project/README.md" "backlog/features.json" "work/current.md" "work/history.md" "work/runtime-status.json" "scripts/agent_status.py" "scripts/new_ticket.py" "scripts/publish_ticket.py" "scripts/install_into_repo.sh" "scripts/start.sh" "platforms/pi/README.md" "platforms/opencode/README.md" ) for f in "${required[@]}"; do if [ -f "$f" ]; then ok "Existe $f" else fail "Falta $f" EXIT_CODE=1 fi done echo "" echo "── 1.5) Validando git repo ───────────────────────────" if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then ok "Git repo detectado" if git remote | grep -q .; then ok "Git remote configurado" else warn "Sin git remote configurado (publish requerirá remote)" fi else fail "Este proyecto debe vivir dentro de un git repo" EXIT_CODE=1 fi echo "" echo "── 2) Validando backlog + gates ───────────────────────" python3 - <<'PY' import json import pathlib import sys root = pathlib.Path('.') path = root / 'backlog' / 'features.json' level_choices = {'low', 'med', 'high'} try: data = json.loads(path.read_text(encoding='utf-8')) except Exception as e: print(f"[FAIL] backlog/features.json inválido: {e}") sys.exit(1) rules = data.get('rules', {}) valid_status = set(rules.get('valid_status', ["pending", "in_progress", "blocked", "done"])) valid_types = set(rules.get('valid_types', ["feature", "fix", "bug", "chore"])) features = data.get('features', []) if not isinstance(features, list): print('[FAIL] features debe ser una lista') sys.exit(1) ids = [str(f.get('id', '')).strip() for f in features] if len(ids) != len(set(ids)): print('[FAIL] Hay IDs de feature duplicados en backlog/features.json') sys.exit(1) in_progress = [f for f in features if f.get('status') == 'in_progress'] if len(in_progress) > 1: print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)") sys.exit(1) for f in features: fid = str(f.get('id', '')).strip() status = f.get('status') title = str(f.get('title', '')).strip() acceptance = f.get('acceptance') gates = f.get('gates', {}) if not fid: print('[FAIL] Hay una feature sin id') sys.exit(1) if not title: print(f"[FAIL] Feature {fid} sin title") sys.exit(1) if status not in valid_status: print(f"[FAIL] Estado inválido en feature {fid}: {status}") sys.exit(1) if not isinstance(acceptance, list) or not acceptance or any(not str(item).strip() for item in acceptance): print(f"[FAIL] Feature {fid} debe tener acceptance como lista no vacía") sys.exit(1) ticket_type = f.get('type') if ticket_type is not None and ticket_type not in valid_types: print(f"[FAIL] Feature {fid} tiene type inválido: {ticket_type}") sys.exit(1) for field in ('priority', 'risk'): value = f.get(field) if value is not None and value not in level_choices: print(f"[FAIL] Feature {fid} tiene {field} inválido: {value}") sys.exit(1) for field in ('scope_in', 'scope_out'): value = f.get(field) if value is not None: if not isinstance(value, list) or any(not str(item).strip() for item in value): print(f"[FAIL] Feature {fid} tiene {field} inválido") sys.exit(1) if gates: for gate_name in ('review', 'security', 'qa'): gate_value = gates.get(gate_name) if not isinstance(gate_value, bool): print(f"[FAIL] Feature {fid} tiene gates.{gate_name} inválido") sys.exit(1) if status == 'done': d = root / 'work' / 'artifacts' / fid req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json', 'documenter.md', 'publish.json'] missing = [name for name in req if not (d / name).is_file()] if missing: print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}") sys.exit(1) expected = { 'reviewer.json': ('reviewer', 'APPROVED'), 'security.json': ('security', 'APPROVED'), 'qa.json': ('qa', 'APPROVED'), 'leader-close.json': ('leader', 'APPROVED'), 'publish.json': ('leader', 'PUBLISHED'), } for filename, rule in expected.items(): agent, verdict = rule try: obj = json.loads((d / filename).read_text(encoding='utf-8')) except Exception as e: print(f"[FAIL] {fid}/{filename} inválido: {e}") sys.exit(1) if obj.get('agent') != agent: print(f"[FAIL] {fid}/{filename} agent debe ser '{agent}'") sys.exit(1) if obj.get('verdict') != verdict: print(f"[FAIL] {fid}/{filename} no está {verdict}") sys.exit(1) if filename == 'publish.json' and obj.get('pushed') is not True: print(f"[FAIL] {fid}/{filename} debe tener pushed=true") sys.exit(1) print(f"[OK] backlog válido ({len(features)} features)") PY if [ $? -ne 0 ]; then EXIT_CODE=1; fi python3 - <<'PY' import json import pathlib import sys path = pathlib.Path('work/runtime-status.json') required = ['feature_id', 'stage', 'agent', 'action', 'state', 'next_agent', 'waiting_for', 'updated_at', 'timeline'] try: data = json.loads(path.read_text(encoding='utf-8')) except Exception as e: print(f"[FAIL] work/runtime-status.json inválido: {e}") sys.exit(1) missing = [key for key in required if key not in data] if missing: print(f"[FAIL] work/runtime-status.json incompleto: {', '.join(missing)}") sys.exit(1) if not isinstance(data.get('timeline'), list): print('[FAIL] work/runtime-status.json timeline debe ser una lista') sys.exit(1) print('[OK] runtime-status válido') PY if [ $? -ne 0 ]; then EXIT_CODE=1; fi echo "" echo "── 3) Verificación de tests/build (auto-detect) ───────" if [ -f "Makefile" ] && grep -qE '^test:' Makefile; then if make test; then ok "make test OK"; else fail "make test falló"; EXIT_CODE=1; fi elif [ -f "package.json" ]; then if command -v npm >/dev/null 2>&1; then if npm test --silent --if-present; then ok "npm test OK"; else fail "npm test falló"; EXIT_CODE=1; fi else warn "package.json detectado pero npm no está disponible" fi elif [ -d "tests" ]; then if command -v pytest >/dev/null 2>&1; then if pytest -q; then ok "pytest OK"; else fail "pytest falló"; EXIT_CODE=1; fi else if python3 -m unittest discover -s tests -v; then ok "unittest OK"; else fail "unittest falló"; EXIT_CODE=1; fi fi else warn "No se detectó suite automática (tests/ | Makefile test | package.json test)" fi echo "" echo "── 4) Overlay local opcional ─────────────────────────" if [ -x "scripts/verify.local.sh" ]; then if ./scripts/verify.local.sh; then ok "verify.local.sh OK" else fail "verify.local.sh falló" EXIT_CODE=1 fi elif [ -f "scripts/verify.local.sh" ]; then warn "scripts/verify.local.sh existe pero no es ejecutable" else warn "Sin overlay local (scripts/verify.local.sh)" fi echo "" echo "── 5) Resumen ─────────────────────────────────────────" if [ $EXIT_CODE -eq 0 ]; then ok "Harness verificado. Template listo para adaptar a cualquier proyecto." else fail "Harness NO verificado. Corrige antes de continuar." fi exit $EXIT_CODE