summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-26 10:49:41 -0500
committerGitHub <[email protected]>2026-01-26 10:49:41 -0500
commit39a73d4894bf7bda69a95b7d5572d5c7c24dd7ee (patch)
tree2b8572ffbb2a1c4ad6cafada94cd50da0bf07e2d
parentb1fbfa7e94d26aeeabdcc5b7963985b9b7c5d933 (diff)
downloadopencode-39a73d4894bf7bda69a95b7d5572d5c7c24dd7ee.tar.gz
opencode-39a73d4894bf7bda69a95b7d5572d5c7c24dd7ee.zip
feat: dynamically resolve AGENTS.md files from subdirectories as agent explores them (#10678)
-rw-r--r--packages/opencode/src/cli/cmd/debug/agent.ts1
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx25
-rw-r--r--packages/opencode/src/session/instruction.ts164
-rw-r--r--packages/opencode/src/session/message-v2.ts2
-rw-r--r--packages/opencode/src/session/prompt.ts9
-rw-r--r--packages/opencode/src/session/system.ts100
-rw-r--r--packages/opencode/src/tool/read.ts9
-rw-r--r--packages/opencode/src/tool/tool.ts1
-rw-r--r--packages/opencode/test/session/instruction.test.ts46
-rw-r--r--packages/opencode/test/tool/apply_patch.test.ts1
-rw-r--r--packages/opencode/test/tool/bash.test.ts1
-rw-r--r--packages/opencode/test/tool/external-directory.test.ts1
-rw-r--r--packages/opencode/test/tool/grep.test.ts1
-rw-r--r--packages/opencode/test/tool/question.test.ts1
-rw-r--r--packages/opencode/test/tool/read.test.ts24
-rw-r--r--packages/web/src/content/docs/rules.mdx2
16 files changed, 282 insertions, 106 deletions
diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts
index d1236ff40..fe3003485 100644
--- a/packages/opencode/src/cli/cmd/debug/agent.ts
+++ b/packages/opencode/src/cli/cmd/debug/agent.ts
@@ -153,6 +153,7 @@ async function createToolContext(agent: Agent.Info) {
callID: Identifier.ascending("part"),
agent: agent.name,
abort: new AbortController().signal,
+ messages: [],
metadata: () => {},
async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
for (const pattern of req.patterns) {
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index b3228c847..5f47562d2 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -1693,10 +1693,29 @@ function Glob(props: ToolProps<typeof GlobTool>) {
}
function Read(props: ToolProps<typeof ReadTool>) {
+ const { theme } = useTheme()
+ const loaded = createMemo(() => {
+ if (props.part.state.status !== "completed") return []
+ if (props.part.state.time.compacted) return []
+ const value = props.metadata.loaded
+ if (!value || !Array.isArray(value)) return []
+ return value.filter((p): p is string => typeof p === "string")
+ })
return (
- <InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
- Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
- </InlineTool>
+ <>
+ <InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
+ Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
+ </InlineTool>
+ <For each={loaded()}>
+ {(filepath) => (
+ <box paddingLeft={3}>
+ <text paddingLeft={3} fg={theme.textMuted}>
+ ↳ Loaded {normalizePath(filepath)}
+ </text>
+ </box>
+ )}
+ </For>
+ </>
)
}
diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts
new file mode 100644
index 000000000..d413e80f6
--- /dev/null
+++ b/packages/opencode/src/session/instruction.ts
@@ -0,0 +1,164 @@
+import path from "path"
+import os from "os"
+import { Global } from "../global"
+import { Filesystem } from "../util/filesystem"
+import { Config } from "../config/config"
+import { Instance } from "../project/instance"
+import { Flag } from "@/flag/flag"
+import { Log } from "../util/log"
+import type { MessageV2 } from "./message-v2"
+
+const log = Log.create({ service: "instruction" })
+
+const FILES = [
+ "AGENTS.md",
+ "CLAUDE.md",
+ "CONTEXT.md", // deprecated
+]
+
+function globalFiles() {
+ const files = [path.join(Global.Path.config, "AGENTS.md")]
+ if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
+ files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
+ }
+ if (Flag.OPENCODE_CONFIG_DIR) {
+ files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
+ }
+ return files
+}
+
+async function resolveRelative(instruction: string): Promise<string[]> {
+ if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
+ return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
+ }
+ if (!Flag.OPENCODE_CONFIG_DIR) {
+ log.warn(
+ `Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
+ )
+ return []
+ }
+ return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
+}
+
+export namespace InstructionPrompt {
+ export async function systemPaths() {
+ const config = await Config.get()
+ const paths = new Set<string>()
+
+ if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
+ for (const file of FILES) {
+ const matches = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
+ if (matches.length > 0) {
+ matches.forEach((p) => paths.add(path.resolve(p)))
+ break
+ }
+ }
+ }
+
+ for (const file of globalFiles()) {
+ if (await Bun.file(file).exists()) {
+ paths.add(path.resolve(file))
+ break
+ }
+ }
+
+ if (config.instructions) {
+ for (let instruction of config.instructions) {
+ if (instruction.startsWith("https://") || instruction.startsWith("http://")) continue
+ if (instruction.startsWith("~/")) {
+ instruction = path.join(os.homedir(), instruction.slice(2))
+ }
+ const matches = path.isAbsolute(instruction)
+ ? await Array.fromAsync(
+ new Bun.Glob(path.basename(instruction)).scan({
+ cwd: path.dirname(instruction),
+ absolute: true,
+ onlyFiles: true,
+ }),
+ ).catch(() => [])
+ : await resolveRelative(instruction)
+ matches.forEach((p) => paths.add(path.resolve(p)))
+ }
+ }
+
+ return paths
+ }
+
+ export async function system() {
+ const config = await Config.get()
+ const paths = await systemPaths()
+
+ const files = Array.from(paths).map(async (p) => {
+ const content = await Bun.file(p)
+ .text()
+ .catch(() => "")
+ return content ? "Instructions from: " + p + "\n" + content : ""
+ })
+
+ const urls: string[] = []
+ if (config.instructions) {
+ for (const instruction of config.instructions) {
+ if (instruction.startsWith("https://") || instruction.startsWith("http://")) {
+ urls.push(instruction)
+ }
+ }
+ }
+ const fetches = urls.map((url) =>
+ fetch(url, { signal: AbortSignal.timeout(5000) })
+ .then((res) => (res.ok ? res.text() : ""))
+ .catch(() => "")
+ .then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
+ )
+
+ return Promise.all([...files, ...fetches]).then((result) => result.filter(Boolean))
+ }
+
+ export function loaded(messages: MessageV2.WithParts[]) {
+ const paths = new Set<string>()
+ for (const msg of messages) {
+ for (const part of msg.parts) {
+ if (part.type === "tool" && part.tool === "read" && part.state.status === "completed") {
+ if (part.state.time.compacted) continue
+ const loaded = part.state.metadata?.loaded
+ if (!loaded || !Array.isArray(loaded)) continue
+ for (const p of loaded) {
+ if (typeof p === "string") paths.add(p)
+ }
+ }
+ }
+ }
+ return paths
+ }
+
+ export async function find(dir: string) {
+ for (const file of FILES) {
+ const filepath = path.resolve(path.join(dir, file))
+ if (await Bun.file(filepath).exists()) return filepath
+ }
+ }
+
+ export async function resolve(messages: MessageV2.WithParts[], filepath: string) {
+ const system = await systemPaths()
+ const already = loaded(messages)
+ const results: { filepath: string; content: string }[] = []
+
+ let current = path.dirname(path.resolve(filepath))
+ const root = path.resolve(Instance.directory)
+
+ while (current.startsWith(root)) {
+ const found = await find(current)
+ if (found && !system.has(found) && !already.has(found)) {
+ const content = await Bun.file(found)
+ .text()
+ .catch(() => undefined)
+ if (content) {
+ results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content })
+ }
+ }
+ if (current === root) break
+ current = path.dirname(current)
+ }
+
+ return results
+ }
+}
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 83ca72add..7c8689037 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -631,7 +631,7 @@ export namespace MessageV2 {
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message"),
}),
- async (input) => {
+ async (input): Promise<WithParts> => {
return {
info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
parts: await parts(input.messageID),
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index b3f11b335..23ca47354 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -15,6 +15,7 @@ import { Instance } from "../project/instance"
import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform"
import { SystemPrompt } from "./system"
+import { InstructionPrompt } from "./instruction"
import { Plugin } from "../plugin"
import PROMPT_PLAN from "../session/prompt/plan.txt"
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
@@ -386,6 +387,7 @@ export namespace SessionPrompt {
abort,
callID: part.callID,
extra: { bypassAgentCheck: true },
+ messages: msgs,
async metadata(input) {
await Session.updatePart({
...part,
@@ -561,6 +563,7 @@ export namespace SessionPrompt {
tools: lastUser.tools,
processor,
bypassAgentCheck,
+ messages: msgs,
})
if (step === 1) {
@@ -598,7 +601,7 @@ export namespace SessionPrompt {
agent,
abort,
sessionID,
- system: [...(await SystemPrompt.environment(model)), ...(await SystemPrompt.custom())],
+ system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())],
messages: [
...MessageV2.toModelMessages(sessionMessages, model),
...(isLastStep
@@ -650,6 +653,7 @@ export namespace SessionPrompt {
tools?: Record<string, boolean>
processor: SessionProcessor.Info
bypassAgentCheck: boolean
+ messages: MessageV2.WithParts[]
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
@@ -661,6 +665,7 @@ export namespace SessionPrompt {
callID: options.toolCallId,
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
agent: input.agent.name,
+ messages: input.messages,
metadata: async (val: { title?: string; metadata?: any }) => {
const match = input.processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
@@ -1008,6 +1013,7 @@ export namespace SessionPrompt {
agent: input.agent!,
messageID: info.id,
extra: { bypassCwdCheck: true, model },
+ messages: [],
metadata: async () => {},
ask: async () => {},
}
@@ -1069,6 +1075,7 @@ export namespace SessionPrompt {
agent: input.agent!,
messageID: info.id,
extra: { bypassCwdCheck: true },
+ messages: [],
metadata: async () => {},
ask: async () => {},
}
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index b055bd10e..d34a086fe 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -1,37 +1,14 @@
import { Ripgrep } from "../file/ripgrep"
-import { Global } from "../global"
-import { Filesystem } from "../util/filesystem"
-import { Config } from "../config/config"
-import { Log } from "../util/log"
import { Instance } from "../project/instance"
-import path from "path"
-import os from "os"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
-import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_CODEX from "./prompt/codex_header.txt"
import type { Provider } from "@/provider/provider"
-import { Flag } from "@/flag/flag"
-
-const log = Log.create({ service: "system-prompt" })
-
-async function resolveRelativeInstruction(instruction: string): Promise<string[]> {
- if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
- return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
- }
- if (!Flag.OPENCODE_CONFIG_DIR) {
- log.warn(
- `Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
- )
- return []
- }
- return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
-}
export namespace SystemPrompt {
export function instructions() {
@@ -72,81 +49,4 @@ export namespace SystemPrompt {
].join("\n"),
]
}
-
- const LOCAL_RULE_FILES = [
- "AGENTS.md",
- "CLAUDE.md",
- "CONTEXT.md", // deprecated
- ]
- const GLOBAL_RULE_FILES = [path.join(Global.Path.config, "AGENTS.md")]
- if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
- GLOBAL_RULE_FILES.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
- }
-
- if (Flag.OPENCODE_CONFIG_DIR) {
- GLOBAL_RULE_FILES.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
- }
-
- export async function custom() {
- const config = await Config.get()
- const paths = new Set<string>()
-
- // Only scan local rule files when project discovery is enabled
- if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
- for (const localRuleFile of LOCAL_RULE_FILES) {
- const matches = await Filesystem.findUp(localRuleFile, Instance.directory, Instance.worktree)
- if (matches.length > 0) {
- matches.forEach((path) => paths.add(path))
- break
- }
- }
- }
-
- for (const globalRuleFile of GLOBAL_RULE_FILES) {
- if (await Bun.file(globalRuleFile).exists()) {
- paths.add(globalRuleFile)
- break
- }
- }
-
- const urls: string[] = []
- if (config.instructions) {
- for (let instruction of config.instructions) {
- if (instruction.startsWith("https://") || instruction.startsWith("http://")) {
- urls.push(instruction)
- continue
- }
- if (instruction.startsWith("~/")) {
- instruction = path.join(os.homedir(), instruction.slice(2))
- }
- let matches: string[] = []
- if (path.isAbsolute(instruction)) {
- matches = await Array.fromAsync(
- new Bun.Glob(path.basename(instruction)).scan({
- cwd: path.dirname(instruction),
- absolute: true,
- onlyFiles: true,
- }),
- ).catch(() => [])
- } else {
- matches = await resolveRelativeInstruction(instruction)
- }
- matches.forEach((path) => paths.add(path))
- }
- }
-
- const foundFiles = Array.from(paths).map((p) =>
- Bun.file(p)
- .text()
- .catch(() => "")
- .then((x) => "Instructions from: " + p + "\n" + x),
- )
- const foundUrls = urls.map((url) =>
- fetch(url, { signal: AbortSignal.timeout(5000) })
- .then((res) => (res.ok ? res.text() : ""))
- .catch(() => "")
- .then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
- )
- return Promise.all([...foundFiles, ...foundUrls]).then((result) => result.filter(Boolean))
- }
}
diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts
index 3b1484cbc..028a007cc 100644
--- a/packages/opencode/src/tool/read.ts
+++ b/packages/opencode/src/tool/read.ts
@@ -8,6 +8,7 @@ import DESCRIPTION from "./read.txt"
import { Instance } from "../project/instance"
import { Identifier } from "../id/id"
import { assertExternalDirectory } from "./external-directory"
+import { InstructionPrompt } from "../session/instruction"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
@@ -59,6 +60,8 @@ export const ReadTool = Tool.define("read", {
throw new Error(`File not found: ${filepath}`)
}
+ const instructions = await InstructionPrompt.resolve(ctx.messages, filepath)
+
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
const isImage =
file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet"
@@ -72,6 +75,7 @@ export const ReadTool = Tool.define("read", {
metadata: {
preview: msg,
truncated: false,
+ ...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
},
attachments: [
{
@@ -133,12 +137,17 @@ export const ReadTool = Tool.define("read", {
LSP.touchFile(filepath, false)
FileTime.read(ctx.sessionID, filepath)
+ if (instructions.length > 0) {
+ output += `\n\n<system-reminder>\n${instructions.map((i) => i.content).join("\n\n")}\n</system-reminder>`
+ }
+
return {
title,
output,
metadata: {
preview,
truncated,
+ ...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
},
}
},
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
index 78ab325af..3d17ea192 100644
--- a/packages/opencode/src/tool/tool.ts
+++ b/packages/opencode/src/tool/tool.ts
@@ -20,6 +20,7 @@ export namespace Tool {
abort: AbortSignal
callID?: string
extra?: { [key: string]: any }
+ messages: MessageV2.WithParts[]
metadata(input: { title?: string; metadata?: M }): void
ask(input: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">): Promise<void>
}
diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts
new file mode 100644
index 000000000..2c44a266e
--- /dev/null
+++ b/packages/opencode/test/session/instruction.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, test } from "bun:test"
+import path from "path"
+import { InstructionPrompt } from "../../src/session/instruction"
+import { Instance } from "../../src/project/instance"
+import { tmpdir } from "../fixture/fixture"
+
+describe("InstructionPrompt.resolve", () => {
+ test("returns empty when AGENTS.md is at project root (already in systemPaths)", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(path.join(dir, "AGENTS.md"), "# Root Instructions")
+ await Bun.write(path.join(dir, "src", "file.ts"), "const x = 1")
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const system = await InstructionPrompt.systemPaths()
+ expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
+
+ const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"))
+ expect(results).toEqual([])
+ },
+ })
+ })
+
+ test("returns AGENTS.md from subdirectory (not in systemPaths)", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
+ await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const system = await InstructionPrompt.systemPaths()
+ expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
+
+ const results = await InstructionPrompt.resolve([], path.join(tmp.path, "subdir", "nested", "file.ts"))
+ expect(results.length).toBe(1)
+ expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
+ },
+ })
+ })
+})
diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts
index de48b074f..a08e23588 100644
--- a/packages/opencode/test/tool/apply_patch.test.ts
+++ b/packages/opencode/test/tool/apply_patch.test.ts
@@ -11,6 +11,7 @@ const baseCtx = {
callID: "",
agent: "build",
abort: AbortSignal.any([]),
+ messages: [],
metadata: () => {},
}
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index 750ff8193..454293c8f 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -12,6 +12,7 @@ const ctx = {
callID: "",
agent: "build",
abort: AbortSignal.any([]),
+ messages: [],
metadata: () => {},
ask: async () => {},
}
diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts
index b21f6a971..33c5e2c73 100644
--- a/packages/opencode/test/tool/external-directory.test.ts
+++ b/packages/opencode/test/tool/external-directory.test.ts
@@ -11,6 +11,7 @@ const baseCtx: Omit<Tool.Context, "ask"> = {
callID: "",
agent: "build",
abort: AbortSignal.any([]),
+ messages: [],
metadata: () => {},
}
diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts
index a79d93157..e774580df 100644
--- a/packages/opencode/test/tool/grep.test.ts
+++ b/packages/opencode/test/tool/grep.test.ts
@@ -10,6 +10,7 @@ const ctx = {
callID: "",
agent: "build",
abort: AbortSignal.any([]),
+ messages: [],
metadata: () => {},
ask: async () => {},
}
diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts
index c11b5ed6a..4a436186d 100644
--- a/packages/opencode/test/tool/question.test.ts
+++ b/packages/opencode/test/tool/question.test.ts
@@ -9,6 +9,7 @@ const ctx = {
callID: "test-call",
agent: "test-agent",
abort: AbortSignal.any([]),
+ messages: [],
metadata: () => {},
ask: async () => {},
}
diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts
index 7250bd2fd..afa14bc6c 100644
--- a/packages/opencode/test/tool/read.test.ts
+++ b/packages/opencode/test/tool/read.test.ts
@@ -14,6 +14,7 @@ const ctx = {
callID: "",
agent: "build",
abort: AbortSignal.any([]),
+ messages: [],
metadata: () => {},
ask: async () => {},
}
@@ -330,3 +331,26 @@ root_type Monster;`
})
})
})
+
+describe("tool.read loaded instructions", () => {
+ test("loads AGENTS.md from parent directory and includes in metadata", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Test Instructions\nDo something special.")
+ await Bun.write(path.join(dir, "subdir", "nested", "test.txt"), "test content")
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const read = await ReadTool.init()
+ const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "nested", "test.txt") }, ctx)
+ expect(result.output).toContain("test content")
+ expect(result.output).toContain("system-reminder")
+ expect(result.output).toContain("Test Instructions")
+ expect(result.metadata.loaded).toBeDefined()
+ expect(result.metadata.loaded).toContain(path.join(tmp.path, "subdir", "AGENTS.md"))
+ },
+ })
+ })
+})
diff --git a/packages/web/src/content/docs/rules.mdx b/packages/web/src/content/docs/rules.mdx
index 3a170019a..26e6de906 100644
--- a/packages/web/src/content/docs/rules.mdx
+++ b/packages/web/src/content/docs/rules.mdx
@@ -88,7 +88,7 @@ export OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1 # Disable only .claude/skills
When opencode starts, it looks for rule files in this order:
-1. **Local files** by traversing up from the current directory (`AGENTS.md`, `CLAUDE.md`, or `CONTEXT.md`)
+1. **Local files** by traversing up from the current directory (`AGENTS.md`, `CLAUDE.md`)
2. **Global file** at `~/.config/opencode/AGENTS.md`
3. **Claude Code file** at `~/.claude/CLAUDE.md` (unless disabled)