From 4826c103a21b5741c5955ba2c2c89def5a4e489d Mon Sep 17 00:00:00 2001 From: pi Date: Sun, 12 Apr 2026 13:26:58 +0100 Subject: [PATCH] Restore preset-owned prompt/tool artifacts in wrapper path: add systemPromptPath artifact, write system-prompt.md, pass --tools and --append-system-prompt flags when present, preserve meta fields, and update tests --- src/artifacts.test.ts | 6 ++- src/artifacts.ts | 8 ++++ src/wrapper/cli.mjs | 22 +++++++++++ src/wrapper/cli.test.ts | 83 ++++++++++++++++++++++++++++++++++++++++- src/wrapper/render.mjs | 13 ++++++- 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/artifacts.test.ts b/src/artifacts.test.ts index e2143fe..87a1385 100644 --- a/src/artifacts.test.ts +++ b/src/artifacts.test.ts @@ -17,6 +17,8 @@ test("createRunArtifacts writes metadata and reserves stable artifact paths", as assert.match(artifacts.dir, /\.pi\/subagents\/runs\/run-1$/); const meta = JSON.parse(await readFile(artifacts.metaPath, "utf8")); assert.equal(meta.task, "inspect auth"); - assert.equal("systemPromptPath" in meta, false); - await assert.rejects(readFile(join(artifacts.dir, "system-prompt.md"), "utf8")); + // systemPromptPath should be present and point to the system-prompt.md file we created. + assert.equal(typeof meta.systemPromptPath, "string"); + const promptText = await readFile(join(artifacts.dir, "system-prompt.md"), "utf8"); + assert.equal(promptText, ""); }); diff --git a/src/artifacts.ts b/src/artifacts.ts index 0dbe3ce..1e1b560 100644 --- a/src/artifacts.ts +++ b/src/artifacts.ts @@ -12,6 +12,7 @@ export interface RunArtifacts { stderrPath: string; transcriptPath: string; sessionPath: string; + systemPromptPath: string; } export async function createRunArtifacts( @@ -32,8 +33,14 @@ export async function createRunArtifacts( stderrPath: join(dir, "stderr.log"), transcriptPath: join(dir, "transcript.log"), sessionPath: join(dir, "child-session.jsonl"), + systemPromptPath: join(dir, "system-prompt.md"), }; + // Write the system prompt file. If meta.systemPrompt is missing, write an empty string + // to keep the path stable for downstream consumers. + const systemPromptContent = typeof meta.systemPrompt === "string" ? meta.systemPrompt : ""; + await writeFile(artifacts.systemPromptPath, systemPromptContent, "utf8"); + await writeFile( artifacts.metaPath, JSON.stringify( @@ -46,6 +53,7 @@ export async function createRunArtifacts( stdoutPath: artifacts.stdoutPath, stderrPath: artifacts.stderrPath, transcriptPath: artifacts.transcriptPath, + systemPromptPath: artifacts.systemPromptPath, }, null, 2, diff --git a/src/wrapper/cli.mjs b/src/wrapper/cli.mjs index 8314074..f838ac0 100644 --- a/src/wrapper/cli.mjs +++ b/src/wrapper/cli.mjs @@ -67,6 +67,28 @@ async function runWrapper(meta, startedAt) { const args = ["--mode", "json", "--session", meta.sessionPath]; if (effectiveModel) args.push("--model", effectiveModel); + + // Pass preset-owned tool list to the child pi process when available. + if (Array.isArray(meta.tools) && meta.tools.length > 0) { + try { + const csv = meta.tools.join(","); + args.push("--tools", csv); + } catch { + // ignore malformed tools metadata + } + } + + // If a system prompt artifact path is present and the file exists, pass it along. + if (typeof meta.systemPromptPath === "string" && meta.systemPromptPath.length > 0) { + try { + // Verify file exists/readable. Do not fail if it's stale/missing. + await readFile(meta.systemPromptPath, "utf8"); + args.push("--append-system-prompt", meta.systemPromptPath); + } catch { + // ignore stale/missing prompt file + } + } + args.push(meta.task); let finalText = ""; diff --git a/src/wrapper/cli.test.ts b/src/wrapper/cli.test.ts index bd69bd1..192a0b6 100644 --- a/src/wrapper/cli.test.ts +++ b/src/wrapper/cli.test.ts @@ -107,12 +107,91 @@ test("wrapper marks anthropic child run as a subagent child", async () => { assert.equal(captured.flags.PI_SUBAGENTS_CHILD, "1"); }); -test("wrapper ignores stale tool and system prompt metadata", async () => { +test("wrapper respects tools metadata but ignores stale system prompt file", async () => { const captured = await runWrapperWithFakePi("anthropic/claude-sonnet-4-5"); - assert.equal(captured.flags.argv.includes("--tools"), false); + // tools metadata should be passed even if there are no tool binaries installed. + assert.equal(captured.flags.argv.includes("--tools"), true); + // system prompt file is not present in this test, so the flag should not be passed. assert.equal(captured.flags.argv.includes("--append-system-prompt"), false); }); +test("wrapper passes --append-system-prompt when the prompt file exists", async () => { + const dir = await mkdtemp(join(tmpdir(), "pi-subagents-wrapper-")); + const metaPath = join(dir, "meta.json"); + const resultPath = join(dir, "result.json"); + const capturePath = join(dir, "capture.json"); + const piPath = join(dir, "pi"); + const promptPath = join(dir, "system-prompt.md"); + + await writeFile( + piPath, + [ + `#!${process.execPath}`, + "const fs = require('fs');", + `const capturePath = ${JSON.stringify(capturePath)};`, + "const obj = {", + " PI_SUBAGENTS_GITHUB_COPILOT_INITIATOR: process.env.PI_SUBAGENTS_GITHUB_COPILOT_INITIATOR || '',", + " PI_SUBAGENTS_CHILD: process.env.PI_SUBAGENTS_CHILD || '',", + " argv: process.argv.slice(2)", + "};", + "fs.writeFileSync(capturePath, JSON.stringify(obj), 'utf8');", + "console.log(JSON.stringify({type:'message_end',message:{role:'assistant',content:[{type:'text',text:'done'}],model:'github-copilot/gpt-4o',stopReason:'stop'}}));", + ].join("\n"), + "utf8", + ); + await chmod(piPath, 0o755); + + // create the system prompt file so the wrapper will pass --append-system-prompt + await writeFile(promptPath, "System prompt here", "utf8"); + + await writeFile( + metaPath, + JSON.stringify( + { + runId: "run-1", + mode: "single", + task: "inspect auth", + cwd: dir, + requestedModel: "anthropic/claude-sonnet-4-5", + resolvedModel: "anthropic/claude-sonnet-4-5", + startedAt: "2026-04-09T00:00:00.000Z", + sessionPath: join(dir, "child-session.jsonl"), + eventsPath: join(dir, "events.jsonl"), + resultPath, + stdoutPath: join(dir, "stdout.log"), + stderrPath: join(dir, "stderr.log"), + transcriptPath: join(dir, "transcript.log"), + tools: ["read", "grep"], + systemPromptPath: promptPath, + }, + null, + 2, + ), + "utf8", + ); + + const wrapperPath = join(dirname(fileURLToPath(import.meta.url)), "cli.mjs"); + const child = spawn(process.execPath, [wrapperPath, metaPath], { + env: { + ...process.env, + PATH: dir, + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + const exitCode = await waitForExit(child); + assert.equal(exitCode, 0); + + const captureJson = JSON.parse(await readFile(capturePath, "utf8")); + const argv = captureJson.argv; + assert.equal(argv.includes("--tools"), true); + const toolsIndex = argv.indexOf("--tools"); + assert.equal(argv[toolsIndex + 1], "read,grep"); + assert.equal(argv.includes("--append-system-prompt"), true); + const idx = argv.indexOf("--append-system-prompt"); + assert.equal(argv[idx + 1], promptPath); +}); + test("wrapper marks github-copilot child runs as agent-initiated", async () => { const captured = await runWrapperWithFakePi("github-copilot/gpt-4o"); assert.equal(captured.flags.PI_SUBAGENTS_GITHUB_COPILOT_INITIATOR, "agent"); diff --git a/src/wrapper/render.mjs b/src/wrapper/render.mjs index 4df3420..4c08987 100644 --- a/src/wrapper/render.mjs +++ b/src/wrapper/render.mjs @@ -1,15 +1,24 @@ import { createProgressFormatter } from "../progress.mjs"; export function renderHeader(meta) { - return [ + const lines = [ "=== subagent ===", `Task: ${meta.task}`, + ]; + + if (meta.preset) { + lines.push(`Preset: ${meta.preset}`); + } + + lines.push( `CWD: ${meta.cwd}`, `Requested model: ${meta.requestedModel ?? "(default)"}`, `Resolved model: ${meta.resolvedModel ?? "(pending)"}`, `Session: ${meta.sessionPath}`, "---------------------", - ].join("\n"); + ); + + return lines.join("\n"); } export function createEventLineRenderer() {