chore(harness): isolate pi stage execution
This commit is contained in:
@@ -7,6 +7,7 @@ const MATRIX_FILE = "harness/agents.matrix.yml";
|
|||||||
const ARTIFACTS_DIR = "work/artifacts";
|
const ARTIFACTS_DIR = "work/artifacts";
|
||||||
const WIDGET_KEY = "orquestra-runtime";
|
const WIDGET_KEY = "orquestra-runtime";
|
||||||
const STATUS_KEY = "orquestra-runtime";
|
const STATUS_KEY = "orquestra-runtime";
|
||||||
|
const STATUS_GAP = " ";
|
||||||
|
|
||||||
const DEFAULT_EMOJIS: Record<string, string> = {
|
const DEFAULT_EMOJIS: Record<string, string> = {
|
||||||
leader: "🧭",
|
leader: "🧭",
|
||||||
@@ -100,8 +101,8 @@ function gateSummary(root: string, featureId: string | null | undefined, emojis:
|
|||||||
const gates = gateState(root, featureId);
|
const gates = gateState(root, featureId);
|
||||||
if (!featureId) return "Gates: —";
|
if (!featureId) return "Gates: —";
|
||||||
return `Gates: ${["reviewer", "security", "qa", "leader"]
|
return `Gates: ${["reviewer", "security", "qa", "leader"]
|
||||||
.map((gate) => `${emojis[gate] || "•"}${gates[gate] === "APPROVED" ? "✅" : gates[gate] === "PRESENT" ? "⚠️" : "⏳"}`)
|
.map((gate) => `${emojis[gate] || "•"} ${gates[gate] === "APPROVED" ? "✅" : gates[gate] === "PRESENT" ? "⚠️" : "⏳"}`)
|
||||||
.join(" ")}`;
|
.join(STATUS_GAP)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function artifactStatus(root: string, featureId: string | null | undefined, agent: string): string {
|
function artifactStatus(root: string, featureId: string | null | undefined, agent: string): string {
|
||||||
@@ -127,9 +128,9 @@ function agentSummary(root: string, status: RuntimeStatus, emojis: Record<string
|
|||||||
.map((agent) => {
|
.map((agent) => {
|
||||||
const state = artifactStatus(root, featureId, agent);
|
const state = artifactStatus(root, featureId, agent);
|
||||||
const mark = state === "DONE" ? "✅" : state === "PRESENT" ? "⚠️" : agent === currentAgent ? "▶️" : "⏳";
|
const mark = state === "DONE" ? "✅" : state === "PRESENT" ? "⚠️" : agent === currentAgent ? "▶️" : "⏳";
|
||||||
return `${emojis[agent] || "•"}${mark}`;
|
return `${emojis[agent] || "•"} ${mark}`;
|
||||||
})
|
})
|
||||||
.join(" ")}`;
|
.join(STATUS_GAP)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsideRel(relPath: string, dirname: string): boolean {
|
function isInsideRel(relPath: string, dirname: string): boolean {
|
||||||
@@ -229,7 +230,6 @@ function render(root: string, status: RuntimeStatus | null, emojis: Record<strin
|
|||||||
export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
||||||
let currentCtx: ExtensionContext | null = null;
|
let currentCtx: ExtensionContext | null = null;
|
||||||
let watching = false;
|
let watching = false;
|
||||||
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
||||||
let lastRendered = "";
|
let lastRendered = "";
|
||||||
|
|
||||||
function apply(ctx: ExtensionContext) {
|
function apply(ctx: ExtensionContext) {
|
||||||
@@ -251,16 +251,13 @@ export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
|||||||
function startWatching() {
|
function startWatching() {
|
||||||
if (watching) return;
|
if (watching) return;
|
||||||
if (!currentCtx) return;
|
if (!currentCtx) return;
|
||||||
watchFile(join(currentCtx.cwd, STATUS_FILE), { interval: 1000 }, () => refresh());
|
watchFile(join(currentCtx.cwd, STATUS_FILE), { interval: 1000, persistent: false }, () => refresh());
|
||||||
refreshTimer = setInterval(refresh, 1000);
|
|
||||||
watching = true;
|
watching = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopWatching() {
|
function stopWatching() {
|
||||||
if (!watching) return;
|
if (!watching) return;
|
||||||
if (currentCtx) unwatchFile(join(currentCtx.cwd, STATUS_FILE));
|
if (currentCtx) unwatchFile(join(currentCtx.cwd, STATUS_FILE));
|
||||||
if (refreshTimer) clearInterval(refreshTimer);
|
|
||||||
refreshTimer = null;
|
|
||||||
watching = false;
|
watching = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ Los agentes **no** devuelven código en chat. Deben escribir a disco y responder
|
|||||||
- `done -> <ruta>`
|
- `done -> <ruta>`
|
||||||
- `blocked -> <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
|
## Rutas estándar por feature
|
||||||
- `work/artifacts/<feature_id>/implementer.md`
|
- `work/artifacts/<feature_id>/implementer.md`
|
||||||
- `work/artifacts/<feature_id>/reviewer.json`
|
- `work/artifacts/<feature_id>/reviewer.json`
|
||||||
|
|||||||
@@ -15,29 +15,54 @@ stages:
|
|||||||
- name: design
|
- name: design
|
||||||
owner: architect
|
owner: architect
|
||||||
optional: true
|
optional: true
|
||||||
|
input:
|
||||||
|
- work/current.md
|
||||||
|
- spec/product.md
|
||||||
|
- spec/tech.md
|
||||||
|
- spec/acceptance.md
|
||||||
output:
|
output:
|
||||||
- work/artifacts/<feature_id>/architect.md
|
- work/artifacts/<feature_id>/architect.md
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
owner: implementer
|
owner: implementer
|
||||||
|
input:
|
||||||
|
- work/current.md
|
||||||
|
- spec/product.md
|
||||||
|
- spec/tech.md
|
||||||
|
- spec/acceptance.md
|
||||||
|
- work/artifacts/<feature_id>/architect.md
|
||||||
output:
|
output:
|
||||||
- work/artifacts/<feature_id>/implementer.md
|
- work/artifacts/<feature_id>/implementer.md
|
||||||
|
|
||||||
- name: review_gate
|
- name: review_gate
|
||||||
owner: reviewer
|
owner: reviewer
|
||||||
required: true
|
required: true
|
||||||
|
input:
|
||||||
|
- work/current.md
|
||||||
|
- spec/acceptance.md
|
||||||
|
- work/artifacts/<feature_id>/implementer.md
|
||||||
output:
|
output:
|
||||||
- work/artifacts/<feature_id>/reviewer.json
|
- work/artifacts/<feature_id>/reviewer.json
|
||||||
|
|
||||||
- name: security_gate
|
- name: security_gate
|
||||||
owner: security
|
owner: security
|
||||||
required: true
|
required: true
|
||||||
|
input:
|
||||||
|
- work/current.md
|
||||||
|
- work/artifacts/<feature_id>/implementer.md
|
||||||
|
- work/artifacts/<feature_id>/reviewer.json
|
||||||
output:
|
output:
|
||||||
- work/artifacts/<feature_id>/security.json
|
- work/artifacts/<feature_id>/security.json
|
||||||
|
|
||||||
- name: qa_gate
|
- name: qa_gate
|
||||||
owner: qa
|
owner: qa
|
||||||
required: true
|
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:
|
output:
|
||||||
- work/artifacts/<feature_id>/qa.json
|
- work/artifacts/<feature_id>/qa.json
|
||||||
|
|
||||||
@@ -45,12 +70,25 @@ stages:
|
|||||||
owner: documenter
|
owner: documenter
|
||||||
optional: true
|
optional: true
|
||||||
when: docs/API/contracts/user-facing behavior changed
|
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:
|
output:
|
||||||
- work/artifacts/<feature_id>/documenter.md
|
- work/artifacts/<feature_id>/documenter.md
|
||||||
|
|
||||||
- name: close
|
- name: close
|
||||||
owner: leader
|
owner: leader
|
||||||
required: true
|
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:
|
output:
|
||||||
- work/artifacts/<feature_id>/leader-close.json
|
- work/artifacts/<feature_id>/leader-close.json
|
||||||
- work/history.md
|
- work/history.md
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ Orquestra se ejecuta desde Pi como **un solo parent session secuencial**. No ins
|
|||||||
|
|
||||||
## Requisitos obligatorios
|
## Requisitos obligatorios
|
||||||
- `pi` debe existir en `PATH` antes de instalar Orquestra.
|
- `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.
|
- El proyecto instalado debe abrirse desde su raíz.
|
||||||
- Arrancar con `./scripts/pi_orquestra.sh`, que ejecuta `pi --no-extensions` y carga solo extensiones Orquestra.
|
- 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`.
|
- 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.
|
- El código de producto vive en `project/`; archivos de código en la raíz son inválidos.
|
||||||
|
|
||||||
@@ -16,12 +17,19 @@ Cuando Orquestra se instala en un repo de proyecto, el instalador debe copiar:
|
|||||||
|
|
||||||
No debe crear `.pi/subagents/` ni `.pi/subagents.json`.
|
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, ejecutar cada stage con `python3 scripts/run_stage.py <stage> --feature-id <feature_id>`; usa `pi --no-session --no-context-files` y carga solo Engram + extensiones Orquestra.
|
||||||
|
|
||||||
## Flujo secuencial
|
## Flujo secuencial
|
||||||
1. Ejecutar `./scripts/verify.sh`.
|
1. Ejecutar `./scripts/verify.sh`.
|
||||||
2. Abrir Pi limpio desde la raíz con `./scripts/pi_orquestra.sh`.
|
2. Abrir Pi limpio desde la raíz con `./scripts/pi_orquestra.sh`.
|
||||||
3. Confirmar el widget con `/orquestra-status`.
|
3. Confirmar el widget con `/orquestra-status`.
|
||||||
4. El mismo parent session cambia de rol siguiendo `harness/workflow.stages.yml`.
|
4. Ejecutar cada stage como proceso fresco: `python3 scripts/run_stage.py <stage> --feature-id <feature_id>`.
|
||||||
5. Antes de cada stage, actualizar estado con `python3 scripts/agent_status.py set ...`.
|
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.
|
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`.
|
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>/`.
|
8. Al terminar cada stage, escribir el artefacto esperado en `work/artifacts/<feature_id>/`.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const MATRIX_FILE = "harness/agents.matrix.yml";
|
|||||||
const ARTIFACTS_DIR = "work/artifacts";
|
const ARTIFACTS_DIR = "work/artifacts";
|
||||||
const WIDGET_KEY = "orquestra-runtime";
|
const WIDGET_KEY = "orquestra-runtime";
|
||||||
const STATUS_KEY = "orquestra-runtime";
|
const STATUS_KEY = "orquestra-runtime";
|
||||||
|
const STATUS_GAP = " ";
|
||||||
|
|
||||||
const DEFAULT_EMOJIS: Record<string, string> = {
|
const DEFAULT_EMOJIS: Record<string, string> = {
|
||||||
leader: "🧭",
|
leader: "🧭",
|
||||||
@@ -100,8 +101,8 @@ function gateSummary(root: string, featureId: string | null | undefined, emojis:
|
|||||||
const gates = gateState(root, featureId);
|
const gates = gateState(root, featureId);
|
||||||
if (!featureId) return "Gates: —";
|
if (!featureId) return "Gates: —";
|
||||||
return `Gates: ${["reviewer", "security", "qa", "leader"]
|
return `Gates: ${["reviewer", "security", "qa", "leader"]
|
||||||
.map((gate) => `${emojis[gate] || "•"}${gates[gate] === "APPROVED" ? "✅" : gates[gate] === "PRESENT" ? "⚠️" : "⏳"}`)
|
.map((gate) => `${emojis[gate] || "•"} ${gates[gate] === "APPROVED" ? "✅" : gates[gate] === "PRESENT" ? "⚠️" : "⏳"}`)
|
||||||
.join(" ")}`;
|
.join(STATUS_GAP)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function artifactStatus(root: string, featureId: string | null | undefined, agent: string): string {
|
function artifactStatus(root: string, featureId: string | null | undefined, agent: string): string {
|
||||||
@@ -127,9 +128,9 @@ function agentSummary(root: string, status: RuntimeStatus, emojis: Record<string
|
|||||||
.map((agent) => {
|
.map((agent) => {
|
||||||
const state = artifactStatus(root, featureId, agent);
|
const state = artifactStatus(root, featureId, agent);
|
||||||
const mark = state === "DONE" ? "✅" : state === "PRESENT" ? "⚠️" : agent === currentAgent ? "▶️" : "⏳";
|
const mark = state === "DONE" ? "✅" : state === "PRESENT" ? "⚠️" : agent === currentAgent ? "▶️" : "⏳";
|
||||||
return `${emojis[agent] || "•"}${mark}`;
|
return `${emojis[agent] || "•"} ${mark}`;
|
||||||
})
|
})
|
||||||
.join(" ")}`;
|
.join(STATUS_GAP)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsideRel(relPath: string, dirname: string): boolean {
|
function isInsideRel(relPath: string, dirname: string): boolean {
|
||||||
@@ -229,7 +230,6 @@ function render(root: string, status: RuntimeStatus | null, emojis: Record<strin
|
|||||||
export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
||||||
let currentCtx: ExtensionContext | null = null;
|
let currentCtx: ExtensionContext | null = null;
|
||||||
let watching = false;
|
let watching = false;
|
||||||
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
||||||
let lastRendered = "";
|
let lastRendered = "";
|
||||||
|
|
||||||
function apply(ctx: ExtensionContext) {
|
function apply(ctx: ExtensionContext) {
|
||||||
@@ -251,16 +251,13 @@ export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
|||||||
function startWatching() {
|
function startWatching() {
|
||||||
if (watching) return;
|
if (watching) return;
|
||||||
if (!currentCtx) return;
|
if (!currentCtx) return;
|
||||||
watchFile(join(currentCtx.cwd, STATUS_FILE), { interval: 1000 }, () => refresh());
|
watchFile(join(currentCtx.cwd, STATUS_FILE), { interval: 1000, persistent: false }, () => refresh());
|
||||||
refreshTimer = setInterval(refresh, 1000);
|
|
||||||
watching = true;
|
watching = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopWatching() {
|
function stopWatching() {
|
||||||
if (!watching) return;
|
if (!watching) return;
|
||||||
if (currentCtx) unwatchFile(join(currentCtx.cwd, STATUS_FILE));
|
if (currentCtx) unwatchFile(join(currentCtx.cwd, STATUS_FILE));
|
||||||
if (refreshTimer) clearInterval(refreshTimer);
|
|
||||||
refreshTimer = null;
|
|
||||||
watching = false;
|
watching = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ done
|
|||||||
copy_dir_update "$SOURCE_DIR/harness" "$TARGET_DIR/harness"
|
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/platforms/pi" "$TARGET_DIR/platforms/pi"
|
||||||
|
|
||||||
for file in scripts/verify.sh scripts/agent_status.py scripts/new_ticket.py scripts/pi_orquestra.sh scripts/install.sh; do
|
for file in scripts/verify.sh scripts/agent_status.py scripts/new_ticket.py scripts/pi_orquestra.sh scripts/run_stage.py scripts/install.sh; do
|
||||||
copy_file_update "$SOURCE_DIR/$file" "$TARGET_DIR/$file"
|
copy_file_update "$SOURCE_DIR/$file" "$TARGET_DIR/$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ if [ -d "$TARGET_DIR/.pi/subagents" ] || [ -f "$TARGET_DIR/.pi/subagents.json" ]
|
|||||||
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."
|
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
|
fi
|
||||||
|
|
||||||
chmod +x "$TARGET_DIR/scripts/verify.sh" "$TARGET_DIR/scripts/pi_orquestra.sh" "$TARGET_DIR/scripts/install.sh"
|
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
|
cat <<EOF
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ fi
|
|||||||
|
|
||||||
STATUS_EXT=".pi/extensions/orquestra-status"
|
STATUS_EXT=".pi/extensions/orquestra-status"
|
||||||
WEB_FETCH_EXT=".pi/extensions/orquestra-web-fetch.ts"
|
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
|
if [ ! -f "$STATUS_EXT/index.ts" ] || [ ! -f "$WEB_FETCH_EXT" ]; then
|
||||||
echo "[FAIL] Orquestra Pi extensions are not installed in this project." >&2
|
echo "[FAIL] Orquestra Pi extensions are not installed in this project." >&2
|
||||||
@@ -18,4 +19,10 @@ if [ ! -f "$STATUS_EXT/index.ts" ] || [ ! -f "$WEB_FETCH_EXT" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec pi --no-extensions --no-skills -e "$STATUS_EXT" -e "$WEB_FETCH_EXT" "$@"
|
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" "$@"
|
||||||
|
|||||||
117
scripts/run_stage.py
Executable file
117
scripts/run_stage.py
Executable file
@@ -0,0 +1,117 @@
|
|||||||
|
#!/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': []}
|
||||||
|
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 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)
|
||||||
|
owner = stage.get('owner', 'leader')
|
||||||
|
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'}
|
||||||
|
|
||||||
|
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.
|
||||||
|
- 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())
|
||||||
@@ -35,6 +35,7 @@ required=(
|
|||||||
"scripts/agent_status.py"
|
"scripts/agent_status.py"
|
||||||
"scripts/new_ticket.py"
|
"scripts/new_ticket.py"
|
||||||
"scripts/pi_orquestra.sh"
|
"scripts/pi_orquestra.sh"
|
||||||
|
"scripts/run_stage.py"
|
||||||
"platforms/pi/README.md"
|
"platforms/pi/README.md"
|
||||||
"platforms/pi/extensions/orquestra-status/index.ts"
|
"platforms/pi/extensions/orquestra-status/index.ts"
|
||||||
"platforms/pi/extensions/orquestra-web-fetch.ts"
|
"platforms/pi/extensions/orquestra-web-fetch.ts"
|
||||||
@@ -79,6 +80,14 @@ else
|
|||||||
EXIT_CODE=1
|
EXIT_CODE=1
|
||||||
fi
|
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
|
if [ -d ".pi/subagents" ] || [ -f ".pi/subagents.json" ]; then
|
||||||
fail "Orquestra es secuencial: no debe instalar .pi/subagents ni .pi/subagents.json"
|
fail "Orquestra es secuencial: no debe instalar .pi/subagents ni .pi/subagents.json"
|
||||||
EXIT_CODE=1
|
EXIT_CODE=1
|
||||||
|
|||||||
Reference in New Issue
Block a user