feat(ADM-018): completed feature
This commit is contained in:
@@ -22,13 +22,14 @@ No debe crear `.pi/subagents/` ni `.pi/subagents.json`.
|
||||
- `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.
|
||||
- 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: `python3 scripts/run_stage.py <stage> --feature-id <feature_id>`.
|
||||
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`.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -50,6 +51,19 @@ type RuntimeStatus = {
|
||||
|
||||
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;
|
||||
@@ -165,12 +179,50 @@ function hasActiveBuildRuntime(status: RuntimeStatus | null): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
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.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Bloquear escrituras directas a backlog/features.json (debe usar close_feature.py)
|
||||
if (relPath === "backlog/features.json") {
|
||||
return {
|
||||
block: true,
|
||||
reason: `Orquestra bloqueó '${relPath}': NUNCA edites backlog/features.json directamente. Usa scripts/close_feature.py para cerrar features. Este script valida gates y actualiza el backlog automáticamente.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (isRootProductFile(relPath)) {
|
||||
return {
|
||||
block: true,
|
||||
@@ -285,4 +337,34 @@ export default function orquestraStatusExtension(pi: ExtensionAPI) {
|
||||
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");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user