2 Commits

Author SHA1 Message Date
Advait Paliwal
6f3eeea75b Fix Feynman runtime auth env 2026-04-17 15:42:30 -07:00
Advait Paliwal
1b53e3b7f1 Fix Pi subagent task outputs 2026-04-17 14:16:57 -07:00
12 changed files with 154 additions and 13 deletions

View File

@@ -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.31`.
The installer downloads a standalone native bundle with its own Node.js runtime.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@companion-ai/feynman",
"version": "0.2.29",
"version": "0.2.31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@companion-ai/feynman",
"version": "0.2.29",
"version": "0.2.31",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@companion-ai/feynman",
"version": "0.2.29",
"version": "0.2.31",
"description": "Research-first CLI agent built on Pi and alphaXiv",
"license": "MIT",
"type": "module",

View File

@@ -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.31
"@
}

View File

@@ -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.31
EOF
exit 1
fi

View File

@@ -5,11 +5,13 @@ 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 = [
"function resolvePiAgentDir(): string {",
' const configured = process.env.PI_CODING_AGENT_DIR?.trim();',
' const configured = process.env.FEYNMAN_CODING_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();',
' if (!configured) return path.join(os.homedir(), ".pi", "agent");',
' return configured.startsWith("~/") ? path.join(os.homedir(), configured.slice(2)) : configured;',
"}",
@@ -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;
}

View File

@@ -123,6 +123,8 @@ export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv {
FEYNMAN_BIN_PATH: resolve(options.appRoot, "bin", "feynman.js"),
FEYNMAN_NPM_PREFIX: feynmanNpmPrefixPath,
// Ensure the Pi child process uses Feynman's agent dir for auth/models/settings.
// Patched Pi uses FEYNMAN_CODING_AGENT_DIR; upstream Pi uses PI_CODING_AGENT_DIR.
FEYNMAN_CODING_AGENT_DIR: options.feynmanAgentDir,
PI_CODING_AGENT_DIR: options.feynmanAgentDir,
PANDOC_PATH: process.env.PANDOC_PATH ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS),
PI_HARDWARE_CURSOR: process.env.PI_HARDWARE_CURSOR ?? "1",

View File

@@ -54,6 +54,7 @@ test("buildPiEnv wires Feynman paths into the Pi environment", () => {
assert.equal(env.FEYNMAN_NPM_PREFIX, "/home/.feynman/npm-global");
assert.equal(env.NPM_CONFIG_PREFIX, "/home/.feynman/npm-global");
assert.equal(env.npm_config_prefix, "/home/.feynman/npm-global");
assert.equal(env.FEYNMAN_CODING_AGENT_DIR, "/home/.feynman/agent");
assert.equal(env.PI_CODING_AGENT_DIR, "/home/.feynman/agent");
assert.ok(
env.PATH?.startsWith(

View File

@@ -83,7 +83,7 @@ for (const scenario of CASES) {
const patched = patchPiSubagentsSource(scenario.file, scenario.input);
assert.match(patched, /function resolvePiAgentDir\(\): string \{/);
assert.match(patched, /process\.env\.PI_CODING_AGENT_DIR\?\.trim\(\)/);
assert.match(patched, /process\.env\.FEYNMAN_CODING_AGENT_DIR\?\.trim\(\) \|\| process\.env\.PI_CODING_AGENT_DIR\?\.trim\(\)/);
assert.ok(patched.includes(scenario.expected));
assert.ok(!patched.includes(scenario.original));
});
@@ -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 = [
"---",

View File

@@ -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.31
EOF
exit 1
fi

View File

@@ -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.31
"@
}

View File

@@ -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.31
```
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.31
```
## Post-install setup