fix: respect feynman agent dir in vendored pi-subagents
This commit is contained in:
@@ -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`.
|
||||
|
||||
2
scripts/lib/pi-subagents-patch.d.mts
Normal file
2
scripts/lib/pi-subagents-patch.d.mts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const PI_SUBAGENTS_PATCH_TARGETS: string[];
|
||||
export function patchPiSubagentsSource(relativePath: string, source: string): string;
|
||||
124
scripts/lib/pi-subagents-patch.mjs
Normal file
124
scripts/lib/pi-subagents-patch.mjs
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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") {
|
||||
|
||||
104
tests/pi-subagents-patch.test.ts
Normal file
104
tests/pi-subagents-patch.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user