refactor: reinforce harness to prevent contract violations
- verify.sh: validate prohibited dirs, gate nomenclature, feature schema, runtime consistency - orquestra-status: block direct writes to backlog/features.json - install.sh: add build artifacts to .gitignore template - AGENTS.md: document prohibited dirs, gate nomenclature, feature schema - Reset runtime-status to idle state
This commit is contained in:
@@ -17,7 +17,10 @@
|
|||||||
- Si `verify.sh` falla, no se cierra la feature.
|
- Si `verify.sh` falla, no se cierra la feature.
|
||||||
- **Solo se puede escribir en carpetas permitidas**: `project/`, `tests/`, `work/`, `backlog/`, `spec/`, `harness/`, `scripts/`, `platforms/`, `docs/`. Cualquier otra escritura será bloqueada por la extensión `orquestra-status`.
|
- **Solo se puede escribir en carpetas permitidas**: `project/`, `tests/`, `work/`, `backlog/`, `spec/`, `harness/`, `scripts/`, `platforms/`, `docs/`. Cualquier otra escritura será bloqueada por la extensión `orquestra-status`.
|
||||||
- **Al cerrar una feature, el leader ejecuta `scripts/close_feature.py <feature_id>`** que valida gates, actualiza el backlog y hace commit/push automáticamente.
|
- **Al cerrar una feature, el leader ejecuta `scripts/close_feature.py <feature_id>`** que valida gates, actualiza el backlog y hace commit/push automáticamente.
|
||||||
- **NUNCA editar `backlog/features.json` directamente**. Usar siempre `scripts/close_feature.py` para cerrar features.
|
- **NUNCA editar `backlog/features.json` directamente**. Usar siempre `scripts/close_feature.py` para cerrar features. La extensión `orquestra-status` bloquea escrituras directas a este archivo.
|
||||||
|
- **Directorios prohibidos**: `specs/` (plural), `apps/`, `frontend/`, `src/`, `lib/`. El código del producto va en `project/`.
|
||||||
|
- **Nomenclatura de gates**: usar `reviewer`, no `review`. El `verify.sh` valida que no se usen nombres deprecated.
|
||||||
|
- **Schema de features**: todas las features deben tener al menos `id` y `status`. Las gates deben usar nombres válidos: `reviewer`, `security`, `qa`, `close`, `leader`.
|
||||||
|
|
||||||
## Reentrada
|
## Reentrada
|
||||||
- Releer `work/current.md`, `work/runtime-status.json` y artefactos de la feature activa.
|
- Releer `work/current.md`, `work/runtime-status.json` y artefactos de la feature activa.
|
||||||
|
|||||||
@@ -215,6 +215,14 @@ function guardWriteOrEdit(event: ToolCallEvent, ctx: ExtensionContext): { block?
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bloquear escrituras directas a backlog/features.json (debe usar close_feature.py)
|
||||||
|
if (relPath === "backlog/features.json") {
|
||||||
|
return {
|
||||||
|
block: true,
|
||||||
|
reason: `Orquestra bloqueó '${relPath}': NUNCA edites backlog/features.json directamente. Usa scripts/close_feature.py para cerrar features. Este script valida gates y actualiza el backlog automáticamente.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isRootProductFile(relPath)) {
|
if (isRootProductFile(relPath)) {
|
||||||
return {
|
return {
|
||||||
block: true,
|
block: true,
|
||||||
|
|||||||
@@ -79,16 +79,39 @@ append_gitignore_block() {
|
|||||||
local gitignore=$1
|
local gitignore=$1
|
||||||
touch "$gitignore"
|
touch "$gitignore"
|
||||||
if ! grep -q '^# BEGIN ORQUESTRA$' "$gitignore"; then
|
if ! grep -q '^# BEGIN ORQUESTRA$' "$gitignore"; then
|
||||||
cat >>"$gitignore" <<'EOF'
|
cat >>"$gitignore" <<'INNEREOF'
|
||||||
|
|
||||||
# BEGIN ORQUESTRA
|
# BEGIN ORQUESTRA
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.codegraph/
|
.codegraph/
|
||||||
.atl/
|
.atl/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
.next/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
# END ORQUESTRA
|
# END ORQUESTRA
|
||||||
EOF
|
INNEREOF
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ for d in "${required_dirs[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
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 \( \
|
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 '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.go' -o -name '*.rs' -o \
|
||||||
-name '*.java' -o -name '*.php' -o -name '*.rb' \
|
-name '*.java' -o -name '*.php' -o -name '*.rb' \
|
||||||
@@ -140,14 +150,39 @@ if len(in_progress) > 1:
|
|||||||
print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)")
|
print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)")
|
||||||
sys.exit(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:
|
for f in features:
|
||||||
fid = str(f.get('id', '')).strip()
|
fid = str(f.get('id', '')).strip()
|
||||||
status = f.get('status')
|
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:
|
if status not in valid:
|
||||||
print(f"[FAIL] Estado inválido en feature {fid}: {status}")
|
print(f"[FAIL] Estado inválido en feature {fid}: {status}")
|
||||||
sys.exit(1)
|
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':
|
if status == 'done':
|
||||||
|
# Check artifacts exist
|
||||||
d = root / 'work' / 'artifacts' / fid
|
d = root / 'work' / 'artifacts' / fid
|
||||||
req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json']
|
req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json']
|
||||||
missing = [name for name in req if not (d / name).is_file()]
|
missing = [name for name in req if not (d / name).is_file()]
|
||||||
@@ -155,6 +190,11 @@ for f in features:
|
|||||||
print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}")
|
print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}")
|
||||||
sys.exit(1)
|
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 = {
|
expected = {
|
||||||
'reviewer.json': 'reviewer',
|
'reviewer.json': 'reviewer',
|
||||||
'security.json': 'security',
|
'security.json': 'security',
|
||||||
@@ -191,6 +231,24 @@ if not isinstance(runtime.get('timeline'), list):
|
|||||||
print('[FAIL] work/runtime-status.json timeline debe ser una lista')
|
print('[FAIL] work/runtime-status.json timeline debe ser una lista')
|
||||||
sys.exit(1)
|
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(f"[OK] backlog válido ({len(features)} features)")
|
||||||
print('[OK] runtime-status válido')
|
print('[OK] runtime-status válido')
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -1,138 +1,11 @@
|
|||||||
{
|
{
|
||||||
"feature_id": "F-001",
|
"feature_id": null,
|
||||||
"stage": "intake",
|
"stage": "idle",
|
||||||
"agent": "leader",
|
"agent": "leader",
|
||||||
"action": "Gate validation reforzado: agent_status.py bloquea close sin gates + close_feature.py valida antes de actualizar backlog",
|
"action": "Sin ejecución activa",
|
||||||
"state": "done",
|
"state": "waiting",
|
||||||
"next_agent": "leader",
|
"next_agent": "leader",
|
||||||
"waiting_for": "Probar en producción",
|
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
||||||
"updated_at": "2026-08-17T05:25:08Z",
|
"updated_at": "2026-08-17T16:26:21Z",
|
||||||
"timeline": [
|
"timeline": []
|
||||||
{
|
|
||||||
"ts": "2026-08-15T07:05:52Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "status extension hangs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T07:08:02Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "verify.sh OK"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T07:12:07Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "emoji overlap + context handoff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T07:13:29Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "verify.sh OK"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T07:16:37Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "fresh Pi process per stage"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T07:18:21Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "py_compile + dry-run + verify.sh OK"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T13:20:09Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "context sharing doc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T13:21:11Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "verify.sh + run_stage dry-run OK"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T15:56:29Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "parent session launcher"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T15:58:00Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "verify.sh OK; dry-run confirms fresh Pi flags"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T16:08:27Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "running",
|
|
||||||
"message": "selective Engram save + install target"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-15T16:09:07Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "source verify OK; target verify OK; target dry-run OK"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-16T22:06:15Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "ALLOWED_WRITE_DIRS + AGENTS.md + docs actualizados"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-17T05:19:30Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "scripts/commit_feature.sh + workflow post_actions + docs actualizados"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-17T05:24:32Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "close",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Starting close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-17T05:24:32Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "close",
|
|
||||||
"state": "done",
|
|
||||||
"message": "Test close with all gates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-17T05:24:51Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "close",
|
|
||||||
"state": "running",
|
|
||||||
"message": "Starting close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ts": "2026-08-17T05:25:08Z",
|
|
||||||
"agent": "leader",
|
|
||||||
"stage": "intake",
|
|
||||||
"state": "done",
|
|
||||||
"message": "Scripts: close_feature.py, agent_status.py actualizado, run_stage.py con instrucción de no editar backlog"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user