--- issue: 428 issue_title: "pi-permission-system: permission-system using incorrect path for `projectAgentsDir`" --- # Fix project agents directory path resolution ## Problem Statement A user defined a project-scoped custom agent at `/.pi/agents/my-custom-agent.md` with a `permission:` block in its YAML frontmatter. pi-subagents discovered and ran the agent correctly, but its permission frontmatter was never applied. The `config.resolved` log shows why: pi-permission-system computed `projectAgentsDir` as `/.pi/agent/agents`, then found nothing there and resolved no project-agent config. The correct location — the one pi-subagents uses — is `/.pi/agents`. ## Goals - Resolve the project agents directory at `/.pi/agents`, matching the Pi platform convention that pi-subagents already follows. - Extract a small named helper inside pi-permission-system that owns this convention, with a comment cross-referencing pi-subagents' sibling encoding. - Add a regression test that exercises the real `agentDir`-derived resolution end-to-end (a project agent's `permission:` frontmatter is actually enforced). - Correct the project-agent override path documented in `docs/configuration.md`, which currently repeats the same wrong path. This change is **breaking** (`fix!:`). On upgrade, project agents' `permission:` frontmatter — silently ignored today — starts being read and enforced without any user edit, which can make a session *more* restrictive than before. ## Non-Goals - No new shared package and no `pi-permission-system → pi-subagents` code dependency. The two packages are decoupled by design (event-bus only, ADR-0002); the path is a Pi platform convention that each client encodes independently, exactly as both already do for the global agents directory. - No change to pi-subagents — its `loadCustomAgents` (`config/custom-agents.ts`) already resolves `/.pi/agents` correctly. - No change to the global agents directory derivation (`/agents`); it is already correct. - No change to the config schema, example config, or the `permission:` frontmatter format. ## Background The resolution lives in `src/permission-manager.ts`. `derivePolicyLoaderOptions(agentDir, cwd)` builds `PolicyLoaderOptions` for a `FilePolicyLoader`, and it is the sole place the project agents directory is derived from a `cwd`: ```typescript return { globalConfigPath: getGlobalConfigPath(agentDir), agentsDir: join(agentDir, "agents"), projectGlobalConfigPath: cwd ? getProjectConfigPath(cwd) : undefined, projectAgentsDir: cwd ? join(cwd, ".pi", "agent", "agents") : undefined, // ← bug }; ``` It is called from the `PermissionManager` constructor (when built with `{ agentDir }`) and from `configureForCwd(cwd)`. The derived `projectAgentsDir` feeds `FilePolicyLoader.loadProjectAgentConfig(agentName)`, which reads `/.md`, extracts its frontmatter, and contributes the `project-agent` scope to the merged ruleset. Pi-convention paths in this package already live in `src/config-paths.ts` (`getGlobalConfigPath`, `getProjectConfigPath`, etc.) — the natural home for an agents-directory helper. pi-subagents encodes the same project convention independently in `config/custom-agents.ts` (`join(cwd, ".pi", "agents")`); the global convention (`join(getAgentDir(), "agents")`) is *also* duplicated between the two packages and has never been a coupling problem, because both are honoring the same platform contract rather than depending on each other. The SDK (`@earendil-works/pi-coding-agent`) exposes `getAgentDir()` — the single Pi agent's home — but no agents-*directory* helper, because Pi is single-agent and has no concept of multiple named agents (see the next subsection and Open Questions). So independent encoding plus a test is the pragmatic, decoupling-preserving answer. ### Per-agent frontmatter is a multi-agent integration concern, not a core one Pi is single-agent by deliberate design — it has no concept of multiple named agents. The notion of agent *types* is introduced entirely by external extensions (pi-subagents, pi-agent-router, some MasuRii packages); `/agents` is itself a pi-subagents command. pi-permission-system already reflects this: it learns the active agent's name from a generic `` tag injected into the system prompt (by pi-agent-router) or an `active_agent` session entry — never from a hard dependency on any one multi-agent extension. It does not enumerate or discover agents (pi-subagents owns that); it reads only the `permission:` sub-document of an agent file, by the active agent's name, on demand. Because pps bridges to multi-agent tooling through generic, extension-agnostic signals rather than a pi-subagents dependency, it likewise reads the agent file itself rather than relying on any one extension to push the data — so encoding the `/.pi/agents` convention is, for now, pps's own integration-layer responsibility. See Open Questions for the longer-term direction. ## Design Overview Add a convention helper to `src/config-paths.ts`: ```typescript /** * Directory holding project-scoped custom agent definitions. * * `/.pi/agents` is a Pi platform convention, also encoded by * @gotgenes/pi-subagents' `loadCustomAgents` (`config/custom-agents.ts`). * The two packages encode it independently — pi-permission-system has no * dependency on pi-subagents (ADR-0002) — so this is this package's * authoritative copy. */ export function getProjectAgentsDir(cwd: string): string { return join(cwd, ".pi", "agents"); } ``` `derivePolicyLoaderOptions` then calls it: ```typescript projectAgentsDir: cwd ? getProjectAgentsDir(cwd) : undefined, ``` No data shape changes — `PolicyLoaderOptions`, `ResolvedPolicyPaths`, and the merge precedence are untouched. The only observable difference is the resolved path value, which now points at the correct directory. ### Edge cases - `cwd` absent (global-only) — `projectAgentsDir` stays `undefined`; unchanged. - A project agent file present at the new path but absent at the old one — now found and applied (the bug's scenario). - Cache stamping (`getCacheStamp`) already keys off `this.projectAgentsDir`, so correcting the directory makes change-detection track the right file with no further work. ## Module-Level Changes - `src/config-paths.ts` — add `getProjectAgentsDir(cwd)` with the convention comment. - `src/permission-manager.ts` — import `getProjectAgentsDir`; replace the inline `join(cwd, ".pi", "agent", "agents")` in `derivePolicyLoaderOptions` with `getProjectAgentsDir(cwd)`. - `test/config-paths.test.ts` — add a unit test for `getProjectAgentsDir`. - `test/permission-manager-unified.test.ts` — extend the `makeAgentDirSetup` helper (in the `configureForCwd and agentDir option` describe block) to optionally write a project agent `.md`; add a path-level assertion and a behavior-level regression test. - `docs/configuration.md` — under "Project Agent Override", change the path from `/.pi/agent/agents/.md` to `/.pi/agents/.md`. Searched for other affected references: - The `package-pi-permission-system` SKILL.md documents the per-agent override mechanism without stating the project path, so it needs no change. - `README.md` references per-agent overrides without a path; no change. - `test/config-reporter.test.ts` uses `/projects/my-app/.pi/agent/agents` as an arbitrary sample string fed to the reporter (not a derived value); leaving it is harmless — an optional cosmetic update only (see Open Questions). ## Test Impact Analysis 1. New tests enabled by the fix: - A unit test for the pure `getProjectAgentsDir` helper. - A regression test that drives the *real* `agentDir`-based derivation through `configureForCwd(cwd)` and asserts a project agent's `permission:` frontmatter is enforced — previously impossible because every existing test passed `projectAgentsDir` explicitly and so never exercised the buggy derivation. 2. Redundant tests: none. The existing `getResolvedPolicyPaths` tests pass explicit paths and exercise reporting, not derivation. 3. Tests that must stay as-is: the explicit-path `getResolvedPolicyPaths` tests and the existing `configureForCwd` precedence tests, which genuinely exercise the loader rebuild and merge precedence. ## Invariants at risk - The global agents directory derivation (`join(agentDir, "agents")`) must stay unchanged. It is pinned by the existing `construction with { agentDir } reads global config from getGlobalConfigPath(agentDir)` test in `test/permission-manager-unified.test.ts`; the new path-level assertion will additionally confirm `agentsDir` is unaffected alongside the corrected `projectAgentsDir`. - The project/global config-path derivations (`getProjectConfigPath`, `getGlobalConfigPath`) must stay unchanged; the existing precedence tests in the same describe block pin them. ## TDD Order 1. `fix!`: correct the project agents directory resolution. - Add `getProjectAgentsDir(cwd)` to `src/config-paths.ts`. - Add its unit test to `test/config-paths.test.ts`. - Extend `makeAgentDirSetup` to optionally write a project agent file, then add to the `configureForCwd and agentDir option` describe block: (a) a path-level test asserting `getResolvedPolicyPaths().projectAgentsDir === join(cwd, ".pi", "agents")` (and that `agentsDir` is unchanged), and (b) a behavior-level test that writes `/.pi/agents/coder.md` with `permission:\n read: deny`, calls `configureForCwd(cwd)`, and asserts `checkPermission("read", { path: "foo.txt" }, "coder").state === "deny"`. These fail against the current code. - Wire `getProjectAgentsDir` into `derivePolicyLoaderOptions` to make them pass. - Run `pnpm --filter @gotgenes/pi-permission-system exec vitest run` (the helper change touches shared resolution). - Commit (`fix!:`) with a `BREAKING CHANGE:` footer: project agents' `permission:` frontmatter at `/.pi/agents/.md` is now read and enforced; previously the wrong directory was checked and the frontmatter was silently ignored, so a session may become more restrictive on upgrade. Remediation: if a project agent's `permission:` block restricts tools unexpectedly, edit or remove that block in `/.pi/agents/.md`. Reference the issue as `(#428)` in the subject. 2. `docs`: correct the documented project-agent override path in `docs/configuration.md` from `/.pi/agent/agents/.md` to `/.pi/agents/.md`. Commit (`docs:`), referencing `(#428)`. ## Risks and Mitigations - Behavior change on upgrade (sessions may become more restrictive when a project agent declares `permission:`). Mitigated by the `fix!:` classification, the `BREAKING CHANGE:` footer with remediation, and the corrected documentation; `/ship-issue` surfaces the same summary in the close comment. - Drift between this package's and pi-subagents' encoding of the convention. Mitigated by the cross-reference comment on `getProjectAgentsDir` and the behavior-level regression test pinning the resolved path. - Cache-key correctness: `getCacheStamp` already keys off `projectAgentsDir`, so correcting the directory only improves change detection — no regression risk. ## Open Questions - Long-term, per-agent `permission:` frontmatter is best modeled as an **extension bridge on top of pps's single-agent core**, not a core responsibility. Pi is single-agent by deliberate design, so neither the SDK nor a hypothetical "small core" should own an agents directory or parse agent frontmatter — doing so would push a multi-agent concept into a core that rejects it. (Earlier drafts of this plan suggested upstreaming `getProjectAgentsDir` to the SDK and having the core parse agent frontmatter; both are withdrawn for this reason.) - A cleaner evolution keeps the bridge generic, mirroring how pps already consumes the active-agent signal: the multi-agent extension that owns agent definitions (and already parses them) would supply the active agent's `permission:` overrides to pps through an extension-agnostic channel, so pps's core never locates or parses agent files. Until such a channel exists, pps encodes the `/.pi/agents` convention itself — which is what this fix does. - Optional symmetry: extract `getGlobalAgentsDir(agentDir)` and dedupe `join(agentDir, "agents")` across `derivePolicyLoaderOptions` and `defaultAgentsDir()`. Deferred — the global path is not buggy, and touching it widens the blast radius.