import { appendFile, readFile, writeFile } from "node:fs/promises"; import { spawn } from "node:child_process"; import { normalizePiEvent } from "./normalize.mjs"; import { renderHeader, renderEventLine } from "./render.mjs"; async function appendJsonLine(path, value) { await appendBestEffort(path, `${JSON.stringify(value)}\n`); } async function appendBestEffort(path, text) { try { await appendFile(path, text, "utf8"); } catch { // Best-effort artifact logging should never prevent result.json from being written. } } function makeResult(meta, startedAt, input = {}) { const errorText = typeof input.errorMessage === "string" ? input.errorMessage.trim() : ""; const exitCode = typeof input.exitCode === "number" ? input.exitCode : 1; return { runId: meta.runId, mode: meta.mode, taskIndex: meta.taskIndex, step: meta.step, agent: meta.agent, agentSource: meta.agentSource, task: meta.task, cwd: meta.cwd, requestedModel: meta.requestedModel, resolvedModel: input.resolvedModel ?? meta.resolvedModel, sessionPath: meta.sessionPath, startedAt, finishedAt: new Date().toISOString(), exitCode, stopReason: input.stopReason ?? (exitCode === 0 ? undefined : "error"), finalText: input.finalText ?? "", usage: input.usage, stdoutPath: meta.stdoutPath, stderrPath: meta.stderrPath, resultPath: meta.resultPath, eventsPath: meta.eventsPath, transcriptPath: meta.transcriptPath, errorMessage: errorText || undefined, }; } async function runWrapper(meta, startedAt) { const header = renderHeader(meta); await appendBestEffort(meta.transcriptPath, `${header}\n`); console.log(header); const effectiveModel = typeof meta.resolvedModel === "string" && meta.resolvedModel.length > 0 ? meta.resolvedModel : meta.requestedModel; const args = ["--mode", "json", "--session", meta.sessionPath]; if (effectiveModel) args.push("--model", effectiveModel); if (Array.isArray(meta.tools) && meta.tools.length > 0) args.push("--tools", meta.tools.join(",")); if (meta.systemPromptPath) args.push("--append-system-prompt", meta.systemPromptPath); args.push(meta.task); let finalText = ""; let resolvedModel = meta.resolvedModel; let stopReason; let usage = undefined; let stdoutBuffer = ""; let stderrText = ""; let spawnError; let queue = Promise.resolve(); const enqueue = (work) => { queue = queue.then(work, work); return queue; }; const handleStdoutLine = async (line) => { if (!line.trim()) return; let parsed; try { parsed = JSON.parse(line); } catch { return; } const normalized = normalizePiEvent(parsed); if (!normalized) return; await appendJsonLine(meta.eventsPath, normalized); const rendered = renderEventLine(normalized); await appendBestEffort(meta.transcriptPath, `${rendered}\n`); console.log(rendered); if (normalized.type === "assistant_text") { finalText = normalized.text; resolvedModel = normalized.model ?? resolvedModel; stopReason = normalized.stopReason ?? stopReason; usage = normalized.usage ?? usage; } }; const childEnv = { ...process.env }; // Ensure the copilot initiator flag is not accidentally inherited from the parent // environment; set it only for github-copilot models. delete childEnv.PI_TMUX_SUBAGENT_GITHUB_COPILOT_INITIATOR; // Mark every child run as a nested tmux subagent so it cannot spawn further subagents. childEnv.PI_TMUX_SUBAGENT_CHILD = "1"; if (typeof effectiveModel === "string" && effectiveModel.startsWith("github-copilot/")) { childEnv.PI_TMUX_SUBAGENT_GITHUB_COPILOT_INITIATOR = "agent"; } const child = spawn("pi", args, { cwd: meta.cwd, env: childEnv, stdio: ["ignore", "pipe", "pipe"], }); child.stdout.on("data", (chunk) => { const text = chunk.toString(); enqueue(async () => { stdoutBuffer += text; await appendBestEffort(meta.stdoutPath, text); const lines = stdoutBuffer.split("\n"); stdoutBuffer = lines.pop() ?? ""; for (const line of lines) { await handleStdoutLine(line); } }); }); child.stderr.on("data", (chunk) => { const text = chunk.toString(); enqueue(async () => { stderrText += text; await appendBestEffort(meta.stderrPath, text); }); }); const exitCode = await new Promise((resolve) => { let done = false; const finish = (code) => { if (done) return; done = true; resolve(code); }; child.on("error", (error) => { spawnError = error; finish(1); }); child.on("close", (code) => { finish(code ?? (spawnError ? 1 : 0)); }); }); await queue; if (stdoutBuffer.trim()) { await handleStdoutLine(stdoutBuffer); stdoutBuffer = ""; } if (spawnError) { const message = spawnError instanceof Error ? spawnError.stack ?? spawnError.message : String(spawnError); if (!stderrText.trim()) { stderrText = message; await appendBestEffort(meta.stderrPath, `${message}\n`); } } return makeResult(meta, startedAt, { exitCode, stopReason, finalText, usage, resolvedModel, errorMessage: stderrText, }); } async function main() { const metaPath = process.argv[2]; if (!metaPath) throw new Error("Expected meta.json path as argv[2]"); const meta = JSON.parse(await readFile(metaPath, "utf8")); const startedAt = meta.startedAt ?? new Date().toISOString(); let result; try { result = await runWrapper(meta, startedAt); } catch (error) { const message = error instanceof Error ? error.stack ?? error.message : String(error); await appendBestEffort(meta.stderrPath, `${message}\n`); result = makeResult(meta, startedAt, { exitCode: 1, stopReason: "error", errorMessage: message, }); } await writeFile(meta.resultPath, JSON.stringify(result, null, 2), "utf8"); if (result.exitCode !== 0) process.exitCode = result.exitCode; } main().catch((error) => { console.error(error instanceof Error ? error.stack : String(error)); process.exitCode = 1; });