From 8ecf8a9883d1cb7c78d07c0fd64d32d6a1fd2c4c Mon Sep 17 00:00:00 2001 From: m3 Date: Wed, 5 Aug 2026 15:24:31 -0400 Subject: [PATCH] feat(coding-agent): support AGENTS.override.md (#7681) --- packages/coding-agent/README.md | 2 ++ packages/coding-agent/docs/quickstart.md | 2 ++ packages/coding-agent/docs/security.md | 2 +- packages/coding-agent/docs/usage.md | 2 ++ .../coding-agent/src/core/resource-loader.ts | 4 ++-- packages/coding-agent/src/core/tools/read.ts | 2 +- .../coding-agent/test/resource-loader.test.ts | 24 ++++++++++++++++++- .../test/tool-execution-component.test.ts | 8 +++++++ 8 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 5a6a56073..f1b950341 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -324,6 +324,8 @@ Pi loads `AGENTS.md` (or `CLAUDE.md`) at startup from: - Parent directories (walking up from cwd) - Current directory +If a directory contains `AGENTS.override.md`, Pi loads it instead of `AGENTS.md` or `CLAUDE.md` from that directory. Context files from other directories are still concatenated. + Use for project instructions (`AGENTS.md`/`CLAUDE.md`), conventions, common commands. All matching files are concatenated. Disable context file loading with `--no-context-files` (or `-nc`). diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index 89314336e..a8b63c21c 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -100,6 +100,8 @@ Pi loads: - `~/.pi/agent/AGENTS.md` for global instructions - `AGENTS.md` or `CLAUDE.md` from parent directories and the current directory +If a directory contains `AGENTS.override.md`, Pi loads it instead of `AGENTS.md` or `CLAUDE.md` from that directory. + Restart pi, or run `/reload`, after changing context files. ## Common things to try diff --git a/packages/coding-agent/docs/security.md b/packages/coding-agent/docs/security.md index 29828e034..ebd3e52a3 100644 --- a/packages/coding-agent/docs/security.md +++ b/packages/coding-agent/docs/security.md @@ -24,7 +24,7 @@ Trusting a project allows pi to load project resources that require trust, inclu - missing project packages configured through project settings - project-local extensions and project package-managed extensions -Declining trust skips protected resources. `AGENTS.md` and `CLAUDE.md` context files are loaded regardless of project trust unless context loading is disabled. Before trust is resolved, pi only loads context files, user/global extensions, and CLI `-e` extensions. User/global and CLI extensions can handle the `project_trust` event; the first extension that returns a yes/no decision owns the decision. +Declining trust skips protected resources. Context files such as `AGENTS.override.md`, `AGENTS.md`, and `CLAUDE.md` are loaded regardless of project trust unless context loading is disabled. Before trust is resolved, pi only loads context files, user/global extensions, and CLI `-e` extensions. User/global and CLI extensions can handle the `project_trust` event; the first extension that returns a yes/no decision owns the decision. Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, `defaultProjectTrust: "ask"` and `"never"` ignore such resources, while `"always"` trusts them. Use `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 3b1da16f1..9588bd79f 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -103,6 +103,8 @@ Pi loads `AGENTS.md` or `CLAUDE.md` at startup from: - parent directories, walking up from the current working directory - the current directory +If a directory contains `AGENTS.override.md`, Pi loads it instead of `AGENTS.md` or `CLAUDE.md` from that directory. Context files from other directories still layer normally. + Use context files for project conventions, commands, safety rules, and preferences. Disable loading with `--no-context-files` or `-nc`. ### System Prompt Files diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 3102a27e0..c24d0a210 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -68,7 +68,7 @@ function resolvePromptInput(input: string | undefined, description: string): str } function loadContextFileFromDir(dir: string): { path: string; content: string } | null { - const candidates = ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]; + const candidates = ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]; for (const filename of candidates) { const filePath = join(dir, filename); if (existsSync(filePath)) { @@ -90,7 +90,7 @@ function loadContextFileFromDir(dir: string): { path: string; content: string } /** * The main repo's context file that a nested linked worktree's own copy shadows: both - * are the same tracked AGENTS.md/CLAUDE.md, so loading both loads it twice. Returns + * occupy the same logical repository scope, so loading both applies that context twice. Returns * undefined when nothing is shadowed, leaving normal ancestor inheritance alone. * * Returned canonicalized (realpath), because `git worktree add` writes the `.git` diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 42f3058c3..9442e9873 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -34,7 +34,7 @@ interface CompactReadClassification { label: string; } -const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]); +const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]); /** * Pluggable operations for the read tool. diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index a437fe976..145eca3b2 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -355,7 +355,27 @@ Content`, expect(agentsFiles.some((f) => f.path.includes("AGENTS.md"))).toBe(true); }); + it("should prefer AGENTS.override.md within each directory while preserving ancestor layering", async () => { + const nestedCwd = join(cwd, "service"); + mkdirSync(nestedCwd); + writeFileSync(join(agentDir, "AGENTS.md"), "global instructions"); + writeFileSync(join(agentDir, "AGENTS.override.md"), "global override"); + writeFileSync(join(cwd, "AGENTS.md"), "project instructions"); + writeFileSync(join(nestedCwd, "AGENTS.md"), "service instructions"); + writeFileSync(join(nestedCwd, "AGENTS.override.md"), "service override"); + + const loader = new DefaultResourceLoader({ cwd: nestedCwd, agentDir }); + await loader.reload(); + + expect(loader.getAgentsFiles().agentsFiles).toEqual([ + { path: join(agentDir, "AGENTS.override.md"), content: "global override" }, + { path: join(cwd, "AGENTS.md"), content: "project instructions" }, + { path: join(nestedCwd, "AGENTS.override.md"), content: "service override" }, + ]); + }); + it("should ignore context file candidates that are directories", async () => { + mkdirSync(join(cwd, "AGENTS.override.md")); mkdirSync(join(cwd, "AGENTS.md")); writeFileSync(join(cwd, "CLAUDE.md"), "Fallback instructions"); const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -368,10 +388,12 @@ Content`, content: "Fallback instructions", }); expect(consoleError).not.toHaveBeenCalledWith(expect.stringContaining(join(cwd, "AGENTS.md"))); + expect(consoleError).not.toHaveBeenCalledWith(expect.stringContaining(join(cwd, "AGENTS.override.md"))); consoleError.mockRestore(); }); - it("should skip AGENTS.md and CLAUDE.md discovery when noContextFiles is true", async () => { + it("should skip context file discovery when noContextFiles is true", async () => { + writeFileSync(join(cwd, "AGENTS.override.md"), "# Override Guidelines\n\nBe helpful."); writeFileSync(join(cwd, "AGENTS.md"), "# Project Guidelines\n\nBe helpful."); writeFileSync(join(cwd, "CLAUDE.md"), "# Claude Guidelines\n\nBe helpful."); diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 096617103..49b3754ae 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -461,6 +461,14 @@ describe("ToolExecutionComponent parity", () => { hidden: "Hidden resource instructions", absent: undefined, }, + { + title: "AGENTS.override.md", + path: join(process.cwd(), ".pi", "AGENTS.override.md"), + content: "Hidden override instructions", + compact: "read resource .pi/AGENTS.override.md", + hidden: "Hidden override instructions", + absent: undefined, + }, { title: "outside AGENTS.md", path: resolve(process.cwd(), "..", "AGENTS.md"),