340 lines
11 KiB
Bash
Executable File
340 lines
11 KiB
Bash
Executable File
#!/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
|