From 1b53e3b7f1f87083698d98c5b3606b65160bab81 Mon Sep 17 00:00:00 2001 From: Advait Paliwal Date: Fri, 17 Apr 2026 14:16:57 -0700 Subject: [PATCH] Fix Pi subagent task outputs --- README.md | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/install/install.ps1 | 2 +- scripts/install/install.sh | 2 +- scripts/lib/pi-subagents-patch.mjs | 72 ++++++++++++++++++- tests/pi-subagents-patch.test.ts | 68 ++++++++++++++++++ website/public/install | 2 +- website/public/install.ps1 | 2 +- .../docs/getting-started/installation.md | 4 +- 10 files changed, 149 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9654568..899579e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ curl -fsSL https://feynman.is/install | bash irm https://feynman.is/install.ps1 | iex ``` -The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.29`. +The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.30`. The installer downloads a standalone native bundle with its own Node.js runtime. diff --git a/package-lock.json b/package-lock.json index c861353..1b526c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@companion-ai/feynman", - "version": "0.2.29", + "version": "0.2.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@companion-ai/feynman", - "version": "0.2.29", + "version": "0.2.30", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 63b6f2e..3e1be39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@companion-ai/feynman", - "version": "0.2.29", + "version": "0.2.30", "description": "Research-first CLI agent built on Pi and alphaXiv", "license": "MIT", "type": "module", diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index da702de..6c415ed 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.29 + & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.30 "@ } diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 78584c7..be9e968 100644 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - curl -fsSL https://feynman.is/install | bash -s -- 0.2.29 + curl -fsSL https://feynman.is/install | bash -s -- 0.2.30 EOF exit 1 fi diff --git a/scripts/lib/pi-subagents-patch.mjs b/scripts/lib/pi-subagents-patch.mjs index 7230163..21cb56a 100644 --- a/scripts/lib/pi-subagents-patch.mjs +++ b/scripts/lib/pi-subagents-patch.mjs @@ -5,6 +5,8 @@ export const PI_SUBAGENTS_PATCH_TARGETS = [ "run-history.ts", "skills.ts", "chain-clarify.ts", + "subagent-executor.ts", + "schemas.ts", ]; const RESOLVE_PI_AGENT_DIR_HELPER = [ @@ -94,6 +96,11 @@ export function patchPiSubagentsSource(relativePath, source) { 'const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");', 'const configPath = path.join(resolvePiAgentDir(), "extensions", "subagent", "config.json");', ); + patched = replaceAll( + patched, + "• PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)", + "• PARALLEL: { tasks: [{agent,task,count?,output?}, ...], concurrency?: number, worktree?: true } - concurrent execution (output: per-task file target, worktree: isolate each task in a git worktree)", + ); break; case "agents.ts": patched = replaceAll( @@ -190,6 +197,69 @@ export function patchPiSubagentsSource(relativePath, source) { 'const dir = path.join(resolvePiAgentDir(), "agents");', ); break; + case "subagent-executor.ts": + patched = replaceAll( + patched, + [ + "\tcwd?: string;", + "\tcount?: number;", + "\tmodel?: string;", + "\tskill?: string | string[] | boolean;", + ].join("\n"), + [ + "\tcwd?: string;", + "\tcount?: number;", + "\tmodel?: string;", + "\tskill?: string | string[] | boolean;", + "\toutput?: string | false;", + ].join("\n"), + ); + patched = replaceAll( + patched, + [ + "\t\t\tcwd: task.cwd,", + "\t\t\t...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),", + ].join("\n"), + [ + "\t\t\tcwd: task.cwd,", + "\t\t\toutput: task.output,", + "\t\t\t...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),", + ].join("\n"), + ); + patched = replaceAll( + patched, + [ + "\t\tcwd: task.cwd,", + "\t\t...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),", + ].join("\n"), + [ + "\t\tcwd: task.cwd,", + "\t\toutput: task.output,", + "\t\t...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),", + ].join("\n"), + ); + break; + case "schemas.ts": + patched = replaceAll( + patched, + [ + "\tcwd: Type.Optional(Type.String()),", + '\tcount: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),', + '\tmodel: Type.Optional(Type.String({ description: "Override model for this task (e.g. \'google/gemini-3-pro\')" })),', + ].join("\n"), + [ + "\tcwd: Type.Optional(Type.String()),", + '\tcount: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),', + '\toutput: Type.Optional(Type.Any({ description: "Output file for this parallel task (string), or false to disable. Relative paths resolve against cwd." })),', + '\tmodel: Type.Optional(Type.String({ description: "Override model for this task (e.g. \'google/gemini-3-pro\')" })),', + ].join("\n"), + ); + patched = replaceAll( + patched, + 'tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?}, ...]" })),', + 'tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?}, ...]" })),', + ); + break; default: return source; } @@ -198,5 +268,5 @@ export function patchPiSubagentsSource(relativePath, source) { return source; } - return injectResolvePiAgentDirHelper(patched); + return patched.includes("resolvePiAgentDir()") ? injectResolvePiAgentDirHelper(patched) : patched; } diff --git a/tests/pi-subagents-patch.test.ts b/tests/pi-subagents-patch.test.ts index 2bc0db5..22fb3a1 100644 --- a/tests/pi-subagents-patch.test.ts +++ b/tests/pi-subagents-patch.test.ts @@ -141,6 +141,74 @@ test("patchPiSubagentsSource rewrites modern agents.ts discovery paths", () => { assert.ok(!patched.includes('fs.existsSync(userDirNew) ? userDirNew : userDirOld')); }); +test("patchPiSubagentsSource preserves output on top-level parallel tasks", () => { + const input = [ + "interface TaskParam {", + "\tagent: string;", + "\ttask: string;", + "\tcwd?: string;", + "\tcount?: number;", + "\tmodel?: string;", + "\tskill?: string | string[] | boolean;", + "}", + "function run(params: { tasks: TaskParam[] }) {", + "\tconst modelOverrides = params.tasks.map(() => undefined);", + "\tconst skillOverrides = params.tasks.map(() => undefined);", + "\tconst parallelTasks = params.tasks.map((task, index) => ({", + "\t\tagent: task.agent,", + "\t\ttask: params.context === \"fork\" ? wrapForkTask(task.task) : task.task,", + "\t\tcwd: task.cwd,", + "\t\t...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),", + "\t\t...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),", + "\t}));", + "}", + ].join("\n"); + + const patched = patchPiSubagentsSource("subagent-executor.ts", input); + + assert.match(patched, /output\?: string \| false;/); + assert.match(patched, /\n\t\toutput: task\.output,/); + assert.doesNotMatch(patched, /resolvePiAgentDir/); +}); + +test("patchPiSubagentsSource documents output in top-level task schema", () => { + const input = [ + "export const TaskItem = Type.Object({ ", + "\tagent: Type.String(), ", + "\ttask: Type.String(), ", + "\tcwd: Type.Optional(Type.String()),", + "\tcount: Type.Optional(Type.Integer({ minimum: 1, description: \"Repeat this parallel task N times with the same settings.\" })),", + "\tmodel: Type.Optional(Type.String({ description: \"Override model for this task (e.g. 'google/gemini-3-pro')\" })),", + "\tskill: Type.Optional(SkillOverride),", + "});", + "export const SubagentParams = Type.Object({", + "\ttasks: Type.Optional(Type.Array(TaskItem, { description: \"PARALLEL mode: [{agent, task, count?}, ...]\" })),", + "});", + ].join("\n"); + + const patched = patchPiSubagentsSource("schemas.ts", input); + + assert.match(patched, /output: Type\.Optional\(Type\.Any/); + assert.match(patched, /count\?, output\?/); + assert.doesNotMatch(patched, /resolvePiAgentDir/); +}); + +test("patchPiSubagentsSource documents output in top-level parallel help", () => { + const input = [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + "const help = `", + "• PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)", + "`;", + ].join("\n"); + + const patched = patchPiSubagentsSource("index.ts", input); + + assert.match(patched, /output\?/); + assert.match(patched, /per-task file target/); + assert.doesNotMatch(patched, /function resolvePiAgentDir/); +}); + test("stripPiSubagentBuiltinModelSource removes built-in model pins", () => { const input = [ "---", diff --git a/website/public/install b/website/public/install index 78584c7..be9e968 100644 --- a/website/public/install +++ b/website/public/install @@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - curl -fsSL https://feynman.is/install | bash -s -- 0.2.29 + curl -fsSL https://feynman.is/install | bash -s -- 0.2.30 EOF exit 1 fi diff --git a/website/public/install.ps1 b/website/public/install.ps1 index da702de..6c415ed 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.29 + & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.30 "@ } diff --git a/website/src/content/docs/getting-started/installation.md b/website/src/content/docs/getting-started/installation.md index c7898ea..66f61f1 100644 --- a/website/src/content/docs/getting-started/installation.md +++ b/website/src/content/docs/getting-started/installation.md @@ -117,13 +117,13 @@ These installers download the bundled `skills/` and `prompts/` trees plus the re The one-line installer already targets the latest tagged release. To pin an exact version, pass it explicitly: ```bash -curl -fsSL https://feynman.is/install | bash -s -- 0.2.29 +curl -fsSL https://feynman.is/install | bash -s -- 0.2.30 ``` On Windows: ```powershell -& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.29 +& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.30 ``` ## Post-install setup