feat(ADM-018): completed feature
This commit is contained in:
@@ -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