feat: Orquestra - sequential orchestration runtime
- Context isolation: fresh Pi process per stage (run_stage.py) - Gate enforcement: blocks close without approved gates - Auto commit/push on feature close (close_feature.py) - Write restrictions: only allowed directories (ALLOWED_WRITE_DIRS) - Pi extension: orquestra-status with /orquestra-stage command - Documentation: context-handoff.md, updated README - Scripts: agent_status.py, verify.sh, install.sh updated
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
.codegraph/
|
||||
|
||||
# Local Pi runtime state
|
||||
.atl/
|
||||
.env
|
||||
22
AGENTS.md
22
AGENTS.md
@@ -1,19 +1,25 @@
|
||||
# AGENTS.md — Entry point del framework
|
||||
# AGENTS.md — Entrada de Orquestra
|
||||
|
||||
## Arranque obligatorio
|
||||
1. Leer `work/current.md`.
|
||||
2. Leer `backlog/features.json` y seleccionar **una** feature `pending`.
|
||||
3. Ejecutar `./scripts/verify.sh`.
|
||||
1. Ejecutar `./scripts/verify.sh`.
|
||||
2. Leer `work/current.md`.
|
||||
3. Leer `backlog/features.json` y seleccionar **una** feature `pending`.
|
||||
4. Seguir `harness/workflow.stages.yml` y `harness/agents.matrix.yml`.
|
||||
5. Actualizar estado visible con `python3 scripts/agent_status.py set ...` antes de cada stage.
|
||||
|
||||
## Reglas duras
|
||||
- Orquestra se ejecuta secuencialmente en Pi: no subagentes.
|
||||
- Para Pi limpio, arrancar con `./scripts/pi_orquestra.sh`, no con `pi` directo.
|
||||
- Una sola feature en `in_progress`.
|
||||
- Ningún agente pasa código por chat: todo va a `work/artifacts/<feature_id>/`.
|
||||
- Ningún rol pasa código por chat: todo va a `work/artifacts/<feature_id>/`.
|
||||
- `implementer` nunca marca `done`.
|
||||
- `done` requiere gates aprobados: `reviewer`, `security`, `qa`.
|
||||
- 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`.
|
||||
- **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.
|
||||
|
||||
## Reentrada (context loss)
|
||||
- Releer `work/current.md` y artefactos de la feature activa.
|
||||
## Reentrada
|
||||
- Releer `work/current.md`, `work/runtime-status.json` y artefactos de la feature activa.
|
||||
- Ejecutar `./scripts/verify.sh`.
|
||||
- Continuar desde “Próximo paso”.
|
||||
- Continuar desde el próximo stage pendiente.
|
||||
|
||||
170
HOWTO.md
170
HOWTO.md
@@ -1,145 +1,93 @@
|
||||
# HOWTO — Cómo usar ARNES Framework
|
||||
# HOWTO — usar Orquestra
|
||||
|
||||
Guía rápida para arrancar proyectos nuevos usando este framework.
|
||||
|
||||
---
|
||||
|
||||
## Fórmula base (siempre igual)
|
||||
|
||||
1. **Crear repo nuevo**
|
||||
2. **Copiar ARNES Framework dentro del repo**
|
||||
3. **Configurar spec + backlog**
|
||||
4. **Ejecutar verificación**
|
||||
5. **Empezar implementación por features (una a la vez)**
|
||||
|
||||
---
|
||||
|
||||
## 1) Crear repo
|
||||
## 1) Requisitos
|
||||
|
||||
```bash
|
||||
mkdir mi-proyecto
|
||||
cd mi-proyecto
|
||||
git init
|
||||
command -v pi
|
||||
command -v python3
|
||||
```
|
||||
|
||||
---
|
||||
Si `pi` no existe, no instales Orquestra todavía.
|
||||
|
||||
## 2) Copiar framework
|
||||
## 2) Instalar en un proyecto
|
||||
|
||||
Desde tu copia local de ARNES:
|
||||
Desde el repo fuente de Orquestra:
|
||||
|
||||
```bash
|
||||
cp -R /ruta/a/arnes/* .
|
||||
cp -R /ruta/a/arnes/.[!.]* . 2>/dev/null || true
|
||||
./scripts/install.sh /path/to/project-repo
|
||||
```
|
||||
|
||||
> Si usas plantilla remota, clónala y copia su contenido al repo nuevo.
|
||||
Para actualizar, ejecutá el mismo comando otra vez sobre el repo destino.
|
||||
|
||||
---
|
||||
La instalación es safe-update:
|
||||
- crear archivos faltantes
|
||||
- conservar o mergear archivos existentes
|
||||
- crear `project/` si falta y no pisar su contenido
|
||||
- no pisar progreso en `project/`, `work/`, `backlog/`, `spec/` ni `work/artifacts/`
|
||||
|
||||
## 3) Personalizar proyecto
|
||||
|
||||
Edita mínimo:
|
||||
|
||||
- `README.md` (contexto del proyecto)
|
||||
- `spec/product.md` (qué construir)
|
||||
- `spec/tech.md` (stack y límites técnicos)
|
||||
- `spec/acceptance.md` (criterios de aceptación)
|
||||
- `backlog/features.json` (features iniciales en `pending`)
|
||||
- `harness/agents.matrix.yml` (roles/permisos)
|
||||
- `harness/workflow.stages.yml` (flujo y gates)
|
||||
|
||||
---
|
||||
|
||||
## 4) Elegir plataforma (pi.dev u opencode)
|
||||
|
||||
Usa el adaptador correspondiente:
|
||||
|
||||
- `platforms/pi/`
|
||||
- `platforms/opencode/`
|
||||
|
||||
El núcleo del framework no cambia; solo cambian prompts/hooks/permisos de plataforma.
|
||||
|
||||
---
|
||||
|
||||
## 5) Inicializar estado de trabajo
|
||||
|
||||
Verifica que existan y estén limpios:
|
||||
|
||||
- `work/current.md`
|
||||
- `work/history.md`
|
||||
- `work/artifacts/`
|
||||
|
||||
Pon solo **1 feature activa** (`in_progress`) como máximo.
|
||||
|
||||
---
|
||||
|
||||
## 6) Ejecutar verificación inicial
|
||||
## 3) Verificar
|
||||
|
||||
```bash
|
||||
./scripts/verify.sh
|
||||
```
|
||||
|
||||
Si falla, **no empezar implementación** hasta dejar todo en verde.
|
||||
Debe comprobar:
|
||||
- estructura mínima
|
||||
- `project/` existente
|
||||
- ausencia de archivos de producto/código en la raíz (`*.py`, `*.js`, `*.ts`, `*.go`, `*.rs`, `*.java`, `*.php`, `*.rb`); usá `project/`
|
||||
- Pi instalado
|
||||
- extensión project-local permitida
|
||||
- sin `.pi/subagents/` ni `.pi/subagents.json`
|
||||
- extensiones Orquestra requeridas
|
||||
- backlog válido
|
||||
- runtime status válido
|
||||
|
||||
---
|
||||
## 4) Ejecutar desde Pi
|
||||
|
||||
## 7) Ciclo operativo por feature
|
||||
```bash
|
||||
cd <repo-instalado>
|
||||
./scripts/pi_orquestra.sh
|
||||
```
|
||||
|
||||
Orden obligatorio:
|
||||
Ese launcher usa `pi --no-extensions` y carga solo `orquestra-status` + `orquestra-web-fetch`, así Pi arranca limpio con las extensiones del proyecto.
|
||||
|
||||
1. `leader` orquesta
|
||||
2. `architect` define/ajusta diseño
|
||||
3. `implementer` implementa + tests
|
||||
4. `reviewer` gate técnico
|
||||
5. `security` gate seguridad
|
||||
6. `qa` gate funcional
|
||||
7. `leader` cierra si todo está aprobado
|
||||
Después de abrir o recargar Pi:
|
||||
|
||||
Reglas clave:
|
||||
- una feature a la vez
|
||||
- evidencia en disco (`work/artifacts/<feature>/...`)
|
||||
- nadie marca `done` si falta un gate
|
||||
```text
|
||||
/orquestra-status
|
||||
```
|
||||
|
||||
---
|
||||
## 5) Flujo secuencial
|
||||
|
||||
## 8) Cierre de feature
|
||||
1. `leader` selecciona una feature pending.
|
||||
2. `architect` diseña si hace falta.
|
||||
3. `implementer` implementa y escribe `implementer.md`.
|
||||
4. `reviewer` escribe `reviewer.json`.
|
||||
5. `security` escribe `security.json`.
|
||||
6. `qa` escribe `qa.json`.
|
||||
7. `documenter` escribe `documenter.md` solo si cambiaron docs/API/contratos/comportamiento user-facing.
|
||||
8. `leader` cierra con `leader-close.json` y `work/history.md`.
|
||||
|
||||
Antes de pasar a `done`:
|
||||
Un stage empieza solo cuando el anterior terminó con artefacto en disco; `document` es opcional/condicional y no bloquea el cierre por defecto.
|
||||
|
||||
- `verify.sh` en verde
|
||||
- review aprobado
|
||||
- security aprobado
|
||||
- qa aprobado
|
||||
- resumen en `work/history.md`
|
||||
## 6) Dónde va el código de producto
|
||||
|
||||
---
|
||||
El código del producto vive en `project/`. No escribas archivos de producto/código en la raíz del repo.
|
||||
|
||||
## 9) Manejo de pérdida de contexto (memoria)
|
||||
En Pi, las herramientas `write` y `edit` solo pueden modificar `project/` o `tests/` cuando `work/runtime-status.json` tiene una feature activa con `stage=build`, `agent=implementer` y `state=running`. Prepará el stage con:
|
||||
|
||||
Si una sesión se corta:
|
||||
```bash
|
||||
python3 scripts/agent_status.py set --feature-id F-001 --stage build --agent implementer --state running --action "Implementing"
|
||||
```
|
||||
|
||||
1. leer `work/current.md`
|
||||
2. revisar `backlog/features.json`
|
||||
3. abrir artefactos de la feature activa
|
||||
4. ejecutar `./scripts/verify.sh`
|
||||
5. continuar desde “Próximo paso”
|
||||
## 7) Estado visible
|
||||
|
||||
---
|
||||
```bash
|
||||
python3 scripts/agent_status.py show
|
||||
python3 scripts/agent_status.py set --feature-id F-001 --stage build --agent implementer --state running --action "Implementing"
|
||||
python3 scripts/agent_status.py reset
|
||||
```
|
||||
|
||||
## 10) Checklist rápido de arranque
|
||||
## Regla corta
|
||||
|
||||
- [ ] Repo creado
|
||||
- [ ] Framework copiado
|
||||
- [ ] Specs escritas
|
||||
- [ ] Backlog definido
|
||||
- [ ] Matriz de agentes configurada
|
||||
- [ ] Workflow de stages configurado
|
||||
- [ ] Verificación inicial OK
|
||||
- [ ] Primera feature en `pending`
|
||||
|
||||
---
|
||||
|
||||
## Comando mental (resumen)
|
||||
|
||||
**Crear repo → copiar framework → definir spec/backlog → verificar → ejecutar pipeline de 6 agentes con gates obligatorios.**
|
||||
Pi instalado → Orquestra instalado sin pisar progreso → producto dentro de `project/` → `verify.sh` verde → `./scripts/pi_orquestra.sh` desde raíz → stages secuenciales con evidencia en disco.
|
||||
|
||||
233
README.md
233
README.md
@@ -1,170 +1,161 @@
|
||||
# ARNES Framework (agnóstico) — Diseño v0.1
|
||||
# Orquestra — harness secuencial para Pi
|
||||
|
||||
Framework para construir aplicaciones con agentes autónomos, con control estricto de calidad, seguridad y trazabilidad.
|
||||
Compatible por diseño con **pi.dev** y **opencode** mediante adaptadores.
|
||||
Orquestra es un harness in-house para instalar en cualquier repo de proyecto y ejecutarlo desde Pi con control de estado, evidencias y gates.
|
||||
|
||||
---
|
||||
No instala subagentes. El flujo es secuencial: termina un rol/stage, se escribe su artefacto, recién ahí empieza el siguiente.
|
||||
|
||||
## Requisitos
|
||||
|
||||
- `pi` instalado y disponible en `PATH` antes de ejecutar Orquestra.
|
||||
- `python3` disponible para scripts del harness.
|
||||
- Ejecutar Pi desde la raíz del proyecto.
|
||||
- Arrancar con `./scripts/pi_orquestra.sh` para usar `pi --no-extensions` y cargar solo extensiones Orquestra.
|
||||
- Extensiones Pi project-local declaradas: `orquestra-status` y `orquestra-web-fetch.ts`.
|
||||
|
||||
> Nota honesta: abrir `pi` directo puede cargar extensiones globales. Para Pi limpio, usá `./scripts/pi_orquestra.sh`.
|
||||
|
||||
## Objetivo
|
||||
|
||||
Permitir que agentes implementen features de forma autónoma **sin perder control**:
|
||||
Permitir trabajo asistido por agentes sin perder control:
|
||||
- una feature a la vez
|
||||
- evidencia en disco (no en chat)
|
||||
- estado persistente en disco
|
||||
- evidencia auditable, nunca solo chat
|
||||
- separación de roles
|
||||
- gates obligatorios de revisión, seguridad y QA
|
||||
- cierre solo con validación completa
|
||||
- documentación opcional cuando cambian docs/API/contratos/comportamiento user-facing
|
||||
- código de producto dentro de `project/` (nunca archivos de código en la raíz)
|
||||
- cierre solo con `./scripts/verify.sh` en verde
|
||||
|
||||
---
|
||||
## Roles secuenciales
|
||||
|
||||
## Principios
|
||||
1. `leader` — selecciona feature, orquesta, cierra.
|
||||
2. `architect` — diseño/contratos cuando haga falta.
|
||||
3. `implementer` — cambia código y tests, no aprueba.
|
||||
4. `reviewer` — gate técnico.
|
||||
5. `security` — gate de seguridad.
|
||||
6. `qa` — gate funcional/aceptación.
|
||||
7. `documenter` — documentación opcional cuando aplique.
|
||||
|
||||
1. **Vendor-neutral**: núcleo independiente de herramienta.
|
||||
2. **Estado persistente**: todo vive en archivos versionables.
|
||||
3. **No confianza ciega**: “funciona” debe demostrarse con evidencia ejecutable.
|
||||
4. **Separación de roles**: quien implementa no aprueba.
|
||||
5. **Anti-trampa por diseño**: no se puede marcar `done` saltando gates.
|
||||
Los modelos por rol se definen en `harness/model-routing.yml`; el cambio de modelo ocurre antes de cada stage, nunca en paralelo.
|
||||
|
||||
---
|
||||
## Pipeline
|
||||
|
||||
## Matriz de agentes (6)
|
||||
1. `intake` → `leader`
|
||||
2. `design` → `architect` opcional
|
||||
3. `build` → `implementer`
|
||||
4. `review_gate` → `reviewer`
|
||||
5. `security_gate` → `security`
|
||||
6. `qa_gate` → `qa`
|
||||
7. `document` → `documenter` opcional/condicional
|
||||
8. `close` → `leader`
|
||||
|
||||
1. **leader**
|
||||
- Orquesta etapas y handoffs.
|
||||
- No implementa código de producto.
|
||||
No hay `done` si falta cualquier gate obligatorio; `documenter.md` no es requisito de cierre salvo que el cambio necesite documentación.
|
||||
|
||||
2. **architect**
|
||||
- Define/ajusta diseño técnico y contratos.
|
||||
- Puede editar documentación y diseño.
|
||||
## Evidencia obligatoria
|
||||
|
||||
3. **implementer**
|
||||
- Implementa una sola feature + tests.
|
||||
- No puede aprobar ni cerrar.
|
||||
|
||||
4. **reviewer**
|
||||
- Revisión técnica vs arquitectura/convenios.
|
||||
- No edita código, solo aprueba/rechaza.
|
||||
|
||||
5. **security**
|
||||
- Gate de seguridad: secretos, dependencias, SAST básico, hardening checks.
|
||||
- No edita código.
|
||||
|
||||
6. **qa**
|
||||
- Gate de calidad funcional: aceptación, integración/E2E, regresión.
|
||||
- No edita código.
|
||||
|
||||
---
|
||||
|
||||
## Flujo de trabajo (pipeline)
|
||||
|
||||
1. `intake` (leader)
|
||||
2. `design` (architect)
|
||||
3. `build` (implementer)
|
||||
4. `review_gate` (reviewer) ✅
|
||||
5. `security_gate` (security) ✅
|
||||
6. `qa_gate` (qa) ✅
|
||||
7. `close` (leader)
|
||||
|
||||
**Regla:** no hay `done` si cualquier gate falla.
|
||||
|
||||
---
|
||||
|
||||
## Anti-trampa (control estricto)
|
||||
|
||||
### Reglas de autorización
|
||||
- Solo `leader` puede mover `in_progress -> done`.
|
||||
- `implementer` no puede editar archivos de estado final de cierre.
|
||||
- `reviewer/security/qa` no pueden editar código de producto.
|
||||
|
||||
### Evidencia obligatoria por etapa
|
||||
Cada agente escribe artefactos en disco:
|
||||
Cada stage escribe en disco:
|
||||
- `work/artifacts/<feature>/implementer.md`
|
||||
- `work/artifacts/<feature>/reviewer.md`
|
||||
- `work/artifacts/<feature>/security.md`
|
||||
- `work/artifacts/<feature>/qa.md`
|
||||
- `work/artifacts/<feature>/reviewer.json`
|
||||
- `work/artifacts/<feature>/security.json`
|
||||
- `work/artifacts/<feature>/qa.json`
|
||||
- `work/artifacts/<feature>/documenter.md` (opcional/condicional)
|
||||
- `work/artifacts/<feature>/leader-close.json`
|
||||
|
||||
Respuesta de agente siempre: `done -> <ruta>` o `blocked -> <ruta>`.
|
||||
Respuesta estándar por stage:
|
||||
- `done -> <ruta>`
|
||||
- `blocked -> <ruta>`
|
||||
|
||||
### Gates ejecutados fuera del agente
|
||||
- Verificación disparada por harness/scripts (no por “declaración” del agente).
|
||||
- Si gate falla, estado vuelve a `blocked` o permanece `in_progress`.
|
||||
|
||||
### Trazabilidad y auditoría
|
||||
- `work/history.md` append-only.
|
||||
- Checklist de cierre firmado por etapa (aprobado/rechazado + evidencia).
|
||||
- Cualquier missing evidence = cierre denegado.
|
||||
|
||||
---
|
||||
|
||||
## Estructura propuesta
|
||||
## Estructura mínima
|
||||
|
||||
```text
|
||||
.
|
||||
├── AGENTS.md
|
||||
├── README.md
|
||||
├── harness/
|
||||
│ ├── agents.matrix.yml
|
||||
│ ├── workflow.stages.yml
|
||||
│ ├── model-routing.yml
|
||||
│ ├── policies/
|
||||
│ │ ├── security.md
|
||||
│ │ ├── quality.md
|
||||
│ │ └── governance.md
|
||||
│ └── contracts/
|
||||
│ ├── handoff.md
|
||||
│ └── evidence.schema.json
|
||||
├── platforms/pi/
|
||||
│ └── extensions/
|
||||
│ ├── orquestra-status/
|
||||
│ └── orquestra-web-fetch.ts
|
||||
├── spec/
|
||||
│ ├── product.md
|
||||
│ ├── tech.md
|
||||
│ └── acceptance.md
|
||||
├── backlog/
|
||||
│ └── features.json
|
||||
├── project/
|
||||
├── backlog/features.json
|
||||
├── work/
|
||||
│ ├── current.md
|
||||
│ ├── history.md
|
||||
│ ├── runtime-status.json
|
||||
│ └── artifacts/
|
||||
└── scripts/
|
||||
└── verify.sh
|
||||
├── verify.sh
|
||||
├── agent_status.py
|
||||
├── new_ticket.py
|
||||
└── pi_orquestra.sh
|
||||
```
|
||||
|
||||
---
|
||||
## Instalación y actualización segura
|
||||
|
||||
## Manejo de pérdidas de memoria (context loss)
|
||||
Desde el repo fuente de Orquestra:
|
||||
|
||||
Sí: el framework está diseñado para eso.
|
||||
```bash
|
||||
./scripts/install.sh /path/to/project-repo
|
||||
```
|
||||
|
||||
Mecanismos:
|
||||
1. **Estado en disco** (`work/current.md`, `backlog/features.json`).
|
||||
2. **Bitácora append-only** (`work/history.md`).
|
||||
3. **Handoffs explícitos** por archivo, no por chat.
|
||||
4. **Protocolo de reentrada** al iniciar sesión:
|
||||
- leer `work/current.md`
|
||||
- leer feature activa
|
||||
- ejecutar verificación base
|
||||
- continuar desde “próximo paso”
|
||||
Para actualizar, volvé a ejecutar el mismo comando sobre el repo destino.
|
||||
|
||||
Si se pierde contexto del modelo, el sistema se puede reconstruir desde archivos.
|
||||
Regla base:
|
||||
- crear si falta
|
||||
- conservar o mergear si existe
|
||||
- nunca pisar `work/`, `backlog/`, `spec/` ni artefactos ya producidos
|
||||
|
||||
---
|
||||
Archivos del harness actualizables: `AGENTS.md`, `README.md`, `HOWTO.md`, `CHECKPOINTS.md`, `harness/`, `scripts/`, `platforms/pi/`.
|
||||
Datos del proyecto: `project/`, `work/`, `backlog/`, `spec/`.
|
||||
|
||||
## Adaptadores de plataforma
|
||||
El instalador crea `project/` si falta y no pisa su contenido. Los archivos de producto/código (`*.py`, `*.js`, `*.ts`, `*.go`, `*.rs`, `*.java`, `*.php`, `*.rb`) son inválidos en la raíz del repo; `./scripts/verify.sh` falla si los encuentra. Durante una sesión Pi, escribir en `project/` o `tests/` requiere una feature activa con `stage=build`, `agent=implementer` y `state=running` en `work/runtime-status.json`.
|
||||
|
||||
- `platforms/pi/`: prompts, hooks, permisos, comandos compatibles con pi.dev.
|
||||
- `platforms/opencode/`: prompts, hooks, permisos, comandos compatibles con opencode.
|
||||
## Pi
|
||||
|
||||
El núcleo no cambia; solo el adaptador.
|
||||
Las extensiones Orquestra esperadas son:
|
||||
|
||||
---
|
||||
```text
|
||||
.pi/extensions/orquestra-status/index.ts
|
||||
.pi/extensions/orquestra-web-fetch.ts
|
||||
```
|
||||
|
||||
## Criterios de éxito del framework
|
||||
Comando manual:
|
||||
|
||||
- No se puede cerrar una feature sin 3 gates en verde (review/security/qa).
|
||||
- Evidencia completa y auditable por feature.
|
||||
- Reentrada robusta tras reinicio o pérdida de contexto.
|
||||
- Portabilidad entre pi.dev y opencode sin rediseñar el núcleo.
|
||||
```bash
|
||||
./scripts/pi_orquestra.sh
|
||||
# dentro de Pi:
|
||||
/orquestra-status
|
||||
```
|
||||
|
||||
---
|
||||
Fuente de verdad:
|
||||
|
||||
## Próximos pasos sugeridos
|
||||
```bash
|
||||
python3 scripts/agent_status.py show
|
||||
python3 scripts/agent_status.py set ...
|
||||
python3 scripts/agent_status.py reset
|
||||
```
|
||||
|
||||
1. Definir `agents.matrix.yml` completo (permisos exactos por rutas).
|
||||
2. Definir `workflow.stages.yml` con transiciones válidas.
|
||||
3. Diseñar `features.json` con estados y criterios de aceptación.
|
||||
4. Especificar `scripts/verify.sh` (lint/test/security/qa gates).
|
||||
5. Crear adaptadores `platforms/pi` y `platforms/opencode`.
|
||||
## Verificación
|
||||
|
||||
```bash
|
||||
./scripts/verify.sh
|
||||
```
|
||||
|
||||
Comprueba:
|
||||
- estructura mínima
|
||||
- `pi` instalado
|
||||
- launcher limpio `scripts/pi_orquestra.sh`
|
||||
- extensiones requeridas: `orquestra-status` y `orquestra-web-fetch`
|
||||
- ausencia de subagentes project-local
|
||||
- precondiciones de stage en `agent_status.py`
|
||||
- extensiones project-local no declaradas
|
||||
- backlog y gates
|
||||
- `work/runtime-status.json`
|
||||
- `project/` existente y sin archivos de producto/código en la raíz
|
||||
- suite del proyecto si existe
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"project": "nuevo-proyecto",
|
||||
"description": "Backlog inicial del proyecto",
|
||||
"project": "orquestra-harness",
|
||||
"description": "Backlog del harness Orquestra instalable",
|
||||
"rules": {
|
||||
"one_feature_at_a_time": true,
|
||||
"require_review_gate": true,
|
||||
@@ -11,12 +11,14 @@
|
||||
"features": [
|
||||
{
|
||||
"id": "F-001",
|
||||
"title": "Definir estructura inicial",
|
||||
"description": "Bootstrap del proyecto con estructura base.",
|
||||
"title": "Definir contrato de instalación y actualización segura",
|
||||
"description": "Establecer el contrato mínimo para instalar Orquestra en cualquier proyecto compatible y actualizarlo sin pisar progreso existente.",
|
||||
"acceptance": [
|
||||
"Estructura base creada",
|
||||
"Tests o checks iniciales definidos",
|
||||
"Artefactos de gate configurados"
|
||||
"Identidad del harness definida",
|
||||
"Estructura mínima requerida para ejecutar Orquestra documentada",
|
||||
"Frontera entre archivos del harness y datos del proyecto documentada",
|
||||
"Regla de actualización segura definida: crear si falta, conservar o mergear si existe",
|
||||
"Artefactos de gate alineados con el esquema JSON"
|
||||
],
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
276
docs/context-handoff.md
Normal file
276
docs/context-handoff.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Context Handoff in Orquestra
|
||||
|
||||
Orquestra keeps agents focused by making context explicit. Agents do not pass a whole chat history to the next agent. They pass durable artifacts on disk, and each new stage reads only the files declared for that stage.
|
||||
|
||||
## Quick path
|
||||
|
||||
1. The current agent writes its result to `work/artifacts/<feature_id>/`.
|
||||
2. From the active Pi session, `/orquestra-stage <stage> [feature_id]` calls `scripts/run_stage.py`.
|
||||
3. `run_stage.py` starts a fresh Pi process.
|
||||
4. The new agent receives only the `input` paths declared in `harness/workflow.stages.yml`.
|
||||
5. The new agent writes its own output artifact.
|
||||
6. `scripts/agent_status.py` and `scripts/verify.sh` enforce stage order and gates.
|
||||
7. When the `close` stage completes, `scripts/close_feature.py <feature_id>` runs automatically to validate gates, update the backlog, commit and push if a remote repo is configured.
|
||||
|
||||
## Mental model
|
||||
|
||||
```text
|
||||
Agent A
|
||||
reads its stage inputs
|
||||
writes one durable artifact
|
||||
│
|
||||
▼
|
||||
work/artifacts/F-001/agent-a-output
|
||||
│
|
||||
▼
|
||||
run_stage.py starts a fresh Pi process
|
||||
│
|
||||
▼
|
||||
Agent B
|
||||
reads only declared inputs, including Agent A's artifact if relevant
|
||||
writes its own artifact
|
||||
```
|
||||
|
||||
The agents “talk” through files, not through memory-heavy chat context.
|
||||
|
||||
## Why fresh processes matter
|
||||
|
||||
A single long Pi session naturally accumulates chat, tool output, false starts, and old assumptions. That causes two problems:
|
||||
|
||||
| Problem | Orquestra answer |
|
||||
|---|---|
|
||||
| The next agent gets distracted by old context | Start the next stage with `--no-session --no-context-files` |
|
||||
| Token usage grows until compaction | Each stage starts with a small prompt and declared files |
|
||||
| Claims become hard to verify | Only artifacts on disk count as handoff evidence |
|
||||
| Agents can skip gates in chat | `agent_status.py` validates stage owner and prerequisites |
|
||||
|
||||
## What a stage receives
|
||||
|
||||
Each stage receives three kinds of context:
|
||||
|
||||
1. **Harness rules** — always read first.
|
||||
- `AGENTS.md`
|
||||
- `harness/agents.matrix.yml`
|
||||
- `harness/workflow.stages.yml`
|
||||
- `harness/contracts/handoff.md`
|
||||
|
||||
2. **Declared stage inputs** — from `harness/workflow.stages.yml`.
|
||||
|
||||
3. **Selected Engram memory** — only when the agent performs a narrow search for a concrete need.
|
||||
|
||||
The full previous chat is not valid input.
|
||||
|
||||
## Example: design to build
|
||||
|
||||
`architect` receives product/spec context:
|
||||
|
||||
```yaml
|
||||
- name: design
|
||||
owner: architect
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/product.md
|
||||
- spec/tech.md
|
||||
- spec/acceptance.md
|
||||
output:
|
||||
- work/artifacts/<feature_id>/architect.md
|
||||
```
|
||||
|
||||
It writes:
|
||||
|
||||
```text
|
||||
work/artifacts/F-001/architect.md
|
||||
```
|
||||
|
||||
Then `implementer` starts fresh from the active Pi session:
|
||||
|
||||
```text
|
||||
/orquestra-stage build F-001
|
||||
```
|
||||
|
||||
Equivalent shell form:
|
||||
|
||||
```bash
|
||||
python3 scripts/run_stage.py build --feature-id F-001
|
||||
```
|
||||
|
||||
The generated prompt gives it only:
|
||||
|
||||
```yaml
|
||||
- name: build
|
||||
owner: implementer
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/product.md
|
||||
- spec/tech.md
|
||||
- spec/acceptance.md
|
||||
- work/artifacts/<feature_id>/architect.md
|
||||
output:
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
```
|
||||
|
||||
So the implementer knows the design result, but not the architect's full chat.
|
||||
|
||||
## Example: build to review
|
||||
|
||||
The implementer writes evidence:
|
||||
|
||||
```text
|
||||
work/artifacts/F-001/implementer.md
|
||||
```
|
||||
|
||||
A minimal implementer artifact should answer:
|
||||
|
||||
```markdown
|
||||
# Implementer Evidence
|
||||
|
||||
## Changed files
|
||||
- project/app.py
|
||||
- tests/test_app.py
|
||||
|
||||
## What changed
|
||||
- Added ticket creation validation.
|
||||
|
||||
## Checks
|
||||
- `python3 -m unittest discover -s tests -v` passed.
|
||||
|
||||
## Notes for reviewer
|
||||
- Main behavior lives in `project/app.py:create_ticket`.
|
||||
```
|
||||
|
||||
Then reviewer starts fresh:
|
||||
|
||||
```text
|
||||
/orquestra-stage review_gate F-001
|
||||
```
|
||||
|
||||
Reviewer input is only:
|
||||
|
||||
```yaml
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/acceptance.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
```
|
||||
|
||||
Reviewer does not need the architect's whole reasoning unless the workflow explicitly declares it. If reviewer needs more, it blocks and asks for a specific artifact/path.
|
||||
|
||||
## Example: security and QA reuse only relevant context
|
||||
|
||||
Security does not need all acceptance prose plus every design note. It needs the implementation evidence and review result:
|
||||
|
||||
```yaml
|
||||
- name: security_gate
|
||||
owner: security
|
||||
input:
|
||||
- work/current.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
- work/artifacts/<feature_id>/reviewer.json
|
||||
```
|
||||
|
||||
QA needs acceptance criteria and approved gate context:
|
||||
|
||||
```yaml
|
||||
- name: qa_gate
|
||||
owner: qa
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/acceptance.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
- work/artifacts/<feature_id>/reviewer.json
|
||||
- work/artifacts/<feature_id>/security.json
|
||||
```
|
||||
|
||||
That is the core rule: give each agent the smallest context that lets it do its job.
|
||||
|
||||
## Anti-cheating controls
|
||||
|
||||
```text
|
||||
run_stage.py
|
||||
├─ starts fresh Pi process
|
||||
├─ disables previous session/context files
|
||||
└─ injects only declared input/output paths
|
||||
|
||||
agent_status.py
|
||||
├─ validates stage owner
|
||||
├─ requires feature_id for real stages
|
||||
├─ rejects gated stages when prerequisite artifacts are missing
|
||||
└─ keeps visible runtime status
|
||||
|
||||
orquestra-status extension
|
||||
├─ blocks writes outside allowed directories: project/, tests/, work/, backlog/, spec/, harness/, scripts/, platforms/, docs/
|
||||
├─ blocks product/test writes outside implementer build stage
|
||||
└─ blocks product code in repository root
|
||||
|
||||
verify.sh
|
||||
├─ validates harness structure
|
||||
├─ validates backlog/runtime JSON
|
||||
├─ validates Engram availability
|
||||
└─ rejects done features without approved gate artifacts
|
||||
```
|
||||
|
||||
Chat-only claims do not pass any gate. The agent must write evidence to disk.
|
||||
|
||||
## What Engram is for
|
||||
|
||||
Engram is durable memory for reusable facts, not a replacement for stage artifacts.
|
||||
|
||||
Each stage must save to Engram only when it produced durable knowledge.
|
||||
|
||||
Use Engram for:
|
||||
|
||||
- stable project decisions,
|
||||
- bug fixes,
|
||||
- non-obvious discoveries,
|
||||
- reusable conventions or patterns,
|
||||
- configuration changes,
|
||||
- repeated gotchas,
|
||||
- environment facts,
|
||||
- lessons learned across features.
|
||||
|
||||
Do not use Engram for:
|
||||
|
||||
- replacing `implementer.md`,
|
||||
- hiding gate evidence,
|
||||
- routine progress,
|
||||
- command output dumps,
|
||||
- artifact summaries,
|
||||
- passing huge summaries to every stage,
|
||||
- bypassing declared workflow inputs.
|
||||
|
||||
A good Engram lookup is narrow:
|
||||
|
||||
```text
|
||||
Search: "orquestra project root product files rule"
|
||||
Use result only if it affects this stage.
|
||||
```
|
||||
|
||||
A bad lookup is broad:
|
||||
|
||||
```text
|
||||
Search: "everything about this project"
|
||||
```
|
||||
|
||||
## Updating an installed project safely
|
||||
|
||||
From the Orquestra source checkout:
|
||||
|
||||
```bash
|
||||
/path/to/orquestra/scripts/install.sh /path/to/project
|
||||
cd /path/to/project
|
||||
./scripts/verify.sh
|
||||
```
|
||||
|
||||
The installer updates harness-owned files and preserves project-owned progress files when they already exist:
|
||||
|
||||
```text
|
||||
backlog/features.json
|
||||
spec/*
|
||||
work/current.md
|
||||
work/history.md
|
||||
work/runtime-status.json
|
||||
project/*
|
||||
work/artifacts/*
|
||||
```
|
||||
|
||||
That means an existing project can adopt fresh-stage handoff without losing its current feature state.
|
||||
@@ -56,6 +56,14 @@ roles:
|
||||
- regression_checks
|
||||
- emit_qa_verdict
|
||||
|
||||
documenter:
|
||||
can_edit: ["work/artifacts/", "docs/", "README.md", "HOWTO.md", "platforms/pi/README.md", "harness/contracts/"]
|
||||
cannot_edit: ["src/", "tests/", "backlog/features.json:status"]
|
||||
responsibilities:
|
||||
- document_changes_when_needed
|
||||
- update_docs_api_contracts_user_facing_notes
|
||||
- produce_documenter_evidence
|
||||
|
||||
anti_cheat:
|
||||
- "Implementer cannot promote feature to done"
|
||||
- "Done requires reviewer/security/qa approved artifacts"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://arnes.local/evidence.schema.json",
|
||||
"$id": "https://orquestra.local/evidence.schema.json",
|
||||
"title": "Gate Evidence",
|
||||
"type": "object",
|
||||
"required": ["feature_id", "agent", "verdict", "summary", "evidence", "timestamp"],
|
||||
|
||||
@@ -5,11 +5,20 @@ Los agentes **no** devuelven código en chat. Deben escribir a disco y responder
|
||||
- `done -> <ruta>`
|
||||
- `blocked -> <ruta>`
|
||||
|
||||
## Contexto mínimo por stage
|
||||
Cada stage recibe solo:
|
||||
1. Los archivos listados en `harness/workflow.stages.yml` como `input`.
|
||||
2. El artefacto del stage anterior cuando el workflow lo declare.
|
||||
3. La memoria Engram que el leader seleccione explícitamente para ese stage.
|
||||
|
||||
No se considera válido pasar todo el chat anterior como contexto operativo. Si un stage necesita más información, debe pedir una ruta o artefacto concreto y bloquear hasta tenerlo.
|
||||
|
||||
## Rutas estándar por feature
|
||||
- `work/artifacts/<feature_id>/implementer.md`
|
||||
- `work/artifacts/<feature_id>/reviewer.json`
|
||||
- `work/artifacts/<feature_id>/security.json`
|
||||
- `work/artifacts/<feature_id>/qa.json`
|
||||
- `work/artifacts/<feature_id>/documenter.md` (opcional; requerido solo si cambiaron docs/API/contratos/comportamiento user-facing)
|
||||
- `work/artifacts/<feature_id>/leader-close.json`
|
||||
|
||||
## Campos mínimos de artefactos JSON de gate
|
||||
|
||||
23
harness/model-routing.yml
Normal file
23
harness/model-routing.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: 1
|
||||
|
||||
# Sequential role model hints for Pi. The parent session changes model before
|
||||
# each stage when a different model is desired. No parallel agents/subagents.
|
||||
roles:
|
||||
leader:
|
||||
model: default
|
||||
reasoning: medium
|
||||
architect:
|
||||
model: strong
|
||||
reasoning: medium
|
||||
implementer:
|
||||
model: default
|
||||
reasoning: medium
|
||||
reviewer:
|
||||
model: strong
|
||||
reasoning: low
|
||||
security:
|
||||
model: strong
|
||||
reasoning: medium
|
||||
qa:
|
||||
model: default
|
||||
reasoning: low
|
||||
@@ -15,38 +15,85 @@ stages:
|
||||
- name: design
|
||||
owner: architect
|
||||
optional: true
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/product.md
|
||||
- spec/tech.md
|
||||
- spec/acceptance.md
|
||||
output:
|
||||
- work/artifacts/<feature_id>/architect.md
|
||||
|
||||
- name: build
|
||||
owner: implementer
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/product.md
|
||||
- spec/tech.md
|
||||
- spec/acceptance.md
|
||||
- work/artifacts/<feature_id>/architect.md
|
||||
output:
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
|
||||
- name: review_gate
|
||||
owner: reviewer
|
||||
required: true
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/acceptance.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
output:
|
||||
- work/artifacts/<feature_id>/reviewer.json
|
||||
|
||||
- name: security_gate
|
||||
owner: security
|
||||
required: true
|
||||
input:
|
||||
- work/current.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
- work/artifacts/<feature_id>/reviewer.json
|
||||
output:
|
||||
- work/artifacts/<feature_id>/security.json
|
||||
|
||||
- name: qa_gate
|
||||
owner: qa
|
||||
required: true
|
||||
input:
|
||||
- work/current.md
|
||||
- spec/acceptance.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
- work/artifacts/<feature_id>/reviewer.json
|
||||
- work/artifacts/<feature_id>/security.json
|
||||
output:
|
||||
- work/artifacts/<feature_id>/qa.json
|
||||
|
||||
- name: document
|
||||
owner: documenter
|
||||
optional: true
|
||||
when: docs/API/contracts/user-facing behavior changed
|
||||
input:
|
||||
- work/current.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
- work/artifacts/<feature_id>/reviewer.json
|
||||
- work/artifacts/<feature_id>/security.json
|
||||
- work/artifacts/<feature_id>/qa.json
|
||||
output:
|
||||
- work/artifacts/<feature_id>/documenter.md
|
||||
|
||||
- name: close
|
||||
owner: leader
|
||||
required: true
|
||||
input:
|
||||
- work/current.md
|
||||
- work/artifacts/<feature_id>/implementer.md
|
||||
- work/artifacts/<feature_id>/reviewer.json
|
||||
- work/artifacts/<feature_id>/security.json
|
||||
- work/artifacts/<feature_id>/qa.json
|
||||
- work/artifacts/<feature_id>/documenter.md
|
||||
output:
|
||||
- work/artifacts/<feature_id>/leader-close.json
|
||||
- work/history.md
|
||||
post_actions:
|
||||
- scripts/close_feature.py <feature_id>
|
||||
|
||||
close_requirements:
|
||||
- reviewer.json.verdict == "APPROVED"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Adaptador opencode
|
||||
|
||||
Usa este adaptador para ejecutar ARNES en opencode.
|
||||
|
||||
## Recomendado
|
||||
- Definir perfiles/roles con la matriz `harness/agents.matrix.yml`.
|
||||
- Separar permisos por rutas para evitar bypass de gates.
|
||||
- Forzar handoff por archivos (`work/artifacts/<feature_id>/`).
|
||||
- Ejecutar `./scripts/verify.sh` en pre-close.
|
||||
|
||||
## Respuesta estándar de subagentes
|
||||
- `done -> <ruta>`
|
||||
- `blocked -> <ruta>`
|
||||
@@ -1,4 +0,0 @@
|
||||
Rol: leader.
|
||||
No escribas código de producto.
|
||||
Coordina intake -> design -> build -> review_gate -> security_gate -> qa_gate -> close.
|
||||
Si falta evidencia en disco o falla verify.sh, no cerrar feature.
|
||||
@@ -1,13 +1,45 @@
|
||||
# Adaptador pi.dev
|
||||
# Adaptador Pi
|
||||
|
||||
Usa este adaptador para ejecutar ARNES en pi.dev.
|
||||
Orquestra se ejecuta desde Pi como **un solo parent session secuencial**. No instala subagentes.
|
||||
|
||||
## Recomendado
|
||||
- Cargar `AGENTS.md` como entrada principal.
|
||||
- Enforzar roles según `harness/agents.matrix.yml`.
|
||||
- Exigir que subagentes escriban artefactos en `work/artifacts/<feature_id>/`.
|
||||
- Ejecutar `./scripts/verify.sh` al inicio y antes de cierre.
|
||||
## Requisitos obligatorios
|
||||
- `pi` debe existir en `PATH` antes de instalar Orquestra.
|
||||
- `gentle-engram` debe estar instalado: Orquestra usa Engram como memoria durable externa; no escribe memoria propia.
|
||||
- El proyecto instalado debe abrirse desde su raíz.
|
||||
- Arrancar con `./scripts/pi_orquestra.sh`, que ejecuta `pi --no-extensions`, carga Engram explícitamente y carga solo extensiones Orquestra.
|
||||
- Extensiones project-local declaradas: `.pi/extensions/orquestra-status/` y `.pi/extensions/orquestra-web-fetch.ts`.
|
||||
- El código de producto vive en `project/`; archivos de código en la raíz son inválidos.
|
||||
|
||||
## Respuesta estándar de subagentes
|
||||
## Instalación esperada
|
||||
Cuando Orquestra se instala en un repo de proyecto, el instalador debe copiar:
|
||||
- `platforms/pi/extensions/orquestra-status/` -> `.pi/extensions/orquestra-status/`
|
||||
- `platforms/pi/extensions/orquestra-web-fetch.ts` -> `.pi/extensions/orquestra-web-fetch.ts`
|
||||
|
||||
No debe crear `.pi/subagents/` ni `.pi/subagents.json`.
|
||||
|
||||
## Técnica de memoria
|
||||
- Engram es la única memoria persistente del harness.
|
||||
- `verify.sh` falla si `~/.pi/agent/npm/node_modules/gentle-engram/index.ts` no existe.
|
||||
- `pi_orquestra.sh` carga Engram con `-e` aunque Pi arranque con `--no-extensions`; así se evita cargar extensiones globales no declaradas sin perder memoria.
|
||||
- Cada rol trabaja desde los `input` declarados en `harness/workflow.stages.yml`; el chat completo no es un handoff válido.
|
||||
- Para aislamiento real desde la sesión Pi, ejecutar cada stage con `/orquestra-stage <stage> [feature_id]`; internamente usa `run_stage.py`, `pi --no-session --no-context-files` y carga solo Engram + extensiones Orquestra.
|
||||
- Detalle completo del handoff: `docs/context-handoff.md`.
|
||||
|
||||
## Flujo secuencial
|
||||
1. Ejecutar `./scripts/verify.sh`.
|
||||
2. Abrir Pi limpio desde la raíz con `./scripts/pi_orquestra.sh`.
|
||||
3. Confirmar el widget con `/orquestra-status`.
|
||||
4. Ejecutar cada stage como proceso fresco desde Pi: `/orquestra-stage <stage> [feature_id]`.
|
||||
5. `run_stage.py` genera un prompt mínimo con las rutas `input`/`output` del stage y no hereda la sesión anterior.
|
||||
6. `agent_status.py` rechaza saltos de stage sin artefactos previos obligatorios.
|
||||
7. Durante `build`, escribir producto en `project/` y tests en `tests/`; requiere `feature_id`, `stage=build`, `agent=implementer` y `state=running` en `work/runtime-status.json`.
|
||||
8. Al terminar cada stage, escribir el artefacto esperado en `work/artifacts/<feature_id>/`.
|
||||
9. Ejecutar `document`/`documenter.md` solo si cambiaron docs/API/contratos/comportamiento user-facing; no es gate obligatorio de cierre.
|
||||
10. Recién después empieza el siguiente rol/stage.
|
||||
|
||||
## Modelos por rol
|
||||
Si querés modelos distintos por etapa, se eligen secuencialmente antes de cada stage según `harness/model-routing.yml`. No hay ejecución paralela.
|
||||
|
||||
## Respuesta estándar por etapa
|
||||
- `done -> <ruta>`
|
||||
- `blocked -> <ruta>`
|
||||
|
||||
42
platforms/pi/extensions/orquestra-status/README.md
Normal file
42
platforms/pi/extensions/orquestra-status/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Orquestra Status Extension
|
||||
|
||||
Pi extension project-local que renderiza `work/runtime-status.json` y aplica el guard mínimo de escritura Orquestra.
|
||||
|
||||
## Muestra
|
||||
- feature activa
|
||||
- stage actual
|
||||
- rol actual
|
||||
- acción/estado
|
||||
- siguiente rol
|
||||
- checklist de artefactos por rol
|
||||
- estado de gates
|
||||
- último evento de timeline
|
||||
|
||||
## Ruta instalada
|
||||
- `.pi/extensions/orquestra-status/index.ts`
|
||||
|
||||
Se lanza junto con `.pi/extensions/orquestra-web-fetch.ts` desde `scripts/pi_orquestra.sh`.
|
||||
|
||||
## Uso
|
||||
1. Abrí `pi` en la raíz del proyecto instalado.
|
||||
2. Si Pi ya estaba abierto, ejecutá `/reload`.
|
||||
3. El widget aparece arriba del editor y el footer muestra estado compacto.
|
||||
4. El widget refresca cada segundo.
|
||||
5. Refresco manual: `/orquestra-status`.
|
||||
|
||||
## Guard de escritura
|
||||
|
||||
La extensión intercepta herramientas Pi built-in `write` y `edit`:
|
||||
- bloquea archivos de producto/código en la raíz (`*.py`, `*.js`, `*.ts`, `*.go`, `*.rs`, `*.java`, `*.php`, `*.rb`); usá `project/`.
|
||||
- bloquea escrituras en `project/` o `tests/` salvo que `work/runtime-status.json` tenga `feature_id`, `stage=build`, `agent=implementer` y `state=running`.
|
||||
- bloquea paths que escapan la raíz del repo con `..`.
|
||||
|
||||
## Fuente de verdad
|
||||
- `work/runtime-status.json`
|
||||
|
||||
Actualizar con:
|
||||
|
||||
```bash
|
||||
python3 scripts/agent_status.py set ...
|
||||
python3 scripts/agent_status.py reset
|
||||
```
|
||||
362
platforms/pi/extensions/orquestra-status/index.ts
Normal file
362
platforms/pi/extensions/orquestra-status/index.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, watchFile, unwatchFile } from "node:fs";
|
||||
import { join, relative, resolve, sep } from "node:path";
|
||||
import type { ExtensionAPI, ExtensionContext, ToolCallEvent } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const STATUS_FILE = "work/runtime-status.json";
|
||||
const MATRIX_FILE = "harness/agents.matrix.yml";
|
||||
const ARTIFACTS_DIR = "work/artifacts";
|
||||
const WIDGET_KEY = "orquestra-runtime";
|
||||
const STATUS_KEY = "orquestra-runtime";
|
||||
const STATUS_GAP = " ";
|
||||
|
||||
const DEFAULT_EMOJIS: Record<string, string> = {
|
||||
leader: "🧭",
|
||||
architect: "🏗️",
|
||||
implementer: "🛠️",
|
||||
reviewer: "🔍",
|
||||
security: "🔒",
|
||||
qa: "🧪",
|
||||
documenter: "📝",
|
||||
};
|
||||
|
||||
const GATE_FILES: Record<string, string> = {
|
||||
reviewer: "reviewer.json",
|
||||
security: "security.json",
|
||||
qa: "qa.json",
|
||||
leader: "leader-close.json",
|
||||
};
|
||||
|
||||
const AGENT_FILES: Record<string, string> = {
|
||||
architect: "architect.md",
|
||||
implementer: "implementer.md",
|
||||
reviewer: "reviewer.json",
|
||||
security: "security.json",
|
||||
qa: "qa.json",
|
||||
documenter: "documenter.md",
|
||||
leader: "leader-close.json",
|
||||
};
|
||||
|
||||
type RuntimeStatus = {
|
||||
feature_id?: string | null;
|
||||
stage?: string;
|
||||
agent?: string;
|
||||
action?: string;
|
||||
state?: string;
|
||||
next_agent?: string;
|
||||
waiting_for?: string;
|
||||
updated_at?: string;
|
||||
timeline?: Array<{ ts?: string; agent?: string; stage?: string; state?: string; message?: string }>;
|
||||
};
|
||||
|
||||
const ROOT_PRODUCT_EXTENSIONS = new Set([".py", ".js", ".ts", ".go", ".rs", ".java", ".php", ".rb"]);
|
||||
|
||||
// Carpetas permitidas para escritura (Orquestra + proyecto)
|
||||
const ALLOWED_WRITE_DIRS = new Set([
|
||||
"project", // código del proyecto
|
||||
"tests", // tests del proyecto
|
||||
"work", // artifacts, current.md, runtime-status.json
|
||||
"backlog", // features.json
|
||||
"spec", // specs globales
|
||||
"harness", // configuración Orquestra (solo leader)
|
||||
"scripts", // scripts Orquestra (solo leader)
|
||||
"platforms", // extensiones Pi (solo leader)
|
||||
"docs", // documentación (solo leader)
|
||||
]);
|
||||
|
||||
function safeReadJson(root: string, path: string): RuntimeStatus | null {
|
||||
try {
|
||||
return JSON.parse(readFileSync(join(root, path), "utf8")) as RuntimeStatus;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadRoleEmojis(root: string): Record<string, string> {
|
||||
try {
|
||||
const text = readFileSync(join(root, MATRIX_FILE), "utf8");
|
||||
const emojis = { ...DEFAULT_EMOJIS };
|
||||
let currentRole: string | null = null;
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const roleMatch = line.match(/^ ([a-z_]+):\s*$/);
|
||||
if (roleMatch) {
|
||||
currentRole = roleMatch[1];
|
||||
continue;
|
||||
}
|
||||
const emojiMatch = line.match(/^\s{4}emoji:\s*["']?(.*?)["']?\s*$/);
|
||||
if (emojiMatch && currentRole) emojis[currentRole] = emojiMatch[1];
|
||||
}
|
||||
return emojis;
|
||||
} catch {
|
||||
return { ...DEFAULT_EMOJIS };
|
||||
}
|
||||
}
|
||||
|
||||
function gateState(root: string, featureId?: string | null): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
if (!featureId) return result;
|
||||
for (const [gate, filename] of Object.entries(GATE_FILES)) {
|
||||
try {
|
||||
if (filename.endsWith(".md")) {
|
||||
readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8");
|
||||
result[gate] = "APPROVED";
|
||||
continue;
|
||||
}
|
||||
const json = JSON.parse(readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8")) as { verdict?: string };
|
||||
result[gate] = json.verdict === "APPROVED" ? "APPROVED" : "PRESENT";
|
||||
} catch {
|
||||
result[gate] = "PENDING";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function gateSummary(root: string, featureId: string | null | undefined, emojis: Record<string, string>): string {
|
||||
const gates = gateState(root, featureId);
|
||||
if (!featureId) return "Gates: —";
|
||||
return `Gates: ${["reviewer", "security", "qa", "leader"]
|
||||
.map((gate) => `${emojis[gate] || "•"} ${gates[gate] === "APPROVED" ? "✅" : gates[gate] === "PRESENT" ? "⚠️" : "⏳"}`)
|
||||
.join(STATUS_GAP)}`;
|
||||
}
|
||||
|
||||
function artifactStatus(root: string, featureId: string | null | undefined, agent: string): string {
|
||||
if (!featureId) return "PENDING";
|
||||
const filename = AGENT_FILES[agent];
|
||||
if (!filename) return "PENDING";
|
||||
try {
|
||||
if (filename.endsWith(".md")) {
|
||||
readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8");
|
||||
return "DONE";
|
||||
}
|
||||
const json = JSON.parse(readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8")) as { verdict?: string };
|
||||
return json.verdict === "APPROVED" ? "DONE" : "PRESENT";
|
||||
} catch {
|
||||
return "PENDING";
|
||||
}
|
||||
}
|
||||
|
||||
function agentSummary(root: string, status: RuntimeStatus, emojis: Record<string, string>): string {
|
||||
const featureId = status.feature_id || null;
|
||||
const currentAgent = status.agent || "leader";
|
||||
return `Roles: ${["architect", "implementer", "reviewer", "security", "qa", "documenter", "leader"]
|
||||
.map((agent) => {
|
||||
const state = artifactStatus(root, featureId, agent);
|
||||
const mark = state === "DONE" ? "✅" : state === "PRESENT" ? "⚠️" : agent === currentAgent ? "▶️" : "⏳";
|
||||
return `${emojis[agent] || "•"} ${mark}`;
|
||||
})
|
||||
.join(STATUS_GAP)}`;
|
||||
}
|
||||
|
||||
function isInsideRel(relPath: string, dirname: string): boolean {
|
||||
return relPath === dirname || relPath.startsWith(`${dirname}/`) || relPath.startsWith(`${dirname}${sep}`);
|
||||
}
|
||||
|
||||
function normalizeWritePath(root: string, rawPath: unknown): { relPath?: string; reason?: string } {
|
||||
if (typeof rawPath !== "string" || rawPath.trim() === "") {
|
||||
return { reason: "Orquestra bloqueó una escritura sin path válido." };
|
||||
}
|
||||
|
||||
const absPath = resolve(root, rawPath);
|
||||
const relPath = relative(root, absPath);
|
||||
if (relPath === "" || relPath.startsWith("..") || resolve(root, relPath) !== absPath) {
|
||||
return { reason: "Orquestra bloqueó una escritura fuera de la raíz del proyecto." };
|
||||
}
|
||||
return { relPath: relPath.replace(/\\/g, "/") };
|
||||
}
|
||||
|
||||
function isRootProductFile(relPath: string): boolean {
|
||||
if (relPath.includes("/")) return false;
|
||||
const dot = relPath.lastIndexOf(".");
|
||||
return dot > 0 && ROOT_PRODUCT_EXTENSIONS.has(relPath.slice(dot));
|
||||
}
|
||||
|
||||
function hasActiveBuildRuntime(status: RuntimeStatus | null): boolean {
|
||||
return Boolean(
|
||||
status?.feature_id &&
|
||||
status.stage === "build" &&
|
||||
status.agent === "implementer" &&
|
||||
status.state === "running",
|
||||
);
|
||||
}
|
||||
|
||||
function parseStageCommand(args: string, status: RuntimeStatus | null): { stage?: string; featureId?: string; error?: string } {
|
||||
const parts = args.trim().split(/\s+/).filter(Boolean);
|
||||
const stage = parts[0];
|
||||
const featureFlag = parts.indexOf("--feature-id");
|
||||
const featureId = featureFlag >= 0 ? parts[featureFlag + 1] : parts[1] || status?.feature_id || undefined;
|
||||
if (!stage) return { error: "Uso: /orquestra-stage <stage> [feature_id]" };
|
||||
if (!featureId) return { error: "Falta feature_id. Uso: /orquestra-stage <stage> <feature_id>" };
|
||||
return { stage, featureId };
|
||||
}
|
||||
|
||||
function lastUsefulLine(text: string): string {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.slice(-1)[0] || "stage terminado";
|
||||
}
|
||||
|
||||
function guardWriteOrEdit(event: ToolCallEvent, ctx: ExtensionContext): { block?: boolean; reason?: string } | undefined {
|
||||
if (event.toolName !== "write" && event.toolName !== "edit") return undefined;
|
||||
|
||||
const { relPath, reason } = normalizeWritePath(ctx.cwd, (event.input as { path?: unknown }).path);
|
||||
if (!relPath) return { block: true, reason };
|
||||
|
||||
// Extraer el directorio raíz del path
|
||||
const slashIndex = relPath.indexOf("/");
|
||||
const rootDir = slashIndex === -1 ? relPath : relPath.slice(0, slashIndex);
|
||||
|
||||
// Bloquear escrituras fuera de carpetas permitidas
|
||||
if (!ALLOWED_WRITE_DIRS.has(rootDir)) {
|
||||
return {
|
||||
block: true,
|
||||
reason: `Orquestra bloqueó '${relPath}': solo se permite escribir en ${Array.from(ALLOWED_WRITE_DIRS).join(", ")}. No crees carpetas ni archivos en la raíz ni en ubicaciones no autorizadas.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (isRootProductFile(relPath)) {
|
||||
return {
|
||||
block: true,
|
||||
reason: `Orquestra bloqueó '${relPath}': los archivos de producto/código no van en la raíz. Escribí ese código dentro de project/.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (isInsideRel(relPath, "project") || isInsideRel(relPath, "tests")) {
|
||||
const status = safeReadJson(ctx.cwd, STATUS_FILE);
|
||||
if (!hasActiveBuildRuntime(status)) {
|
||||
return {
|
||||
block: true,
|
||||
reason:
|
||||
"Orquestra bloqueó esta escritura: los cambios en project/ o tests/ requieren una feature/ticket activa, stage build, agent implementer y state running. Seleccioná o creá un ticket y actualizá work/runtime-status.json con scripts/agent_status.py antes de escribir.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function render(root: string, status: RuntimeStatus | null, emojis: Record<string, string>) {
|
||||
if (!status) {
|
||||
return {
|
||||
widget: ["Orquestra: no se pudo leer work/runtime-status.json"],
|
||||
footer: "Orquestra status unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
const feature = status.feature_id || "—";
|
||||
const stage = status.stage || "—";
|
||||
const agent = status.agent || "leader";
|
||||
const action = status.action || "—";
|
||||
const state = status.state || "—";
|
||||
const nextAgent = status.next_agent || "—";
|
||||
const waitingFor = status.waiting_for || "—";
|
||||
const updatedAt = status.updated_at || "—";
|
||||
const lastEvent = status.timeline && status.timeline.length > 0 ? status.timeline[status.timeline.length - 1] : null;
|
||||
const lastLine = lastEvent
|
||||
? `Último: ${lastEvent.ts || "—"} · ${emojis[lastEvent.agent || "leader"] || "•"} ${lastEvent.message || "—"}`
|
||||
: "Último: —";
|
||||
|
||||
return {
|
||||
widget: [
|
||||
`Orquestra ${emojis[agent] || "•"} ${feature} · ${stage} · ${agent} · ${state}`,
|
||||
`Acción: ${action}`,
|
||||
`Siguiente: ${emojis[nextAgent] || "•"} ${nextAgent} · Esperando: ${waitingFor}`,
|
||||
agentSummary(root, status, emojis),
|
||||
gateSummary(root, status.feature_id || null, emojis),
|
||||
lastLine,
|
||||
`Actualizado: ${updatedAt}`,
|
||||
],
|
||||
footer: `Orquestra ${emojis[agent] || "•"} ${feature} · ${stage} · ${state}`,
|
||||
};
|
||||
}
|
||||
|
||||
export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
||||
let currentCtx: ExtensionContext | null = null;
|
||||
let watching = false;
|
||||
let lastRendered = "";
|
||||
|
||||
function apply(ctx: ExtensionContext) {
|
||||
const root = ctx.cwd;
|
||||
const emojis = loadRoleEmojis(root);
|
||||
const rendered = render(root, safeReadJson(root, STATUS_FILE), emojis);
|
||||
const signature = JSON.stringify(rendered);
|
||||
if (signature === lastRendered) return;
|
||||
lastRendered = signature;
|
||||
ctx.ui.setWidget(WIDGET_KEY, rendered.widget);
|
||||
ctx.ui.setStatus(STATUS_KEY, rendered.footer);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!currentCtx || !currentCtx.hasUI) return;
|
||||
apply(currentCtx);
|
||||
}
|
||||
|
||||
function startWatching() {
|
||||
if (watching) return;
|
||||
if (!currentCtx) return;
|
||||
watchFile(join(currentCtx.cwd, STATUS_FILE), { interval: 1000, persistent: false }, () => refresh());
|
||||
watching = true;
|
||||
}
|
||||
|
||||
function stopWatching() {
|
||||
if (!watching) return;
|
||||
if (currentCtx) unwatchFile(join(currentCtx.cwd, STATUS_FILE));
|
||||
watching = false;
|
||||
}
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
currentCtx = ctx;
|
||||
apply(ctx);
|
||||
startWatching();
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async (_event, ctx) => {
|
||||
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
||||
ctx.ui.setStatus(STATUS_KEY, undefined);
|
||||
stopWatching();
|
||||
currentCtx = null;
|
||||
});
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => guardWriteOrEdit(event, ctx));
|
||||
|
||||
pi.registerCommand("orquestra-status", {
|
||||
description: "Refresca el widget de runtime status de Orquestra",
|
||||
handler: async (_args, ctx) => {
|
||||
currentCtx = ctx;
|
||||
apply(ctx);
|
||||
ctx.ui.notify("Orquestra status refrescado", "info");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("orquestra-stage", {
|
||||
description: "Ejecuta un stage Orquestra en un proceso Pi fresco: /orquestra-stage <stage> [feature_id]",
|
||||
handler: async (args, ctx) => {
|
||||
const parsed = parseStageCommand(args, safeReadJson(ctx.cwd, STATUS_FILE));
|
||||
if (parsed.error) {
|
||||
ctx.ui.notify(parsed.error, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ui.notify(`Orquestra ejecutando ${parsed.stage} en Pi fresco…`, "info");
|
||||
try {
|
||||
const output = execFileSync("python3", ["scripts/run_stage.py", parsed.stage!, "--feature-id", parsed.featureId!], {
|
||||
cwd: ctx.cwd,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
currentCtx = ctx;
|
||||
apply(ctx);
|
||||
ctx.ui.notify(`Orquestra stage terminado: ${lastUsefulLine(output)}`, "info");
|
||||
} catch (error) {
|
||||
const failed = error as { stdout?: string; stderr?: string; message?: string };
|
||||
const detail = lastUsefulLine(`${failed.stdout || ""}\n${failed.stderr || ""}`) || failed.message || "error desconocido";
|
||||
currentCtx = ctx;
|
||||
apply(ctx);
|
||||
ctx.ui.notify(`Orquestra stage falló: ${detail}`, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
56
platforms/pi/extensions/orquestra-web-fetch.ts
Normal file
56
platforms/pi/extensions/orquestra-web-fetch.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
|
||||
function textFromHtml(html: string): { title: string; content: string } {
|
||||
const title = html.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim() || "No title";
|
||||
const content = html
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, " ")
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return { title, content };
|
||||
}
|
||||
|
||||
async function fetchText(url: string, signal?: AbortSignal) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 30000);
|
||||
const activeSignal = signal ?? controller.signal;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: activeSignal,
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 Orquestra/1.0",
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return textFromHtml(await response.text());
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export default function orquestraWebFetch(pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
name: "orquestra_web_fetch",
|
||||
label: "Orquestra Web Fetch",
|
||||
description: "Fetch a web page and return extracted text using a simple non-browser fetch.",
|
||||
parameters: Type.Object({
|
||||
url: Type.String({ description: "URL to fetch" }),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 500, maximum: 20000, description: "Max characters to return" })),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal) {
|
||||
const { title, content } = await fetchText(params.url, signal);
|
||||
const limit = params.limit ?? 8000;
|
||||
return {
|
||||
content: [{ type: "text", text: `Title: ${title}\n\nContent:\n${content.slice(0, limit)}` }],
|
||||
details: { title, url: params.url, characters: content.length },
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
Actúa como `leader`.
|
||||
No implementes código de producto.
|
||||
Orquesta stages según `harness/workflow.stages.yml`.
|
||||
Aplica anti-trampa de `harness/agents.matrix.yml`.
|
||||
No marques `done` sin gates APPROVED y `./scripts/verify.sh` en verde.
|
||||
Actúa como `leader` secuencial de Orquestra.
|
||||
|
||||
Reglas obligatorias:
|
||||
- No implementes código de producto.
|
||||
- No uses subagentes.
|
||||
- Antes de cada stage, ejecutá `python3 scripts/agent_status.py set ...` con el rol/stage correcto.
|
||||
- Si `agent_status.py` rechaza el cambio de stage, STOP: falta evidencia previa.
|
||||
- Orquestá stages según `harness/workflow.stages.yml`.
|
||||
- Usá `document`/`documenter` solo si cambiaron docs/API/contratos/comportamiento user-facing.
|
||||
- Aplicá anti-trampa de `harness/agents.matrix.yml`.
|
||||
- No marques `done` sin reviewer/security/qa APPROVED y `./scripts/verify.sh` en verde; `documenter.md` no es requisito de cierre por defecto.
|
||||
|
||||
0
project/.gitkeep
Normal file
0
project/.gitkeep
Normal file
382
scripts/agent_status.py
Executable file
382
scripts/agent_status.py
Executable file
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
STATUS_PATH = ROOT / 'work' / 'runtime-status.json'
|
||||
MATRIX_PATH = ROOT / 'harness' / 'agents.matrix.yml'
|
||||
WORKFLOW_PATH = ROOT / 'harness' / 'workflow.stages.yml'
|
||||
ARTIFACTS_DIR = ROOT / 'work' / 'artifacts'
|
||||
VALID_RUNTIME_STATES = {'idle', 'waiting', 'running', 'blocked', 'done'}
|
||||
|
||||
DEFAULT_EMOJIS = {
|
||||
'leader': '🧭',
|
||||
'triager': '🧩',
|
||||
'architect': '🏗️',
|
||||
'implementer': '🛠️',
|
||||
'reviewer': '🔍',
|
||||
'security': '🔒',
|
||||
'qa': '🧪',
|
||||
'documenter': '📝',
|
||||
}
|
||||
|
||||
GATE_FILES = {
|
||||
'reviewer': 'reviewer.json',
|
||||
'security': 'security.json',
|
||||
'qa': 'qa.json',
|
||||
'leader': 'leader-close.json',
|
||||
}
|
||||
|
||||
AGENT_FILES = {
|
||||
'architect': 'architect.md',
|
||||
'implementer': 'implementer.md',
|
||||
'reviewer': 'reviewer.json',
|
||||
'security': 'security.json',
|
||||
'qa': 'qa.json',
|
||||
'documenter': 'documenter.md',
|
||||
'leader': 'leader-close.json',
|
||||
}
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
|
||||
|
||||
|
||||
def load_json(path: Path, default=None):
|
||||
if not path.exists():
|
||||
return default
|
||||
return json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def save_json(path: Path, payload):
|
||||
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
def load_role_emojis():
|
||||
emojis = dict(DEFAULT_EMOJIS)
|
||||
if not MATRIX_PATH.exists():
|
||||
return emojis
|
||||
current_role = None
|
||||
for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines():
|
||||
match_role = re.match(r'^ ([a-z_]+):\s*$', line)
|
||||
if match_role:
|
||||
current_role = match_role.group(1)
|
||||
continue
|
||||
match_emoji = re.match(r'^\s{4}emoji:\s*["\']?(.*?)["\']?\s*$', line)
|
||||
if match_emoji and current_role:
|
||||
emojis[current_role] = match_emoji.group(1)
|
||||
return emojis
|
||||
|
||||
|
||||
def load_roles():
|
||||
roles = []
|
||||
if not MATRIX_PATH.exists():
|
||||
return roles
|
||||
for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines():
|
||||
match_role = re.match(r'^ ([a-z_]+):\s*$', line)
|
||||
if match_role:
|
||||
roles.append(match_role.group(1))
|
||||
return roles
|
||||
|
||||
|
||||
def load_stage_names():
|
||||
stages = []
|
||||
if not WORKFLOW_PATH.exists():
|
||||
return stages
|
||||
for line in WORKFLOW_PATH.read_text(encoding='utf-8').splitlines():
|
||||
match_stage = re.match(r'^ - name:\s*([a-z_]+)\s*$', line)
|
||||
if match_stage:
|
||||
stages.append(match_stage.group(1))
|
||||
return stages
|
||||
|
||||
|
||||
def default_status():
|
||||
return {
|
||||
'feature_id': None,
|
||||
'stage': 'idle',
|
||||
'agent': 'leader',
|
||||
'action': 'Sin ejecución activa',
|
||||
'state': 'waiting',
|
||||
'next_agent': 'leader',
|
||||
'waiting_for': 'Seleccionar una feature pending y actualizar este estado',
|
||||
'updated_at': now_iso(),
|
||||
'timeline': [],
|
||||
}
|
||||
|
||||
|
||||
def load_status():
|
||||
status = load_json(STATUS_PATH, default_status())
|
||||
base = default_status()
|
||||
for key, value in base.items():
|
||||
status.setdefault(key, value)
|
||||
if not isinstance(status.get('timeline'), list):
|
||||
status['timeline'] = []
|
||||
return status
|
||||
|
||||
|
||||
def gate_status(feature_id):
|
||||
gates = {}
|
||||
if not feature_id:
|
||||
return gates
|
||||
feature_dir = ARTIFACTS_DIR / feature_id
|
||||
for gate, filename in GATE_FILES.items():
|
||||
path = feature_dir / filename
|
||||
if not path.exists():
|
||||
gates[gate] = 'pending'
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding='utf-8'))
|
||||
gates[gate] = 'approved' if payload.get('verdict') == 'APPROVED' else 'present'
|
||||
except Exception:
|
||||
gates[gate] = 'invalid'
|
||||
return gates
|
||||
|
||||
|
||||
def render_gate(gate, state, emojis):
|
||||
icon = {
|
||||
'approved': '✅',
|
||||
'pending': '⏳',
|
||||
'present': '⚠️',
|
||||
'invalid': '❌',
|
||||
}.get(state, '•')
|
||||
label = {
|
||||
'leader': 'close',
|
||||
}.get(gate, gate)
|
||||
return f"{icon} {emojis.get(gate, '•')} {label}: {state.upper()}"
|
||||
|
||||
|
||||
def artifact_status(feature_id, agent):
|
||||
if not feature_id:
|
||||
return 'pending'
|
||||
filename = AGENT_FILES.get(agent)
|
||||
if not filename:
|
||||
return 'pending'
|
||||
path = ARTIFACTS_DIR / feature_id / filename
|
||||
if not path.exists():
|
||||
return 'pending'
|
||||
if filename.endswith('.md'):
|
||||
return 'done'
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding='utf-8'))
|
||||
return 'done' if payload.get('verdict') == 'APPROVED' else 'present'
|
||||
except Exception:
|
||||
return 'invalid'
|
||||
|
||||
|
||||
def render_artifact(agent, state, current_agent, emojis):
|
||||
icon = {
|
||||
'done': '✅',
|
||||
'pending': '▶️' if agent == current_agent else '⏳',
|
||||
'present': '⚠️',
|
||||
'invalid': '❌',
|
||||
}.get(state, '•')
|
||||
return f"{icon} {emojis.get(agent, '•')} {agent}: {state.upper()}"
|
||||
|
||||
|
||||
STAGE_OWNERS = {
|
||||
'idle': 'leader',
|
||||
'intake': 'leader',
|
||||
'design': 'architect',
|
||||
'build': 'implementer',
|
||||
'review_gate': 'reviewer',
|
||||
'security_gate': 'security',
|
||||
'qa_gate': 'qa',
|
||||
'document': 'documenter',
|
||||
'close': 'leader',
|
||||
}
|
||||
|
||||
STAGE_PREREQUISITES = {
|
||||
'review_gate': [('implementer.md', None)],
|
||||
'security_gate': [('implementer.md', None), ('reviewer.json', 'APPROVED')],
|
||||
'qa_gate': [('implementer.md', None), ('reviewer.json', 'APPROVED'), ('security.json', 'APPROVED')],
|
||||
'document': [('implementer.md', None), ('reviewer.json', 'APPROVED'), ('security.json', 'APPROVED'), ('qa.json', 'APPROVED')],
|
||||
'close': [('implementer.md', None), ('reviewer.json', 'APPROVED'), ('security.json', 'APPROVED'), ('qa.json', 'APPROVED')],
|
||||
}
|
||||
|
||||
|
||||
def artifact_ok(feature_id, filename, verdict=None):
|
||||
path = ARTIFACTS_DIR / feature_id / filename
|
||||
if not path.exists():
|
||||
return False
|
||||
if verdict is None:
|
||||
return True
|
||||
try:
|
||||
return json.loads(path.read_text(encoding='utf-8')).get('verdict') == verdict
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def validate_stage_transition(status, args):
|
||||
stage = args.stage or status.get('stage') or 'idle'
|
||||
agent = args.agent or status.get('agent') or STAGE_OWNERS.get(stage)
|
||||
feature_id = args.feature_id if args.feature_id is not None else status.get('feature_id')
|
||||
state = args.state
|
||||
|
||||
expected_owner = STAGE_OWNERS.get(stage)
|
||||
if expected_owner and agent != expected_owner:
|
||||
raise SystemExit(f"Stage {stage} debe ejecutarlo {expected_owner}, no {agent}")
|
||||
|
||||
if stage != 'idle' and not feature_id:
|
||||
raise SystemExit(f"Stage {stage} requiere feature_id")
|
||||
|
||||
if feature_id:
|
||||
for filename, verdict in STAGE_PREREQUISITES.get(stage, []):
|
||||
if not artifact_ok(feature_id, filename, verdict):
|
||||
suffix = f" con verdict {verdict}" if verdict else ""
|
||||
raise SystemExit(f"No se puede entrar en {stage}: falta {filename}{suffix} para {feature_id}")
|
||||
|
||||
# Block marking close stage as done without all required gates
|
||||
if stage == 'close' and state == 'done' and feature_id:
|
||||
missing_gates = []
|
||||
for gate_agent in ['reviewer', 'security', 'qa']:
|
||||
gate_file = GATE_FILES[gate_agent]
|
||||
if not artifact_ok(feature_id, gate_file, 'APPROVED'):
|
||||
missing_gates.append(f"{gate_file} (APPROVED)")
|
||||
if missing_gates:
|
||||
raise SystemExit(f"No se puede cerrar {feature_id}: faltan gates aprobados: {', '.join(missing_gates)}")
|
||||
|
||||
|
||||
def validate_runtime_args(args):
|
||||
roles = set(load_roles()) or set(DEFAULT_EMOJIS)
|
||||
stages = set(load_stage_names()) | {'idle'}
|
||||
|
||||
if args.agent is not None and args.agent not in roles:
|
||||
raise SystemExit(f"Invalid agent: {args.agent}. Allowed: {', '.join(sorted(roles))}")
|
||||
if args.next_agent is not None and args.next_agent not in roles:
|
||||
raise SystemExit(f"Invalid next-agent: {args.next_agent}. Allowed: {', '.join(sorted(roles))}")
|
||||
if args.stage is not None and args.stage not in stages:
|
||||
raise SystemExit(f"Invalid stage: {args.stage}. Allowed: {', '.join(sorted(stages))}")
|
||||
if args.state is not None and args.state not in VALID_RUNTIME_STATES:
|
||||
raise SystemExit(f"Invalid state: {args.state}. Allowed: {', '.join(sorted(VALID_RUNTIME_STATES))}")
|
||||
|
||||
|
||||
def show_status():
|
||||
status = load_status()
|
||||
emojis = load_role_emojis()
|
||||
feature_id = status.get('feature_id') or '—'
|
||||
current_agent = status.get('agent', 'leader')
|
||||
next_agent = status.get('next_agent') or '—'
|
||||
gates = gate_status(status.get('feature_id'))
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════════╗')
|
||||
print('║ Orquestra · Runtime Status ║')
|
||||
print('╚══════════════════════════════════════════════════════════════╝')
|
||||
print(f"Feature activa : {feature_id}")
|
||||
print(f"Stage actual : {status.get('stage', '—')}")
|
||||
print(f"Agente actual : {emojis.get(current_agent, '•')} {current_agent}")
|
||||
print(f"Acción : {status.get('action', '—')}")
|
||||
print(f"Estado : {status.get('state', '—')}")
|
||||
print(f"Siguiente : {emojis.get(next_agent, '•')} {next_agent}")
|
||||
print(f"Esperando : {status.get('waiting_for', '—')}")
|
||||
print(f"Actualizado : {status.get('updated_at', '—')}")
|
||||
print()
|
||||
print('Gates')
|
||||
if gates:
|
||||
for gate in ['reviewer', 'security', 'qa', 'leader']:
|
||||
print(f" {render_gate(gate, gates.get(gate, 'pending'), emojis)}")
|
||||
else:
|
||||
print(' — Sin feature activa —')
|
||||
print()
|
||||
print('Artifacts')
|
||||
if status.get('feature_id'):
|
||||
for agent in ['architect', 'implementer', 'reviewer', 'security', 'qa', 'documenter', 'leader']:
|
||||
state = artifact_status(status.get('feature_id'), agent)
|
||||
print(f" {render_artifact(agent, state, current_agent, emojis)}")
|
||||
else:
|
||||
print(' — Sin feature activa —')
|
||||
print()
|
||||
print('Timeline')
|
||||
timeline = status.get('timeline', [])[-8:]
|
||||
if not timeline:
|
||||
print(' — Sin eventos —')
|
||||
return
|
||||
for item in timeline:
|
||||
agent = item.get('agent', 'leader')
|
||||
emoji = emojis.get(agent, '•')
|
||||
ts = item.get('ts', '—')
|
||||
stage = item.get('stage', '—')
|
||||
state = item.get('state', '—')
|
||||
message = item.get('message', '')
|
||||
print(f" - {ts} · {emoji} {agent} · {stage} · {state} · {message}")
|
||||
|
||||
|
||||
def set_status(args):
|
||||
validate_runtime_args(args)
|
||||
status = load_status()
|
||||
validate_stage_transition(status, args)
|
||||
if args.feature_id is not None:
|
||||
status['feature_id'] = args.feature_id or None
|
||||
if args.stage is not None:
|
||||
status['stage'] = args.stage
|
||||
if args.agent is not None:
|
||||
status['agent'] = args.agent
|
||||
if args.action is not None:
|
||||
status['action'] = args.action
|
||||
if args.state is not None:
|
||||
status['state'] = args.state
|
||||
if args.next_agent is not None:
|
||||
status['next_agent'] = args.next_agent
|
||||
if args.waiting_for is not None:
|
||||
status['waiting_for'] = args.waiting_for
|
||||
|
||||
status['updated_at'] = now_iso()
|
||||
event_message = args.note or status.get('action') or 'Estado actualizado'
|
||||
status['timeline'].append({
|
||||
'ts': status['updated_at'],
|
||||
'agent': status.get('agent', 'leader'),
|
||||
'stage': status.get('stage', '—'),
|
||||
'state': status.get('state', '—'),
|
||||
'message': event_message,
|
||||
})
|
||||
status['timeline'] = status['timeline'][-20:]
|
||||
save_json(STATUS_PATH, status)
|
||||
show_status()
|
||||
|
||||
|
||||
def reset_status(_args):
|
||||
status = default_status()
|
||||
status['updated_at'] = now_iso()
|
||||
save_json(STATUS_PATH, status)
|
||||
show_status()
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(description='Renderiza y actualiza el estado visible de Orquestra.')
|
||||
sub = parser.add_subparsers(dest='command', required=True)
|
||||
|
||||
sub.add_parser('show', help='Muestra el panel visible de estado')
|
||||
|
||||
set_parser = sub.add_parser('set', help='Actualiza el estado runtime y añade evento a timeline')
|
||||
set_parser.add_argument('--feature-id')
|
||||
set_parser.add_argument('--stage')
|
||||
set_parser.add_argument('--agent')
|
||||
set_parser.add_argument('--action')
|
||||
set_parser.add_argument('--state')
|
||||
set_parser.add_argument('--next-agent')
|
||||
set_parser.add_argument('--waiting-for')
|
||||
set_parser.add_argument('--note')
|
||||
|
||||
sub.add_parser('reset', help='Resetea el estado runtime a idle')
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
if args.command == 'show':
|
||||
show_status()
|
||||
elif args.command == 'set':
|
||||
set_status(args)
|
||||
elif args.command == 'reset':
|
||||
reset_status(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
119
scripts/close_feature.py
Executable file
119
scripts/close_feature.py
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Close a feature: validates all gates are approved, updates backlog, and runs commit.
|
||||
Usage: python3 scripts/close_feature.py <feature_id>
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKLOG_PATH = ROOT / 'backlog' / 'features.json'
|
||||
ARTIFACTS_DIR = ROOT / 'work' / 'artifacts'
|
||||
COMMIT_SCRIPT = ROOT / 'scripts' / 'commit_feature.sh'
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
|
||||
|
||||
|
||||
def check_gate(feature_id, gate_file, expected_verdict='APPROVED'):
|
||||
"""Check if a gate artifact exists and has the expected verdict."""
|
||||
path = ARTIFACTS_DIR / feature_id / gate_file
|
||||
if not path.exists():
|
||||
return False, f"Missing {gate_file}"
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding='utf-8'))
|
||||
if data.get('verdict') != expected_verdict:
|
||||
return False, f"{gate_file} verdict is {data.get('verdict')}, expected {expected_verdict}"
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, f"{gate_file} invalid: {e}"
|
||||
|
||||
|
||||
def close_feature(feature_id):
|
||||
"""Close a feature after validating all gates."""
|
||||
print(f"Closing feature {feature_id}...")
|
||||
|
||||
# Check all required gates
|
||||
gates = [
|
||||
('reviewer.json', 'APPROVED'),
|
||||
('security.json', 'APPROVED'),
|
||||
('qa.json', 'APPROVED'),
|
||||
('leader-close.json', None), # Just needs to exist
|
||||
]
|
||||
|
||||
errors = []
|
||||
for gate_file, verdict in gates:
|
||||
if verdict:
|
||||
ok, err = check_gate(feature_id, gate_file, verdict)
|
||||
else:
|
||||
path = ARTIFACTS_DIR / feature_id / gate_file
|
||||
ok = path.exists()
|
||||
err = f"Missing {gate_file}" if not ok else None
|
||||
|
||||
if not ok:
|
||||
errors.append(err)
|
||||
print(f" ❌ {err}")
|
||||
else:
|
||||
print(f" ✅ {gate_file}")
|
||||
|
||||
if errors:
|
||||
print(f"\n[FAIL] Cannot close {feature_id}: {len(errors)} gate(s) missing or invalid")
|
||||
return 1
|
||||
|
||||
# Update backlog
|
||||
if not BACKLOG_PATH.exists():
|
||||
print(f"[FAIL] Backlog not found: {BACKLOG_PATH}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
backlog = json.loads(BACKLOG_PATH.read_text(encoding='utf-8'))
|
||||
except Exception as e:
|
||||
print(f"[FAIL] Invalid backlog: {e}")
|
||||
return 1
|
||||
|
||||
feature_found = False
|
||||
for feature in backlog.get('features', []):
|
||||
if feature.get('id') == feature_id:
|
||||
feature['status'] = 'done'
|
||||
feature['completed_at'] = now_iso()
|
||||
feature_found = True
|
||||
print(f"\n[OK] Updated backlog: {feature_id} -> done")
|
||||
break
|
||||
|
||||
if not feature_found:
|
||||
print(f"[FAIL] Feature {feature_id} not found in backlog")
|
||||
return 1
|
||||
|
||||
# Save backlog
|
||||
try:
|
||||
BACKLOG_PATH.write_text(json.dumps(backlog, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
except Exception as e:
|
||||
print(f"[FAIL] Cannot save backlog: {e}")
|
||||
return 1
|
||||
|
||||
# Run commit script
|
||||
if COMMIT_SCRIPT.exists():
|
||||
print(f"\nRunning commit script...")
|
||||
result = subprocess.run([str(COMMIT_SCRIPT), feature_id], cwd=ROOT)
|
||||
if result.returncode != 0:
|
||||
print(f"[WARN] Commit script returned {result.returncode}")
|
||||
|
||||
print(f"\n✅ Feature {feature_id} closed successfully")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python3 scripts/close_feature.py <feature_id>")
|
||||
return 1
|
||||
|
||||
feature_id = sys.argv[1]
|
||||
return close_feature(feature_id)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
58
scripts/commit_feature.sh
Executable file
58
scripts/commit_feature.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Commit and optionally push a completed feature
|
||||
# Usage: ./scripts/commit_feature.sh <feature_id>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FEATURE_ID="${1:-}"
|
||||
if [ -z "$FEATURE_ID" ]; then
|
||||
echo "[FAIL] Usage: $0 <feature_id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if git repo
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "[SKIP] Not a git repository, skipping commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stage all changes (excluding node_modules, dist, .next, etc.)
|
||||
git add -A
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --cached --quiet; then
|
||||
echo "[SKIP] No changes to commit for $FEATURE_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create commit message
|
||||
COMMIT_MSG="feat($FEATURE_ID): completed feature"
|
||||
|
||||
# Check if there's a description in the feature
|
||||
if [ -f "work/current.md" ]; then
|
||||
# Extract first line of description if available
|
||||
DESC=$(grep -m1 "^## Description" work/current.md -A1 2>/dev/null | tail -1 | sed 's/^[[:space:]]*//' || true)
|
||||
if [ -n "$DESC" ]; then
|
||||
COMMIT_MSG="$COMMIT_MSG
|
||||
|
||||
$DESC"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Commit
|
||||
git commit -m "$COMMIT_MSG"
|
||||
|
||||
echo "[OK] Committed $FEATURE_ID"
|
||||
|
||||
# Check if remote exists and push
|
||||
if git remote get-url origin >/dev/null 2>&1; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "[INFO] Pushing to origin/$BRANCH..."
|
||||
if git push origin "$BRANCH"; then
|
||||
echo "[OK] Pushed to origin/$BRANCH"
|
||||
else
|
||||
echo "[WARN] Push failed (you can push manually later)"
|
||||
fi
|
||||
else
|
||||
echo "[SKIP] No remote 'origin' configured, skipping push"
|
||||
fi
|
||||
166
scripts/install.sh
Executable file
166
scripts/install.sh
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
banner() {
|
||||
cat <<'EOF'
|
||||
___ ____ ___ _ _ _____ ____ _____ ____ _
|
||||
/ _ \| _ \ / _ \| | | | ____/ ___|_ _| _ \ / \
|
||||
| | | | |_) | | | | | | | _| \___ \ | | | |_) | / _ \
|
||||
| |_| | _ <| |_| | |_| | |___ ___) || | | _ < / ___ \
|
||||
\___/|_| \_\\__\_\\___/|_____|____/ |_| |_| \_\/_/ \_\
|
||||
|
||||
sequential orchestration runtime
|
||||
EOF
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/install.sh /path/to/project-repo
|
||||
|
||||
Install or update Orquestra from this source checkout into a target project repo.
|
||||
Project-owned progress files are created only when missing and are never overwritten.
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[FAIL] %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[WARN] %s\n' "$1" >&2
|
||||
}
|
||||
|
||||
info() {
|
||||
printf '[OK] %s\n' "$1"
|
||||
}
|
||||
|
||||
copy_file_update() {
|
||||
local src=$1
|
||||
local dst=$2
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp "$src" "$dst"
|
||||
}
|
||||
|
||||
copy_file_if_missing() {
|
||||
local src=$1
|
||||
local dst=$2
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
if [ ! -e "$dst" ]; then
|
||||
cp "$src" "$dst"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_dir_update() {
|
||||
local src=$1
|
||||
local dst=$2
|
||||
mkdir -p "$dst"
|
||||
cp -R "$src"/. "$dst"/
|
||||
}
|
||||
|
||||
copy_dir_if_missing_contents() {
|
||||
local src=$1
|
||||
local dst=$2
|
||||
local dir file rel
|
||||
mkdir -p "$dst"
|
||||
if [ -d "$src" ]; then
|
||||
find "$src" -type d | while IFS= read -r dir; do
|
||||
rel=${dir#"$src"}
|
||||
mkdir -p "$dst$rel"
|
||||
done
|
||||
find "$src" -type f | while IFS= read -r file; do
|
||||
rel=${file#"$src"/}
|
||||
copy_file_if_missing "$file" "$dst/$rel"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
append_gitignore_block() {
|
||||
local gitignore=$1
|
||||
touch "$gitignore"
|
||||
if ! grep -q '^# BEGIN ORQUESTRA$' "$gitignore"; then
|
||||
cat >>"$gitignore" <<'EOF'
|
||||
|
||||
# BEGIN ORQUESTRA
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
.codegraph/
|
||||
.atl/
|
||||
# END ORQUESTRA
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
banner
|
||||
|
||||
command -v pi >/dev/null 2>&1 || fail "pi is required in PATH before installing Orquestra"
|
||||
command -v python3 >/dev/null 2>&1 || fail "python3 is required in PATH before installing Orquestra"
|
||||
|
||||
SOURCE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
TARGET_ARG=$1
|
||||
TARGET_DIR=$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$TARGET_ARG")
|
||||
|
||||
if [ "$TARGET_DIR" = "$SOURCE_DIR" ]; then
|
||||
fail "target repo must be different from the Orquestra source repo"
|
||||
fi
|
||||
|
||||
mkdir -p "$TARGET_DIR"
|
||||
|
||||
if [ ! -d "$TARGET_DIR/.git" ]; then
|
||||
git -C "$TARGET_DIR" init >/dev/null
|
||||
info "Initialized git repo in $TARGET_DIR"
|
||||
fi
|
||||
|
||||
# Harness-owned files: update on every run.
|
||||
for file in AGENTS.md README.md HOWTO.md CHECKPOINTS.md; do
|
||||
copy_file_update "$SOURCE_DIR/$file" "$TARGET_DIR/$file"
|
||||
done
|
||||
|
||||
copy_dir_update "$SOURCE_DIR/harness" "$TARGET_DIR/harness"
|
||||
copy_dir_update "$SOURCE_DIR/platforms/pi" "$TARGET_DIR/platforms/pi"
|
||||
copy_dir_update "$SOURCE_DIR/docs" "$TARGET_DIR/docs"
|
||||
|
||||
for file in scripts/verify.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/install.sh; do
|
||||
copy_file_update "$SOURCE_DIR/$file" "$TARGET_DIR/$file"
|
||||
done
|
||||
|
||||
append_gitignore_block "$TARGET_DIR/.gitignore"
|
||||
|
||||
# Project-owned/state files: create only when missing.
|
||||
copy_dir_if_missing_contents "$SOURCE_DIR/project" "$TARGET_DIR/project"
|
||||
copy_file_if_missing "$SOURCE_DIR/backlog/features.json" "$TARGET_DIR/backlog/features.json"
|
||||
copy_dir_if_missing_contents "$SOURCE_DIR/spec" "$TARGET_DIR/spec"
|
||||
copy_file_if_missing "$SOURCE_DIR/work/current.md" "$TARGET_DIR/work/current.md"
|
||||
copy_file_if_missing "$SOURCE_DIR/work/history.md" "$TARGET_DIR/work/history.md"
|
||||
copy_file_if_missing "$SOURCE_DIR/work/runtime-status.json" "$TARGET_DIR/work/runtime-status.json"
|
||||
mkdir -p "$TARGET_DIR/work/artifacts"
|
||||
if [ -f "$SOURCE_DIR/work/artifacts/.gitkeep" ] && [ ! -e "$TARGET_DIR/work/artifacts/.gitkeep" ]; then
|
||||
cp "$SOURCE_DIR/work/artifacts/.gitkeep" "$TARGET_DIR/work/artifacts/.gitkeep"
|
||||
fi
|
||||
|
||||
# Pi project-local runtime extensions.
|
||||
mkdir -p "$TARGET_DIR/.pi/extensions"
|
||||
copy_dir_update "$SOURCE_DIR/platforms/pi/extensions/orquestra-status" "$TARGET_DIR/.pi/extensions/orquestra-status"
|
||||
copy_file_update "$SOURCE_DIR/platforms/pi/extensions/orquestra-web-fetch.ts" "$TARGET_DIR/.pi/extensions/orquestra-web-fetch.ts"
|
||||
|
||||
if [ -d "$TARGET_DIR/.pi/subagents" ] || [ -f "$TARGET_DIR/.pi/subagents.json" ]; then
|
||||
warn "Found .pi/subagents or .pi/subagents.json in target. Orquestra does not delete user Pi config automatically; remove Orquestra-owned stale subagent files manually before running verify.sh."
|
||||
fi
|
||||
|
||||
chmod +x "$TARGET_DIR/scripts/verify.sh" "$TARGET_DIR/scripts/pi_orquestra.sh" "$TARGET_DIR/scripts/run_stage.py" "$TARGET_DIR/scripts/install.sh"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Orquestra installed/updated safely in: $TARGET_DIR
|
||||
|
||||
Next steps:
|
||||
cd "$TARGET_DIR"
|
||||
./scripts/verify.sh
|
||||
./scripts/pi_orquestra.sh
|
||||
EOF
|
||||
102
scripts/new_ticket.py
Executable file
102
scripts/new_ticket.py
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKLOG = ROOT / 'backlog' / 'features.json'
|
||||
TYPE_CHOICES = ('feature', 'fix', 'bug', 'chore')
|
||||
LEVEL_CHOICES = ('low', 'med', 'high')
|
||||
|
||||
|
||||
def ask(prompt, default=''):
|
||||
value = input(f"{prompt}{' [' + default + ']' if default else ''}: ").strip()
|
||||
return value if value else default
|
||||
|
||||
|
||||
def ask_choice(prompt, choices, default):
|
||||
while True:
|
||||
value = ask(prompt, default).lower()
|
||||
if value in choices:
|
||||
return value
|
||||
print(f"Invalid value. Use one of: {', '.join(choices)}")
|
||||
|
||||
|
||||
def ask_list(prompt, default_csv=''):
|
||||
raw = ask(prompt, default_csv)
|
||||
return [item.strip() for item in raw.split(',') if item.strip()]
|
||||
|
||||
|
||||
def next_id(features):
|
||||
nums = []
|
||||
for feature in features:
|
||||
fid = str(feature.get('id', ''))
|
||||
if fid.startswith('F-') and fid[2:].isdigit():
|
||||
nums.append(int(fid[2:]))
|
||||
return f"F-{(max(nums) + 1) if nums else 1:03d}"
|
||||
|
||||
|
||||
def main():
|
||||
data = json.loads(BACKLOG.read_text(encoding='utf-8'))
|
||||
features = data.get('features', [])
|
||||
|
||||
print('Create ticket (English caveman style).')
|
||||
ticket_type = ask_choice('Type (feature/fix/bug/chore)', TYPE_CHOICES, 'feature')
|
||||
title = ask('Title (short EN)', f'{ticket_type.capitalize()} TODO')
|
||||
problem = ask('Problem (short EN)', 'Need change')
|
||||
goal = ask('Goal (short EN)', 'Make flow better')
|
||||
scope_in = ask_list('Scope IN (comma list EN)', 'Core flow')
|
||||
scope_out = ask_list('Scope OUT (comma list EN)', 'No redesign')
|
||||
risk = ask_choice('Risk (low/med/high)', LEVEL_CHOICES, 'low')
|
||||
priority = ask_choice('Priority (low/med/high)', LEVEL_CHOICES, 'med')
|
||||
|
||||
print('Acceptance bullets (EN caveman). Empty line to end.')
|
||||
acceptance = []
|
||||
while True:
|
||||
line = input('- ').strip()
|
||||
if not line:
|
||||
break
|
||||
acceptance.append(line)
|
||||
|
||||
if not acceptance:
|
||||
acceptance = [
|
||||
'Flow works end to end',
|
||||
'No break old behavior',
|
||||
'verify.sh is green',
|
||||
]
|
||||
|
||||
fid = next_id(features)
|
||||
desc = (
|
||||
f"Problem: {problem}. "
|
||||
f"Goal: {goal}. "
|
||||
f"Scope IN: {', '.join(scope_in) or 'none'}. "
|
||||
f"Scope OUT: {', '.join(scope_out) or 'none'}. "
|
||||
f"Type: {ticket_type}. Priority: {priority}. Risk: {risk}."
|
||||
)
|
||||
|
||||
features.append({
|
||||
'id': fid,
|
||||
'type': ticket_type,
|
||||
'title': title,
|
||||
'problem': problem,
|
||||
'goal': goal,
|
||||
'scope_in': scope_in,
|
||||
'scope_out': scope_out,
|
||||
'priority': priority,
|
||||
'risk': risk,
|
||||
'description': desc,
|
||||
'acceptance': acceptance,
|
||||
'status': 'pending',
|
||||
'created_at': str(date.today()),
|
||||
'gates': {'review': False, 'security': False, 'qa': False},
|
||||
})
|
||||
|
||||
data['features'] = features
|
||||
rules = data.setdefault('rules', {})
|
||||
rules.setdefault('valid_types', list(TYPE_CHOICES))
|
||||
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
print(f'Created {fid}: {title}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
28
scripts/pi_orquestra.sh
Executable file
28
scripts/pi_orquestra.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if ! command -v pi >/dev/null 2>&1; then
|
||||
echo "[FAIL] pi is required before running Orquestra" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATUS_EXT=".pi/extensions/orquestra-status"
|
||||
WEB_FETCH_EXT=".pi/extensions/orquestra-web-fetch.ts"
|
||||
ENGRAM_EXT="$HOME/.pi/agent/npm/node_modules/gentle-engram/index.ts"
|
||||
|
||||
if [ ! -f "$STATUS_EXT/index.ts" ] || [ ! -f "$WEB_FETCH_EXT" ]; then
|
||||
echo "[FAIL] Orquestra Pi extensions are not installed in this project." >&2
|
||||
echo " Run: /path/to/orquestra/scripts/install.sh $ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENGRAM_EXT" ]; then
|
||||
echo "[FAIL] gentle-engram is required for Orquestra memory." >&2
|
||||
echo " Install gentle-engram, then run this script again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec pi --no-extensions --no-skills -e "$ENGRAM_EXT" -e "$STATUS_EXT" -e "$WEB_FETCH_EXT" "$@"
|
||||
130
scripts/run_stage.py
Executable file
130
scripts/run_stage.py
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WORKFLOW = ROOT / 'harness' / 'workflow.stages.yml'
|
||||
STATUS_EXT = ROOT / '.pi' / 'extensions' / 'orquestra-status'
|
||||
WEB_FETCH_EXT = ROOT / '.pi' / 'extensions' / 'orquestra-web-fetch.ts'
|
||||
ENGRAM_EXT = Path.home() / '.pi' / 'agent' / 'npm' / 'node_modules' / 'gentle-engram' / 'index.ts'
|
||||
|
||||
|
||||
def parse_stages():
|
||||
stages = {}
|
||||
current = None
|
||||
section = None
|
||||
for raw in WORKFLOW.read_text(encoding='utf-8').splitlines():
|
||||
line = raw.rstrip()
|
||||
if line.startswith(' - name:'):
|
||||
current = {'input': [], 'output': [], 'post_actions': []}
|
||||
current['name'] = line.split(':', 1)[1].strip()
|
||||
stages[current['name']] = current
|
||||
section = None
|
||||
continue
|
||||
if current is None:
|
||||
continue
|
||||
if line.startswith(' owner:'):
|
||||
current['owner'] = line.split(':', 1)[1].strip()
|
||||
elif line.startswith(' input:'):
|
||||
section = 'input'
|
||||
elif line.startswith(' output:'):
|
||||
section = 'output'
|
||||
elif line.startswith(' post_actions:'):
|
||||
section = 'post_actions'
|
||||
elif section and line.startswith(' - '):
|
||||
current[section].append(line.split('- ', 1)[1].strip())
|
||||
elif line.startswith(' '):
|
||||
section = None
|
||||
return stages
|
||||
|
||||
|
||||
def expand(paths, feature_id):
|
||||
return [p.replace('<feature_id>', feature_id) for p in paths]
|
||||
|
||||
|
||||
def prompt_for(stage, feature_id):
|
||||
inputs = expand(stage.get('input', []), feature_id)
|
||||
outputs = expand(stage.get('output', []), feature_id)
|
||||
post_actions = expand(stage.get('post_actions', []), feature_id)
|
||||
owner = stage.get('owner', 'leader')
|
||||
|
||||
post_actions_text = ""
|
||||
if post_actions:
|
||||
post_actions_text = f"""
|
||||
Post-actions (execute after completing the stage):
|
||||
{chr(10).join(f'- {a}' for a in post_actions)}
|
||||
"""
|
||||
|
||||
return f"""You are the Orquestra stage agent '{owner}' for feature '{feature_id}'.
|
||||
|
||||
Fresh-process rule: do not rely on previous chat/session context. Use only the files listed here, explicit Engram memories you choose after a narrow search, and repository evidence you read yourself.
|
||||
|
||||
First read these harness rules:
|
||||
- AGENTS.md
|
||||
- harness/agents.matrix.yml
|
||||
- harness/workflow.stages.yml
|
||||
- harness/contracts/handoff.md
|
||||
|
||||
Stage input paths:
|
||||
{chr(10).join(f'- {p}' for p in inputs) or '- none'}
|
||||
|
||||
Stage output paths:
|
||||
{chr(10).join(f'- {p}' for p in outputs) or '- none'}
|
||||
{post_actions_text}
|
||||
Rules:
|
||||
- Before work, run: python3 scripts/agent_status.py set --feature-id {feature_id} --stage {stage['name']} --agent {owner} --state running --action "Running {stage['name']}"
|
||||
- Read only the stage inputs that exist. If a required input is missing, write a blocked artifact when possible and answer blocked -> <path>.
|
||||
- Do not carry or summarize previous chat. Previous stage context is on disk in the declared input artifacts.
|
||||
- Write evidence to the declared output path(s). Do not return code in chat.
|
||||
- NEVER edit backlog/features.json directly. Use scripts/close_feature.py to close features.
|
||||
- Before finishing, save to Engram only if this stage produced durable knowledge: a decision, bugfix, non-obvious discovery, reusable convention, or configuration change. Do not save routine progress, command output, or artifact summaries; those belong in the output artifact.
|
||||
- Finish by updating runtime status to done or blocked.
|
||||
- Final response must be exactly: done -> <path> or blocked -> <path>.
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Run one Orquestra stage in a fresh Pi process with minimal declared context.')
|
||||
parser.add_argument('stage')
|
||||
parser.add_argument('--feature-id', required=True)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
stages = parse_stages()
|
||||
if args.stage not in stages:
|
||||
print(f"[FAIL] Unknown stage: {args.stage}", file=sys.stderr)
|
||||
print(f"Allowed: {', '.join(stages)}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
cmd = [
|
||||
'pi', '--no-session', '--no-context-files', '--no-extensions', '--no-skills',
|
||||
'-e', str(ENGRAM_EXT), '-e', str(STATUS_EXT), '-e', str(WEB_FETCH_EXT),
|
||||
'-p', prompt_for(stages[args.stage], args.feature_id),
|
||||
]
|
||||
if args.dry_run:
|
||||
print(' '.join(cmd[:-1]))
|
||||
print('\n--- prompt ---')
|
||||
print(cmd[-1])
|
||||
return 0
|
||||
|
||||
missing = []
|
||||
if not ENGRAM_EXT.is_file():
|
||||
missing.append(str(ENGRAM_EXT))
|
||||
if not (STATUS_EXT / 'index.ts').is_file():
|
||||
missing.append(str(STATUS_EXT / 'index.ts'))
|
||||
if not WEB_FETCH_EXT.is_file():
|
||||
missing.append(str(WEB_FETCH_EXT))
|
||||
if missing:
|
||||
print('[FAIL] Missing Pi runtime extension(s):', file=sys.stderr)
|
||||
for path in missing:
|
||||
print(f' - {path}', file=sys.stderr)
|
||||
print('Run scripts/install.sh for this project and install gentle-engram.', file=sys.stderr)
|
||||
return 1
|
||||
return subprocess.run(cmd, cwd=ROOT, env=os.environ.copy()).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -23,12 +23,25 @@ required=(
|
||||
"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"
|
||||
"platforms/pi/README.md"
|
||||
"platforms/pi/extensions/orquestra-status/index.ts"
|
||||
"platforms/pi/extensions/orquestra-web-fetch.ts"
|
||||
)
|
||||
|
||||
for f in "${required[@]}"; do
|
||||
@@ -40,8 +53,68 @@ for f in "${required[@]}"; do
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
echo ""
|
||||
echo "── 2) Validando backlog + gates ───────────────────────"
|
||||
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
|
||||
@@ -102,12 +175,29 @@ for f in features:
|
||||
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)
|
||||
|
||||
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 "── 3) Verificación de tests/build (opcional auto-detect) ─"
|
||||
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
|
||||
@@ -123,15 +213,15 @@ elif [ -d "tests" ]; then
|
||||
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)"
|
||||
ok "Sin suite de producto detectada; no aplica al harness base"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── 4) Resumen ─────────────────────────────────────────"
|
||||
echo "── 5) Resumen ─────────────────────────────────────────"
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
ok "Harness verificado. Puedes trabajar."
|
||||
ok "Orquestra verificado. Puedes trabajar."
|
||||
else
|
||||
fail "Harness NO verificado. Corrige antes de continuar."
|
||||
fail "Orquestra NO verificado. Corrige antes de continuar."
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
|
||||
138
work/runtime-status.json
Normal file
138
work/runtime-status.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"feature_id": "F-001",
|
||||
"stage": "intake",
|
||||
"agent": "leader",
|
||||
"action": "Gate validation reforzado: agent_status.py bloquea close sin gates + close_feature.py valida antes de actualizar backlog",
|
||||
"state": "done",
|
||||
"next_agent": "leader",
|
||||
"waiting_for": "Probar en producción",
|
||||
"updated_at": "2026-08-17T05:25:08Z",
|
||||
"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