feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -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`.

View File

@@ -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");
}
},
});
}