From ab8a284c7451ec598ca5860fd1a59088d346ff10 Mon Sep 17 00:00:00 2001 From: Advait Paliwal Date: Sat, 28 Mar 2026 21:44:50 -0700 Subject: [PATCH] fix: respect feynman agent dir in vendored pi-subagents --- CHANGELOG.md | 9 ++ scripts/lib/pi-subagents-patch.d.mts | 2 + scripts/lib/pi-subagents-patch.mjs | 124 +++++++++++++++++++++++++++ scripts/patch-embedded-pi.mjs | 15 ++++ tests/pi-subagents-patch.test.ts | 104 ++++++++++++++++++++++ 5 files changed, 254 insertions(+) create mode 100644 scripts/lib/pi-subagents-patch.d.mts create mode 100644 scripts/lib/pi-subagents-patch.mjs create mode 100644 tests/pi-subagents-patch.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d627572..2956bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,3 +77,12 @@ Use this file to track chronology, not release notes. Keep entries short, factua - Failed / learned: The open subagent issue is fixed on `main` but still user-visible on tagged installs until a fresh release is cut. - Blockers: Need the GitHub publish workflow to finish successfully before the issue can be honestly closed as released. - Next: Push `0.2.15`, monitor the publish workflow, then update and close the relevant GitHub issue/PR once the release is live. + +### 2026-03-28 15:15 PDT — pi-subagents-agent-dir-compat + +- Objective: Debug why tagged installs can still fail subagent/auth flows after `0.2.15` when users are not on Anthropic. +- Changed: Added `scripts/lib/pi-subagents-patch.mjs` plus type declarations and wired `scripts/patch-embedded-pi.mjs` to rewrite vendored `pi-subagents` runtime files so they resolve user-scoped paths from `PI_CODING_AGENT_DIR` instead of hardcoded `~/.pi/agent`; added `tests/pi-subagents-patch.test.ts`. +- Verified: Materialized `.feynman/npm`, inspected the shipped `pi-subagents@0.11.11` sources, confirmed the hardcoded `~/.pi/agent` paths in `index.ts`, `agents.ts`, `artifacts.ts`, `run-history.ts`, `skills.ts`, and `chain-clarify.ts`; ran `node scripts/patch-embedded-pi.mjs`; ran `npm test`, `npm run typecheck`, and `npm run build`. +- Failed / learned: The earlier `0.2.15` fix only proved that Feynman exported `PI_CODING_AGENT_DIR` to the top-level Pi child; it did not cover vendored extension code that still hardcoded `.pi` paths internally. +- Blockers: Users still need a release containing this patch before tagged installs benefit from it. +- Next: Cut the next release and verify a tagged install exercises subagents without reading from `~/.pi/agent`. diff --git a/scripts/lib/pi-subagents-patch.d.mts b/scripts/lib/pi-subagents-patch.d.mts new file mode 100644 index 0000000..2312432 --- /dev/null +++ b/scripts/lib/pi-subagents-patch.d.mts @@ -0,0 +1,2 @@ +export const PI_SUBAGENTS_PATCH_TARGETS: string[]; +export function patchPiSubagentsSource(relativePath: string, source: string): string; diff --git a/scripts/lib/pi-subagents-patch.mjs b/scripts/lib/pi-subagents-patch.mjs new file mode 100644 index 0000000..f0b1631 --- /dev/null +++ b/scripts/lib/pi-subagents-patch.mjs @@ -0,0 +1,124 @@ +export const PI_SUBAGENTS_PATCH_TARGETS = [ + "index.ts", + "agents.ts", + "artifacts.ts", + "run-history.ts", + "skills.ts", + "chain-clarify.ts", +]; + +const RESOLVE_PI_AGENT_DIR_HELPER = [ + "function resolvePiAgentDir(): string {", + ' const configured = 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;', + "}", +].join("\n"); + +function injectResolvePiAgentDirHelper(source) { + if (source.includes("function resolvePiAgentDir(): string {")) { + return source; + } + + const lines = source.split("\n"); + let insertAt = 0; + let importSeen = false; + let importOpen = false; + + for (let index = 0; index < lines.length; index += 1) { + const trimmed = lines[index].trim(); + if (!importSeen) { + if (trimmed === "" || trimmed.startsWith("/**") || trimmed.startsWith("*") || trimmed.startsWith("*/")) { + insertAt = index + 1; + continue; + } + if (trimmed.startsWith("import ")) { + importSeen = true; + importOpen = !trimmed.endsWith(";"); + insertAt = index + 1; + continue; + } + break; + } + + if (trimmed.startsWith("import ")) { + importOpen = !trimmed.endsWith(";"); + insertAt = index + 1; + continue; + } + if (importOpen) { + if (trimmed.endsWith(";")) importOpen = false; + insertAt = index + 1; + continue; + } + if (trimmed === "") { + insertAt = index + 1; + continue; + } + insertAt = index; + break; + } + + return [...lines.slice(0, insertAt), "", RESOLVE_PI_AGENT_DIR_HELPER, "", ...lines.slice(insertAt)].join("\n"); +} + +function replaceAll(source, from, to) { + return source.split(from).join(to); +} + +export function patchPiSubagentsSource(relativePath, source) { + let patched = source; + + switch (relativePath) { + case "index.ts": + patched = replaceAll( + patched, + 'const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");', + 'const configPath = path.join(resolvePiAgentDir(), "extensions", "subagent", "config.json");', + ); + break; + case "agents.ts": + patched = replaceAll( + patched, + 'const userDir = path.join(os.homedir(), ".pi", "agent", "agents");', + 'const userDir = path.join(resolvePiAgentDir(), "agents");', + ); + break; + case "artifacts.ts": + patched = replaceAll( + patched, + 'const sessionsBase = path.join(os.homedir(), ".pi", "agent", "sessions");', + 'const sessionsBase = path.join(resolvePiAgentDir(), "sessions");', + ); + break; + case "run-history.ts": + patched = replaceAll( + patched, + 'const HISTORY_PATH = path.join(os.homedir(), ".pi", "agent", "run-history.jsonl");', + 'const HISTORY_PATH = path.join(resolvePiAgentDir(), "run-history.jsonl");', + ); + break; + case "skills.ts": + patched = replaceAll( + patched, + 'const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");', + "const AGENT_DIR = resolvePiAgentDir();", + ); + break; + case "chain-clarify.ts": + patched = replaceAll( + patched, + 'const dir = path.join(os.homedir(), ".pi", "agent", "agents");', + 'const dir = path.join(resolvePiAgentDir(), "agents");', + ); + break; + default: + return source; + } + + if (patched === source) { + return source; + } + + return injectResolvePiAgentDirHelper(patched); +} diff --git a/scripts/patch-embedded-pi.mjs b/scripts/patch-embedded-pi.mjs index 22eb29a..e1ff6e1 100644 --- a/scripts/patch-embedded-pi.mjs +++ b/scripts/patch-embedded-pi.mjs @@ -4,6 +4,7 @@ import { createRequire } from "node:module"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { FEYNMAN_LOGO_HTML } from "../logo.mjs"; +import { PI_SUBAGENTS_PATCH_TARGETS, patchPiSubagentsSource } from "./lib/pi-subagents-patch.mjs"; const here = dirname(fileURLToPath(import.meta.url)); const appRoot = resolve(here, ".."); @@ -54,6 +55,7 @@ const interactiveThemePath = piPackageRoot ? resolve(piPackageRoot, "dist", "mod const terminalPath = piTuiRoot ? resolve(piTuiRoot, "dist", "terminal.js") : null; const editorPath = piTuiRoot ? resolve(piTuiRoot, "dist", "components", "editor.js") : null; const workspaceRoot = resolve(appRoot, ".feynman", "npm", "node_modules"); +const piSubagentsRoot = resolve(workspaceRoot, "pi-subagents"); const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts"); const sessionSearchIndexerPath = resolve( workspaceRoot, @@ -243,6 +245,19 @@ function ensurePandoc() { ensurePandoc(); +if (existsSync(piSubagentsRoot)) { + for (const relativePath of PI_SUBAGENTS_PATCH_TARGETS) { + const entryPath = resolve(piSubagentsRoot, relativePath); + if (!existsSync(entryPath)) continue; + + const source = readFileSync(entryPath, "utf8"); + const patched = patchPiSubagentsSource(relativePath, source); + if (patched !== source) { + writeFileSync(entryPath, patched, "utf8"); + } + } +} + if (packageJsonPath && existsSync(packageJsonPath)) { const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")); if (pkg.piConfig?.name !== "feynman" || pkg.piConfig?.configDir !== ".feynman") { diff --git a/tests/pi-subagents-patch.test.ts b/tests/pi-subagents-patch.test.ts new file mode 100644 index 0000000..e03f482 --- /dev/null +++ b/tests/pi-subagents-patch.test.ts @@ -0,0 +1,104 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { patchPiSubagentsSource } from "../scripts/lib/pi-subagents-patch.mjs"; + +const CASES = [ + { + name: "index.ts config path", + file: "index.ts", + input: [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + 'const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");', + "", + ].join("\n"), + original: 'const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");', + expected: 'const configPath = path.join(resolvePiAgentDir(), "extensions", "subagent", "config.json");', + }, + { + name: "agents.ts user agents dir", + file: "agents.ts", + input: [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + 'const userDir = path.join(os.homedir(), ".pi", "agent", "agents");', + "", + ].join("\n"), + original: 'const userDir = path.join(os.homedir(), ".pi", "agent", "agents");', + expected: 'const userDir = path.join(resolvePiAgentDir(), "agents");', + }, + { + name: "artifacts.ts sessions dir", + file: "artifacts.ts", + input: [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + 'const sessionsBase = path.join(os.homedir(), ".pi", "agent", "sessions");', + "", + ].join("\n"), + original: 'const sessionsBase = path.join(os.homedir(), ".pi", "agent", "sessions");', + expected: 'const sessionsBase = path.join(resolvePiAgentDir(), "sessions");', + }, + { + name: "run-history.ts history file", + file: "run-history.ts", + input: [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + 'const HISTORY_PATH = path.join(os.homedir(), ".pi", "agent", "run-history.jsonl");', + "", + ].join("\n"), + original: 'const HISTORY_PATH = path.join(os.homedir(), ".pi", "agent", "run-history.jsonl");', + expected: 'const HISTORY_PATH = path.join(resolvePiAgentDir(), "run-history.jsonl");', + }, + { + name: "skills.ts agent dir", + file: "skills.ts", + input: [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + 'const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");', + "", + ].join("\n"), + original: 'const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");', + expected: "const AGENT_DIR = resolvePiAgentDir();", + }, + { + name: "chain-clarify.ts chain save dir", + file: "chain-clarify.ts", + input: [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + 'const dir = path.join(os.homedir(), ".pi", "agent", "agents");', + "", + ].join("\n"), + original: 'const dir = path.join(os.homedir(), ".pi", "agent", "agents");', + expected: 'const dir = path.join(resolvePiAgentDir(), "agents");', + }, +]; + +for (const scenario of CASES) { + test(`patchPiSubagentsSource rewrites ${scenario.name}`, () => { + const patched = patchPiSubagentsSource(scenario.file, scenario.input); + + assert.match(patched, /function resolvePiAgentDir\(\): string \{/); + assert.match(patched, /process\.env\.PI_CODING_AGENT_DIR\?\.trim\(\)/); + assert.ok(patched.includes(scenario.expected)); + assert.ok(!patched.includes(scenario.original)); + }); +} + +test("patchPiSubagentsSource is idempotent", () => { + const input = [ + 'import * as os from "node:os";', + 'import * as path from "node:path";', + 'const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");', + "", + ].join("\n"); + + const once = patchPiSubagentsSource("index.ts", input); + const twice = patchPiSubagentsSource("index.ts", once); + + assert.equal(twice, once); +});