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

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