Discover ancestor Pi project resources #23

Merged
alexion merged 1 commits from pi-project-resource-ancestors into main 2026-09-07 22:46:31 -04:00
5 changed files with 146 additions and 11 deletions

View File

@@ -32,11 +32,11 @@
- Use produced literals for Herdr key names that include shifted punctuation.
For example, use `prefix+<` instead of `prefix+shift+comma`.
- Put global Pi extension sources in `configs/nixos/modules/shell/pi/extensions/`.
Home Manager deploys them to `~/.pi/agent/extensions/`.
- Treat Home Manager-managed Pi extension, prompt, and skill directories as generated output.
Do not edit those directories except for throwaway development or local experiments.
Promote finished work into the NixOS configuration before treating it as deployed.
- Put monorepo Pi extension sources in `.pi/extensions/`.
Pi discovers them from subdirectories through the local ancestor-resource patch.
- Treat Home Manager-managed Pi settings as generated output.
Do not edit them except for throwaway development or local experiments.
Promote finished settings into the NixOS configuration before treating them as deployed.
## Gotchas

View File

@@ -13,6 +13,7 @@ let
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
];
});
@@ -44,12 +45,6 @@ in
# The first declarative rollout replaces the interactive settings file.
# Login state stays in auth.json, which this module does not manage.
"${piDir}/settings.json".force = true;
"${piDir}/extensions" = {
source = ./extensions;
recursive = true;
force = true;
};
};
};
};

View File

@@ -0,0 +1,140 @@
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,
);
}