From 0834b79e39276a1866eff218bfd378e8f0a4d680 Mon Sep 17 00:00:00 2001 From: sry27 Date: Fri, 19 Jun 2026 02:12:07 +0800 Subject: [PATCH] feat: support AGENTS.local.md as personal local instruction override Add a .local.md personal local instruction override layer in Instruction.systemPaths (packages/opencode/src/session/instruction.ts), stacked on top of the main AGENTS.md/CLAUDE.md (unchanged): - Candidates AGENTS.local.md / CLAUDE.local.md, first-match-wins (only one is loaded - AGENTS.local.md wins if present, CLAUDE.local.md is then skipped). - Gated behind the existing MIMOCODE_DISABLE_CLAUDE_CODE_PROMPT flag, and sits inside the MIMOCODE_DISABLE_PROJECT_CONFIG gate. - All ancestor matches of the winning candidate within the worktree are loaded via the existing fs.findUp. Includes a CLAUDE.local.md template as a reference for personal no-upstream-pollution constraints. Closes #872 --- CLAUDE.local.md | 10 +++ packages/opencode/src/session/instruction.ts | 13 +++ .../opencode/test/session/instruction.test.ts | 80 +++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 CLAUDE.local.md diff --git a/CLAUDE.local.md b/CLAUDE.local.md new file mode 100644 index 000000000..57a116408 --- /dev/null +++ b/CLAUDE.local.md @@ -0,0 +1,10 @@ +# CLAUDE.local.md + +--- + +## [最高行为准则 - 严禁污染上游] + +- 严禁以任何形式修改、暂存(stage)或提交项目全局的 `AGENTS.md` 或 `CLAUDE.md`,它们对你而言是绝对只读的。 +- 所有个人偏好、本地工作流或提示词调整,只能写入当前本地文件(`AGENTS.local.md` 或 `CLAUDE.local.md`)。 + +--- diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 2006f23d1..068fdaa5b 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -149,6 +149,19 @@ export const layer: Layer.Layer 0) { + local.forEach((item) => paths.add(path.resolve(item))) + break // first-match-wins + } + } + } } for (const file of globalFiles()) { diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index b5bba50cf..f40b55668 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -270,6 +270,86 @@ describe("Instruction.system", () => { } } }) + + test("loads CLAUDE.local.md alongside AGENTS.md (stacking)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions") + await Bun.write(path.join(dir, "CLAUDE.local.md"), "# Personal Local") + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: () => + run( + Instruction.Service.use((svc) => + Effect.gen(function* () { + const paths = yield* svc.systemPaths() + expect(paths.has(path.join(tmp.path, "AGENTS.md"))).toBe(true) + expect(paths.has(path.join(tmp.path, "CLAUDE.local.md"))).toBe(true) + + const rules = (yield* svc.system()).content + expect(rules).toContain( + `Instructions from: ${path.join(tmp.path, "CLAUDE.local.md")}\n# Personal Local`, + ) + }), + ), + ), + }) + }) + + test("loads CLAUDE.local.md alongside CLAUDE.md fallback (no AGENTS.md)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "CLAUDE.md"), "# Claude Instructions") + await Bun.write(path.join(dir, "CLAUDE.local.md"), "# Personal Local") + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: () => + run( + Instruction.Service.use((svc) => + Effect.gen(function* () { + const paths = yield* svc.systemPaths() + expect(paths.has(path.join(tmp.path, "CLAUDE.md"))).toBe(true) + expect(paths.has(path.join(tmp.path, "CLAUDE.local.md"))).toBe(true) + }), + ), + ), + }) + }) + + test("prefers AGENTS.local.md over CLAUDE.local.md (first-match-wins)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions") + await Bun.write(path.join(dir, "AGENTS.local.md"), "# Agents Local") + await Bun.write(path.join(dir, "CLAUDE.local.md"), "# Claude Local") + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: () => + run( + Instruction.Service.use((svc) => + Effect.gen(function* () { + const paths = yield* svc.systemPaths() + expect(paths.has(path.join(tmp.path, "AGENTS.local.md"))).toBe(true) + expect(paths.has(path.join(tmp.path, "CLAUDE.local.md"))).toBe(false) + + const rules = (yield* svc.system()).content + expect(rules).toContain( + `Instructions from: ${path.join(tmp.path, "AGENTS.local.md")}\n# Agents Local`, + ) + expect(rules).not.toContain( + `Instructions from: ${path.join(tmp.path, "CLAUDE.local.md")}\n# Claude Local`, + ) + }), + ), + ), + }) + }) }) describe("Instruction.systemPaths MIMOCODE_CONFIG_DIR", () => {