From 9e7759c9f5f352483d50754b4a403abfdee0baf2 Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 14 Sep 2026 10:40:42 -0400 Subject: [PATCH] feat(nixos): build vendored pi --- .pi/extensions/herdr-agent-state.ts | 287 ------------------ configs/nixos/modules/shell/pi/default.nix | 12 +- .../pi-project-resource-ancestors.patch | 140 --------- .../patches/pi-tool-lookup-validation.patch | 23 -- .../coding-agent/src/core/package-manager.ts | 97 +++--- .../coding-agent/src/utils/tools-manager.ts | 6 +- 6 files changed, 54 insertions(+), 511 deletions(-) delete mode 100644 .pi/extensions/herdr-agent-state.ts delete mode 100644 configs/nixos/modules/shell/pi/patches/pi-project-resource-ancestors.patch delete mode 100644 configs/nixos/modules/shell/pi/patches/pi-tool-lookup-validation.patch diff --git a/.pi/extensions/herdr-agent-state.ts b/.pi/extensions/herdr-agent-state.ts deleted file mode 100644 index caa50ed..0000000 --- a/.pi/extensions/herdr-agent-state.ts +++ /dev/null @@ -1,287 +0,0 @@ -// installed by herdr -// managed by herdr; reinstalling or updating the integration overwrites this file. -// add custom hooks/plugins beside this file instead of editing it. -// HERDR_INTEGRATION_ID=pi -// HERDR_INTEGRATION_VERSION=6 -// @ts-nocheck - -import net from "node:net"; - -const HERDR_ENV = process.env.HERDR_ENV; -const socketPath = process.env.HERDR_SOCKET_PATH; -const socketEndpoint = - process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath; -const paneId = process.env.HERDR_PANE_ID; -const source = "herdr:pi"; - -function enabled() { - return HERDR_ENV === "1" && !!socketPath && !!paneId; -} - -function sendRequestAttempt(request: unknown, timeoutMs: number): Promise { - if (!enabled()) { - return Promise.resolve(true); - } - - return new Promise((resolve) => { - let done = false; - let timeout: ReturnType | undefined; - const finish = (delivered: boolean) => { - if (done) return; - done = true; - if (timeout) { - clearTimeout(timeout); - } - socket.destroy(); - resolve(delivered); - }; - - const socket = net.createConnection(socketEndpoint!); - socket.on("error", () => finish(false)); - socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`)); - socket.on("data", () => finish(true)); - socket.on("end", () => finish(false)); - timeout = setTimeout(() => finish(false), timeoutMs); - timeout.unref?.(); - }); -} - -async function sendRequest(request: unknown): Promise { - if (await sendRequestAttempt(request, 500)) { - return; - } - await sendRequestAttempt(request, 1500); -} - -type AgentState = "working" | "blocked" | "idle"; - -type QueuedState = { - state: AgentState; - message?: string; - seq: number; -}; - -let reportSeq = Date.now() * 1000; -let currentAgentSessionId: string | undefined; -let currentAgentSessionPath: string | undefined; - -function nextReportSeq(): number { - reportSeq += 1; - return reportSeq; -} - -function updateSessionRef(ctx: any): void { - try { - const file = ctx?.sessionManager?.getSessionFile?.(); - currentAgentSessionPath = - typeof file === "string" && file.startsWith("/") ? file : undefined; - } catch { - currentAgentSessionPath = undefined; - } - - try { - const id = ctx?.sessionManager?.getSessionId?.(); - currentAgentSessionId = typeof id === "string" && id.length > 0 ? id : undefined; - } catch { - currentAgentSessionId = undefined; - } -} - -function withSessionRef(params: Record): Record { - if (currentAgentSessionPath) { - return { ...params, agent_session_path: currentAgentSessionPath }; - } - if (currentAgentSessionId) { - return { ...params, agent_session_id: currentAgentSessionId }; - } - return params; -} - -function currentSessionRef(): Record | undefined { - if (currentAgentSessionPath) { - return { agent_session_path: currentAgentSessionPath }; - } - if (currentAgentSessionId) { - return { agent_session_id: currentAgentSessionId }; - } - return undefined; -} - -function reportSession(sessionStartSource?: string): Promise { - const sessionRef = currentSessionRef(); - if (!sessionRef) { - return Promise.resolve(); - } - - return sendRequest({ - id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`, - method: "pane.report_agent_session", - params: { - pane_id: paneId, - source, - agent: "pi", - seq: nextReportSeq(), - session_start_source: sessionStartSource, - ...sessionRef, - }, - }); -} - -function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise { - return sendRequest({ - id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, - method: "pane.report_agent", - params: withSessionRef({ - pane_id: paneId, - source, - agent: "pi", - state, - message, - seq, - }), - }); -} - -function releaseAgent(): Promise { - return sendRequest({ - id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`, - method: "pane.release_agent", - params: { - pane_id: paneId, - source, - agent: "pi", - seq: nextReportSeq(), - }, - }); -} - -function shouldReleaseOnSessionShutdown(event: any): boolean { - // Pi tears down and rebinds extension runtimes for internal lifecycle actions - // such as /reload, /new, /resume, and /fork. Those do not mean the pane's - // agent process has exited, and releasing hook authority there can suppress - // legitimate reports from the replacement runtime. Only a user/process quit - // should release Herdr's full-lifecycle authority. - const reason = event?.reason; - return reason === "quit"; -} - -let sendInFlight = false; -let queuedState: QueuedState | undefined; - -function queueState(state: AgentState, message?: string): void { - queuedState = { state, message, seq: nextReportSeq() }; - if (!sendInFlight) { - void drainStateQueue(); - } -} - -async function drainStateQueue(): Promise { - if (sendInFlight) { - return; - } - - sendInFlight = true; - try { - while (queuedState) { - const next = queuedState; - queuedState = undefined; - await sendState(next.state, next.message, next.seq); - } - } finally { - sendInFlight = false; - if (queuedState) { - void drainStateQueue(); - } - } -} - -export default function (pi) { - if (!enabled()) { - return; - } - - let agentActive = false; - let blockedCount = 0; - let blockedMessage: string | undefined; - let lastState: AgentState | undefined; - let lastMessage: string | undefined; - let rootSession = false; - - function desiredState() { - if (blockedCount > 0) { - return { state: "blocked" as const, message: blockedMessage }; - } - if (agentActive) { - return { state: "working" as const, message: undefined }; - } - return { state: "idle" as const, message: undefined }; - } - - function publishState(force = false) { - const next = desiredState(); - if (!force && next.state === lastState && next.message === lastMessage) { - return; - } - lastState = next.state; - lastMessage = next.message; - queueState(next.state, next.message); - } - - pi.events.on("herdr:blocked", (data) => { - if (!rootSession) { - return; - } - if (!data?.active) { - blockedCount = Math.max(0, blockedCount - 1); - if (blockedCount === 0) { - blockedMessage = undefined; - } - publishState(); - return; - } - - blockedCount += 1; - blockedMessage = data.label; - publishState(); - }); - - pi.on("session_start", async (event, ctx) => { - if (ctx?.hasUI !== true) { - return; - } - rootSession = true; - updateSessionRef(ctx); - await reportSession(event?.reason); - // A reload can replace this extension mid-run without emitting another agent_start. - agentActive = ctx?.isIdle?.() === false; - publishState(true); - }); - - pi.on("agent_start", (_event, ctx) => { - if (!rootSession) { - return; - } - updateSessionRef(ctx); - void reportSession(); - agentActive = true; - publishState(); - }); - - pi.on("agent_settled", (_event, ctx) => { - if (!rootSession || ctx?.isIdle?.() !== true) { - return; - } - - agentActive = false; - publishState(); - }); - - pi.on("session_shutdown", async (event) => { - if (!rootSession) { - return; - } - if (shouldReleaseOnSessionShutdown(event)) { - await releaseAgent(); - } - }); -} diff --git a/configs/nixos/modules/shell/pi/default.nix b/configs/nixos/modules/shell/pi/default.nix index 39889eb..2b1c74b 100644 --- a/configs/nixos/modules/shell/pi/default.nix +++ b/configs/nixos/modules/shell/pi/default.nix @@ -11,12 +11,10 @@ let cfg = config.modules.shell.pi; user = config.user.name; piDir = "${config.users.users.${user}.home}/.pi/agent"; - patchedPi = pkgs.pi-coding-agent.overrideAttrs (old: { - patches = (old.patches or [ ]) ++ [ - ./patches/pi-project-resource-ancestors.patch - ./patches/pi-tool-lookup-validation.patch - ]; - }); + vendoredPi = pkgs.pi-coding-agent.overrideAttrs { + src = ../../../../../third_party/pi; + patches = [ ]; + }; in { options.modules.shell.pi = { @@ -28,7 +26,7 @@ in home-manager.users.${user} = { programs.pi-coding-agent = { enable = true; - package = patchedPi; + package = vendoredPi; settings = { defaultProvider = "openai-codex"; diff --git a/configs/nixos/modules/shell/pi/patches/pi-project-resource-ancestors.patch b/configs/nixos/modules/shell/pi/patches/pi-project-resource-ancestors.patch deleted file mode 100644 index fc8ff64..0000000 --- a/configs/nixos/modules/shell/pi/patches/pi-project-resource-ancestors.patch +++ /dev/null @@ -1,140 +0,0 @@ -diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts ---- a/packages/coding-agent/src/core/package-manager.ts -+++ b/packages/coding-agent/src/core/package-manager.ts -@@ -459,14 +459,14 @@ function findGitRepoRoot(startDir: string): string | null { - } - } - --function collectAncestorAgentsSkillDirs(startDir: string): string[] { -- const skillDirs: string[] = []; -+function collectAncestorConfigDirs(startDir: string, configDirName: string): string[] { -+ const configDirs: string[] = []; - const resolvedStartDir = resolve(startDir); - const gitRepoRoot = findGitRepoRoot(resolvedStartDir); - - let dir = resolvedStartDir; - while (true) { -- skillDirs.push(join(dir, ".agents", "skills")); -+ configDirs.push(join(dir, configDirName)); - if (gitRepoRoot && dir === gitRepoRoot) { - break; - } -@@ -477,7 +477,7 @@ function collectAncestorAgentsSkillDirs(startDir: string): string[] { - dir = parent; - } - -- return skillDirs; -+ return configDirs; - } - - function collectAutoPromptEntries(dir: string): string[] { -@@ -2388,16 +2388,13 @@ export class DefaultPackageManager implements PackageManager { - prompts: join(globalBaseDir, "prompts"), - themes: join(globalBaseDir, "themes"), - }; -- const projectDirs = { -- extensions: join(projectBaseDir, "extensions"), -- skills: join(projectBaseDir, "skills"), -- prompts: join(projectBaseDir, "prompts"), -- themes: join(projectBaseDir, "themes"), -- }; - const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills"); - const projectTrusted = this.settingsManager.isProjectTrusted(); -- const projectAgentsSkillDirs = projectTrusted -- ? collectAncestorAgentsSkillDirs(this.cwd).filter((dir) => resolve(dir) !== resolve(userAgentsSkillsDir)) -+ const projectConfigDirs = projectTrusted -+ ? collectAncestorConfigDirs(this.cwd, CONFIG_DIR_NAME).filter((dir) => resolve(dir) !== resolve(globalBaseDir)) -+ : []; -+ const projectAgentsDirs = projectTrusted -+ ? collectAncestorConfigDirs(this.cwd, ".agents").filter((dir) => resolve(dir) !== resolve(dirname(userAgentsSkillsDir))) - : []; - - const addResources = ( -@@ -2414,56 +2411,54 @@ export class DefaultPackageManager implements PackageManager { - } - }; - -- if (projectTrusted) { -- // Project extensions from .pi/ -+ // Project resources from .pi/ in cwd and ancestors. -+ for (const configDir of projectConfigDirs) { -+ const configMetadata: PathMetadata = { -+ ...projectMetadata, -+ baseDir: configDir, -+ }; - addResources( - "extensions", -- collectAutoExtensionEntries(projectDirs.extensions), -- projectMetadata, -+ collectAutoExtensionEntries(join(configDir, "extensions")), -+ configMetadata, - projectOverrides.extensions, -- projectBaseDir, -+ configDir, - ); -- -- // Project skills from .pi/ - addResources( - "skills", -- collectAutoSkillEntries(projectDirs.skills, "pi"), -- projectMetadata, -+ collectAutoSkillEntries(join(configDir, "skills"), "pi"), -+ configMetadata, - projectOverrides.skills, -- projectBaseDir, -+ configDir, -+ ); -+ addResources( -+ "prompts", -+ collectAutoPromptEntries(join(configDir, "prompts")), -+ configMetadata, -+ projectOverrides.prompts, -+ configDir, -+ ); -+ addResources( -+ "themes", -+ collectAutoThemeEntries(join(configDir, "themes")), -+ configMetadata, -+ projectOverrides.themes, -+ configDir, - ); - } - -- // Project skills from .agents/ (each with its own baseDir) -- for (const agentsSkillsDir of projectAgentsSkillDirs) { -- const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory -+ // Project skills from .agents/ in cwd and ancestors. -+ for (const agentsDir of projectAgentsDirs) { - const agentsMetadata: PathMetadata = { - ...projectMetadata, -- baseDir: agentsBaseDir, -+ baseDir: agentsDir, - }; - addResources( - "skills", -- collectAutoSkillEntries(agentsSkillsDir, "agents"), -+ collectAutoSkillEntries(join(agentsDir, "skills"), "agents"), - agentsMetadata, - projectOverrides.skills, -- agentsBaseDir, -- ); -- } -- -- if (projectTrusted) { -- addResources( -- "prompts", -- collectAutoPromptEntries(projectDirs.prompts), -- projectMetadata, -- projectOverrides.prompts, -- projectBaseDir, -- ); -- addResources( -- "themes", -- collectAutoThemeEntries(projectDirs.themes), -- projectMetadata, -- projectOverrides.themes, -- projectBaseDir, -+ agentsDir, - ); - } - diff --git a/configs/nixos/modules/shell/pi/patches/pi-tool-lookup-validation.patch b/configs/nixos/modules/shell/pi/patches/pi-tool-lookup-validation.patch deleted file mode 100644 index 27a1afa..0000000 --- a/configs/nixos/modules/shell/pi/patches/pi-tool-lookup-validation.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts ---- a/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:36.970496010 -0400 -+++ b/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:37.028186009 -0400 -@@ -74,8 +74,8 @@ - function commandExists(cmd: string): boolean { - try { - const result = spawnSync(cmd, ["--version"], { stdio: "pipe" }); -- // Check for ENOENT error (command not found) -- return result.error === undefined || result.error === null; -+ // Downloaded generic Linux binaries can exist but fail on NixOS before they print a version. -+ return (result.error === undefined || result.error === null) && result.status === 0; - } catch { - return false; - } -@@ -88,7 +87,7 @@ - - // Check our tools directory first - const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : "")); -- if (existsSync(localPath)) { -+ if (existsSync(localPath) && commandExists(localPath)) { - return localPath; - } - diff --git a/third_party/pi/packages/coding-agent/src/core/package-manager.ts b/third_party/pi/packages/coding-agent/src/core/package-manager.ts index d65386c..dc57e09 100644 --- a/third_party/pi/packages/coding-agent/src/core/package-manager.ts +++ b/third_party/pi/packages/coding-agent/src/core/package-manager.ts @@ -459,14 +459,14 @@ function findGitRepoRoot(startDir: string): string | null { } } -function collectAncestorAgentsSkillDirs(startDir: string): string[] { - const skillDirs: string[] = []; +function collectAncestorConfigDirs(startDir: string, configDirName: string): string[] { + const configDirs: string[] = []; const resolvedStartDir = resolve(startDir); const gitRepoRoot = findGitRepoRoot(resolvedStartDir); let dir = resolvedStartDir; while (true) { - skillDirs.push(join(dir, ".agents", "skills")); + configDirs.push(join(dir, configDirName)); if (gitRepoRoot && dir === gitRepoRoot) { break; } @@ -477,7 +477,7 @@ function collectAncestorAgentsSkillDirs(startDir: string): string[] { dir = parent; } - return skillDirs; + return configDirs; } function collectAutoPromptEntries(dir: string): string[] { @@ -2388,16 +2388,13 @@ export class DefaultPackageManager implements PackageManager { prompts: join(globalBaseDir, "prompts"), themes: join(globalBaseDir, "themes"), }; - const projectDirs = { - extensions: join(projectBaseDir, "extensions"), - skills: join(projectBaseDir, "skills"), - prompts: join(projectBaseDir, "prompts"), - themes: join(projectBaseDir, "themes"), - }; const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills"); const projectTrusted = this.settingsManager.isProjectTrusted(); - const projectAgentsSkillDirs = projectTrusted - ? collectAncestorAgentsSkillDirs(this.cwd).filter((dir) => resolve(dir) !== resolve(userAgentsSkillsDir)) + const projectConfigDirs = projectTrusted + ? collectAncestorConfigDirs(this.cwd, CONFIG_DIR_NAME).filter((dir) => resolve(dir) !== resolve(globalBaseDir)) + : []; + const projectAgentsDirs = projectTrusted + ? collectAncestorConfigDirs(this.cwd, ".agents").filter((dir) => resolve(dir) !== resolve(dirname(userAgentsSkillsDir))) : []; const addResources = ( @@ -2414,56 +2411,54 @@ export class DefaultPackageManager implements PackageManager { } }; - if (projectTrusted) { - // Project extensions from .pi/ - addResources( - "extensions", - collectAutoExtensionEntries(projectDirs.extensions), - projectMetadata, - projectOverrides.extensions, - projectBaseDir, - ); - - // Project skills from .pi/ - addResources( - "skills", - collectAutoSkillEntries(projectDirs.skills, "pi"), - projectMetadata, - projectOverrides.skills, - projectBaseDir, - ); - } - - // Project skills from .agents/ (each with its own baseDir) - for (const agentsSkillsDir of projectAgentsSkillDirs) { - const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory - const agentsMetadata: PathMetadata = { + // Project resources from .pi/ in cwd and ancestors. + for (const configDir of projectConfigDirs) { + const configMetadata: PathMetadata = { ...projectMetadata, - baseDir: agentsBaseDir, + baseDir: configDir, }; addResources( - "skills", - collectAutoSkillEntries(agentsSkillsDir, "agents"), - agentsMetadata, - projectOverrides.skills, - agentsBaseDir, + "extensions", + collectAutoExtensionEntries(join(configDir, "extensions")), + configMetadata, + projectOverrides.extensions, + configDir, + ); + addResources( + "skills", + collectAutoSkillEntries(join(configDir, "skills"), "pi"), + configMetadata, + projectOverrides.skills, + configDir, ); - } - - if (projectTrusted) { addResources( "prompts", - collectAutoPromptEntries(projectDirs.prompts), - projectMetadata, + collectAutoPromptEntries(join(configDir, "prompts")), + configMetadata, projectOverrides.prompts, - projectBaseDir, + configDir, ); addResources( "themes", - collectAutoThemeEntries(projectDirs.themes), - projectMetadata, + collectAutoThemeEntries(join(configDir, "themes")), + configMetadata, projectOverrides.themes, - projectBaseDir, + configDir, + ); + } + + // Project skills from .agents/ in cwd and ancestors. + for (const agentsDir of projectAgentsDirs) { + const agentsMetadata: PathMetadata = { + ...projectMetadata, + baseDir: agentsDir, + }; + addResources( + "skills", + collectAutoSkillEntries(join(agentsDir, "skills"), "agents"), + agentsMetadata, + projectOverrides.skills, + agentsDir, ); } diff --git a/third_party/pi/packages/coding-agent/src/utils/tools-manager.ts b/third_party/pi/packages/coding-agent/src/utils/tools-manager.ts index f040f52..2bcca47 100644 --- a/third_party/pi/packages/coding-agent/src/utils/tools-manager.ts +++ b/third_party/pi/packages/coding-agent/src/utils/tools-manager.ts @@ -72,8 +72,8 @@ const TOOLS: Record = { function commandExists(cmd: string): boolean { try { const result = spawnSync(cmd, ["--version"], { stdio: "pipe" }); - // Check for ENOENT error (command not found) - return result.error === undefined || result.error === null; + // Downloaded generic Linux binaries can exist but fail on NixOS before they print a version. + return (result.error === undefined || result.error === null) && result.status === 0; } catch { return false; } @@ -86,7 +86,7 @@ export function getToolPath(tool: "fd" | "rg"): string | null { // Check our tools directory first const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : "")); - if (existsSync(localPath)) { + if (existsSync(localPath) && commandExists(localPath)) { return localPath; }