#!/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 echo "── 1) Verificando estructura base ─────────────────────" required=( "AGENTS.md" "CHECKPOINTS.md" "harness/agents.matrix.yml" "harness/workflow.stages.yml" "harness/policies/governance.md" "harness/policies/security.md" "harness/policies/quality.md" "harness/contracts/handoff.md" "harness/contracts/evidence.schema.json" "harness/model-routing.yml" "docs/context-handoff.md" "spec/product.md" "spec/tech.md" "spec/acceptance.md" "backlog/features.json" "work/current.md" "work/history.md" "work/runtime-status.json" "scripts/install.sh" "scripts/agent_status.py" "scripts/new_ticket.py" "scripts/pi_orquestra.sh" "scripts/run_stage.py" "scripts/commit_feature.sh" "scripts/close_feature.py" "scripts/validate_conventional_commit.py" "scripts/version.py" "scripts/install_git_hooks.sh" "scripts/git-hooks/commit-msg" "project/VERSION" "platforms/pi/README.md" "platforms/pi/extensions/orquestra-status/index.ts" "platforms/pi/extensions/orquestra-web-fetch.ts" ) for f in "${required[@]}"; do if [ -f "$f" ]; then ok "Existe $f" else fail "Falta $f" EXIT_CODE=1 fi done required_dirs=("project") for d in "${required_dirs[@]}"; do if [ -d "$d" ]; then ok "Existe directorio $d" else fail "Falta directorio $d" EXIT_CODE=1 fi done # Check for prohibited directories prohibited_dirs=("specs" "apps" "frontend" "src" "lib") for d in "${prohibited_dirs[@]}"; do if [ -d "$d" ]; then fail "Directorio prohibido encontrado: $d/ (usar project/ para código)" EXIT_CODE=1 fi done ok "Sin directorios prohibidos (specs/, apps/, frontend/, src/, lib/)" root_product_files=$(find . -mindepth 1 -maxdepth 1 -type f \( \ -name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.go' -o -name '*.rs' -o \ -name '*.java' -o -name '*.php' -o -name '*.rb' \ \) -print 2>/dev/null) if [ -n "$root_product_files" ]; then fail "Archivos de producto/código en la raíz no permitidos; usar project/: $root_product_files" EXIT_CODE=1 else ok "Sin archivos de producto/código en la raíz" fi python3 - <<'PY' import json import pathlib import re import subprocess import sys root = pathlib.Path('.') version_path = root / 'project' / 'VERSION' semver = re.compile(r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$') try: version = version_path.read_text(encoding='utf-8').strip() except Exception as e: print(f'[FAIL] project/VERSION no se puede leer: {e}') sys.exit(1) if not semver.match(version): print(f'[FAIL] project/VERSION no es semver válido: {version}') sys.exit(1) for rel in [ 'project/package.json', 'project/apps/admin/package.json', 'project/apps/pos/package.json', 'project/frontend/package.json', 'project/storefront/package.json', ]: path = root / rel if not path.exists(): continue data = json.loads(path.read_text(encoding='utf-8')) if data.get('version') != version: print(f"[FAIL] {rel} version {data.get('version')} != project/VERSION {version}") sys.exit(1) try: msg = subprocess.check_output(['git', 'log', '-1', '--pretty=%B'], text=True).strip() except Exception: msg = '' if msg: result = subprocess.run(['python3', 'scripts/validate_conventional_commit.py', '--message', msg]) if result.returncode != 0: print('[FAIL] Último commit no cumple Conventional Commits') sys.exit(1) print(f'[OK] semver válido y sincronizado: {version}') print('[OK] último commit cumple Conventional Commits') PY if [ $? -ne 0 ]; then EXIT_CODE=1; fi echo "" echo "── 2) Validando requisitos Pi ─────────────────────────" if command -v pi >/dev/null 2>&1; then ok "Pi instalado: $(command -v pi)" else fail "Pi es obligatorio antes de instalar Orquestra" EXIT_CODE=1 fi ENGRAM_EXT="$HOME/.pi/agent/npm/node_modules/gentle-engram/index.ts" if [ -f "$ENGRAM_EXT" ]; then ok "Engram instalado: $ENGRAM_EXT" else fail "Engram es obligatorio para la memoria Orquestra: falta $ENGRAM_EXT" EXIT_CODE=1 fi if [ -d ".pi/subagents" ] || [ -f ".pi/subagents.json" ]; then fail "Orquestra es secuencial: no debe instalar .pi/subagents ni .pi/subagents.json" EXIT_CODE=1 else ok "Sin subagentes project-local" fi if [ -d ".pi/extensions" ]; then extra_extensions=$(find .pi/extensions -mindepth 1 -maxdepth 1 \ ! -name orquestra-status \ ! -name orquestra-web-fetch.ts \ -print 2>/dev/null) if [ -n "$extra_extensions" ]; then fail "Extensiones project-local no declaradas: $extra_extensions" EXIT_CODE=1 else ok "Extensiones project-local declaradas" fi else warn "Sin .pi/extensions; la instalación debe copiar extensiones Orquestra en proyectos" fi echo "" echo "── 3) Validando backlog + runtime ─────────────────────" python3 - <<'PY' import json import pathlib import sys root = pathlib.Path('.') path = root / 'backlog' / 'features.json' 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) valid = set(data.get('rules', {}).get('valid_status', ["pending", "in_progress", "blocked", "done"])) features = data.get('features', []) if not isinstance(features, list): print('[FAIL] features debe ser una lista') 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) # Required fields for feature schema required_fields = ['id', 'status'] # Valid gate names valid_gate_names = {'reviewer', 'security', 'qa', 'close', 'leader'} for f in features: fid = str(f.get('id', '')).strip() status = f.get('status') # Check required fields for field in required_fields: if field not in f: print(f"[FAIL] Feature {fid} missing required field: {field}") sys.exit(1) if status not in valid: print(f"[FAIL] Estado inválido en feature {fid}: {status}") sys.exit(1) # Validate gate nomenclature gates = f.get('gates', {}) if 'review' in gates: print(f"[FAIL] Feature {fid} uses deprecated gate name 'review' (should be 'reviewer')") sys.exit(1) # Check for invalid gate names for gate_name in gates.keys(): if gate_name not in valid_gate_names: print(f"[FAIL] Feature {fid} has invalid gate name: {gate_name}") sys.exit(1) if status == 'done': # Check artifacts exist d = root / 'work' / 'artifacts' / fid req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.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) # Check gates in backlog match artifacts if not gates.get('reviewer') or not gates.get('security') or not gates.get('qa'): print(f"[FAIL] Feature {fid} done sin gates aprobados en backlog") sys.exit(1) expected = { 'reviewer.json': 'reviewer', 'security.json': 'security', 'qa.json': 'qa', 'leader-close.json': 'leader', } for filename, agent in expected.items(): 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') != 'APPROVED': print(f"[FAIL] {fid}/{filename} no está APPROVED") sys.exit(1) runtime_path = root / 'work' / 'runtime-status.json' try: runtime = json.loads(runtime_path.read_text(encoding='utf-8')) except Exception as e: print(f"[FAIL] work/runtime-status.json inválido: {e}") sys.exit(1) required_runtime = ['feature_id', 'stage', 'agent', 'action', 'state', 'next_agent', 'waiting_for', 'updated_at', 'timeline'] missing_runtime = [key for key in required_runtime if key not in runtime] if missing_runtime: print(f"[FAIL] work/runtime-status.json incompleto: {', '.join(missing_runtime)}") sys.exit(1) if not isinstance(runtime.get('timeline'), list): print('[FAIL] work/runtime-status.json timeline debe ser una lista') sys.exit(1) # Check runtime consistency with backlog runtime_feature_id = runtime.get('feature_id') if runtime_feature_id: feature_in_backlog = None for f in features: if f.get('id') == runtime_feature_id: feature_in_backlog = f break if feature_in_backlog: runtime_state = runtime.get('state') backlog_status = feature_in_backlog.get('status') # If feature is pending in backlog but running/done in runtime, that's inconsistent if backlog_status == 'pending' and runtime_state in ['running', 'done']: print(f"[FAIL] Inconsistencia: {runtime_feature_id} está pending en backlog pero {runtime_state} en runtime-status") sys.exit(1) print(f"[OK] backlog válido ({len(features)} features)") print('[OK] runtime-status válido') PY if [ $? -ne 0 ]; then EXIT_CODE=1; fi echo "" echo "── 4) Verificación de tests/build (opcional 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 ok "Sin suite de producto detectada; no aplica al harness base" fi echo "" echo "── 5) Resumen ─────────────────────────────────────────" if [ $EXIT_CODE -eq 0 ]; then ok "Orquestra verificado. Puedes trabajar." else fail "Orquestra NO verificado. Corrige antes de continuar." fi exit $EXIT_CODE