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.
|
- 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.
|
- 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.
|
- 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 { dirname, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { FEYNMAN_LOGO_HTML } from "../logo.mjs";
|
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 here = dirname(fileURLToPath(import.meta.url));
|
||||||
const appRoot = resolve(here, "..");
|
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 terminalPath = piTuiRoot ? resolve(piTuiRoot, "dist", "terminal.js") : null;
|
||||||
const editorPath = piTuiRoot ? resolve(piTuiRoot, "dist", "components", "editor.js") : null;
|
const editorPath = piTuiRoot ? resolve(piTuiRoot, "dist", "components", "editor.js") : null;
|
||||||
const workspaceRoot = resolve(appRoot, ".feynman", "npm", "node_modules");
|
const workspaceRoot = resolve(appRoot, ".feynman", "npm", "node_modules");
|
||||||
|
const piSubagentsRoot = resolve(workspaceRoot, "pi-subagents");
|
||||||
const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts");
|
const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts");
|
||||||
const sessionSearchIndexerPath = resolve(
|
const sessionSearchIndexerPath = resolve(
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
@@ -243,6 +245,19 @@ function ensurePandoc() {
|
|||||||
|
|
||||||
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)) {
|
if (packageJsonPath && existsSync(packageJsonPath)) {
|
||||||
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||||
if (pkg.piConfig?.name !== "feynman" || pkg.piConfig?.configDir !== ".feynman") {
|
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