mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 18:03:27 +00:00
feat(core): discover AGENTS.md up to the home directory (#41652)
This commit is contained in:
parent
17536a9237
commit
136bad9f17
4 changed files with 86 additions and 15 deletions
|
|
@ -27,8 +27,10 @@ export const Plugin = define({
|
|||
const changes = yield* PubSub.sliding<string>(1)
|
||||
const lock = Semaphore.makeUnsafe(1)
|
||||
const start = yield* fs.resolve(location.directory)
|
||||
const stop = yield* fs.resolve(location.project.directory)
|
||||
const project = discovery.project && FSUtil.contains(stop, start)
|
||||
const root = yield* fs.resolve(location.project.directory)
|
||||
const home = yield* fs.resolve(global.home)
|
||||
const project = discovery.project && FSUtil.contains(root, start)
|
||||
const stop = FSUtil.contains(home, start) ? home : root
|
||||
const globalFile = yield* fs.resolve(join(global.config, "AGENTS.md"))
|
||||
const loaded: { current: Loaded } = { current: { type: "available", files: [] } }
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const it = testEffect(Layer.empty)
|
|||
|
||||
const instructionLayer = (input: {
|
||||
config?: string
|
||||
home?: string
|
||||
locationServiceLayer: Layer.Layer<Location.Service>
|
||||
filesystemLayer?: Layer.Layer<FSUtil.Service>
|
||||
project?: boolean
|
||||
|
|
@ -34,7 +35,15 @@ const instructionLayer = (input: {
|
|||
LayerNode.group([InstructionDiscovery.node, Bus.node, FSUtil.node, Global.node, Location.node, Watcher.node]),
|
||||
[
|
||||
[InstructionDiscovery.node, InstructionDiscovery.configured({ project: input.project })],
|
||||
[Global.node, input.config ? Global.layerWith({ config: input.config }) : tempGlobalLayer],
|
||||
[
|
||||
Global.node,
|
||||
input.config || input.home
|
||||
? Global.layerWith({
|
||||
...(input.config ? { config: input.config } : {}),
|
||||
...(input.home ? { home: input.home } : {}),
|
||||
})
|
||||
: tempGlobalLayer,
|
||||
],
|
||||
[Location.node, input.locationServiceLayer],
|
||||
[Watcher.node, watcher],
|
||||
...(input.filesystemLayer ? [[FSUtil.node, input.filesystemLayer] as const] : []),
|
||||
|
|
@ -112,10 +121,13 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
const global = path.join(tmp.path, "global")
|
||||
const project = path.join(tmp.path, "project")
|
||||
const home = path.join(tmp.path, "home")
|
||||
const shared = path.join(home, "code")
|
||||
const project = path.join(shared, "repo")
|
||||
const directory = path.join(project, "packages", "core")
|
||||
const outside = path.join(tmp.path, "AGENTS.md")
|
||||
const globalFile = path.join(global, "AGENTS.md")
|
||||
const sharedFile = path.join(shared, "AGENTS.md")
|
||||
const projectFile = path.join(project, "AGENTS.md")
|
||||
const packageFile = path.join(directory, "AGENTS.md")
|
||||
return Effect.gen(function* () {
|
||||
|
|
@ -124,6 +136,7 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
await fs.mkdir(directory, { recursive: true })
|
||||
await fs.writeFile(outside, "outside")
|
||||
await fs.writeFile(globalFile, "global")
|
||||
await fs.writeFile(sharedFile, "shared")
|
||||
await fs.writeFile(projectFile, "project")
|
||||
await fs.writeFile(packageFile, "package")
|
||||
})
|
||||
|
|
@ -135,13 +148,20 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
{ path: packageFile, type: "file" },
|
||||
{ path: path.join(project, "packages", "AGENTS.md"), type: "file" },
|
||||
{ path: projectFile, type: "file" },
|
||||
{ path: sharedFile, type: "file" },
|
||||
{ path: path.join(home, "AGENTS.md"), type: "file" },
|
||||
])
|
||||
expect(yield* watcher.subscriptions()).not.toContainEqual({
|
||||
path: path.join(tmp.path, "AGENTS.md"),
|
||||
type: "file",
|
||||
})
|
||||
const initialized = yield* readInitial(yield* discovery.load())
|
||||
expect(initialized.text).toBe(
|
||||
[
|
||||
`Instructions from: ${globalFile}\nglobal`,
|
||||
`Instructions from: ${packageFile}\npackage`,
|
||||
`Instructions from: ${projectFile}\nproject`,
|
||||
`Instructions from: ${sharedFile}\nshared`,
|
||||
].join("\n\n"),
|
||||
)
|
||||
expect(initialized.text).not.toContain("outside")
|
||||
|
|
@ -159,6 +179,7 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
"These instructions replace all previously loaded ambient instructions.",
|
||||
`Instructions from: ${globalFile}\nglobal`,
|
||||
`Instructions from: ${projectFile}\nproject`,
|
||||
`Instructions from: ${sharedFile}\nshared`,
|
||||
].join("\n\n"),
|
||||
)
|
||||
|
||||
|
|
@ -166,6 +187,8 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
yield* emitAndWait({ type: "delete", path: globalFile })
|
||||
yield* Effect.promise(() => fs.rm(projectFile))
|
||||
yield* emitAndWait({ type: "delete", path: projectFile })
|
||||
yield* Effect.promise(() => fs.rm(sharedFile))
|
||||
yield* emitAndWait({ type: "delete", path: sharedFile })
|
||||
expect((yield* readUpdate(yield* discovery.load(), initialized)).text).toBe(
|
||||
"Previously loaded instructions no longer apply.",
|
||||
)
|
||||
|
|
@ -173,6 +196,7 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
Effect.provide(
|
||||
instructionLayer({
|
||||
config: global,
|
||||
home,
|
||||
locationServiceLayer: Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
|
|
@ -215,15 +239,17 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
),
|
||||
)
|
||||
|
||||
it.live("discovers a newly created instruction file in an intermediate directory", () =>
|
||||
it.live("discovers a newly created instruction file above the project root", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
const project = path.join(tmp.path, "project")
|
||||
const intermediate = path.join(project, "packages", "AGENTS.md")
|
||||
const directory = path.join(project, "packages", "core")
|
||||
const home = path.join(tmp.path, "home")
|
||||
const shared = path.join(home, "code")
|
||||
const project = path.join(shared, "repo")
|
||||
const intermediate = path.join(shared, "AGENTS.md")
|
||||
const directory = path.join(project, "core")
|
||||
const projectFile = path.join(project, "AGENTS.md")
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
|
||||
|
|
@ -235,7 +261,7 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
yield* emitAndWait({ type: "create", path: intermediate })
|
||||
|
||||
expect((yield* readInitial(yield* discovery.load())).text).toBe(
|
||||
[`Instructions from: ${intermediate}\nintermediate`, `Instructions from: ${projectFile}\nproject`].join(
|
||||
[`Instructions from: ${projectFile}\nproject`, `Instructions from: ${intermediate}\nintermediate`].join(
|
||||
"\n\n",
|
||||
),
|
||||
)
|
||||
|
|
@ -243,6 +269,48 @@ describe("ConfigInstructionPlugin.Plugin", () => {
|
|||
Effect.provide(
|
||||
instructionLayer({
|
||||
config: path.join(tmp.path, "global"),
|
||||
home,
|
||||
locationServiceLayer: Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
location(
|
||||
{ directory: AbsolutePath.make(directory) },
|
||||
{ projectDirectory: AbsolutePath.make(project) },
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("stops instruction candidates at the project root outside home", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
const global = path.join(tmp.path, "global")
|
||||
const home = path.join(tmp.path, "home")
|
||||
const project = path.join(tmp.path, "scratch", "repo")
|
||||
const directory = path.join(project, "packages", "core")
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
|
||||
yield* start()
|
||||
const watcher = yield* Watcher.Test
|
||||
expect(yield* watcher.subscriptions()).toEqual([
|
||||
{ path: path.join(global, "AGENTS.md"), type: "file" },
|
||||
{ path: path.join(directory, "AGENTS.md"), type: "file" },
|
||||
{ path: path.join(project, "packages", "AGENTS.md"), type: "file" },
|
||||
{ path: path.join(project, "AGENTS.md"), type: "file" },
|
||||
])
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
instructionLayer({
|
||||
config: global,
|
||||
home,
|
||||
locationServiceLayer: Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ V2 loads:
|
|||
|
||||
1. The global file at `$XDG_CONFIG_HOME/opencode/AGENTS.md`, normally
|
||||
`~/.config/opencode/AGENTS.md`.
|
||||
2. Every `AGENTS.md` from the current Location up to and including the project
|
||||
root.
|
||||
2. Every `AGENTS.md` from the current Location up to and including the home
|
||||
directory when the Location is inside it. For Locations outside home, the
|
||||
scan stops at the project root.
|
||||
|
||||
For example, when the Location is `packages/web`, OpenCode can load all three
|
||||
project files below:
|
||||
|
|
@ -35,9 +36,9 @@ my-project/
|
|||
```
|
||||
|
||||
The files are combined rather than selecting a single winner. They are rendered
|
||||
in this order: global, then project files from the Location toward the project
|
||||
in this order: global, then files from the Location toward home or the project
|
||||
root. OpenCode does not resolve conflicts between their contents, so keep broad
|
||||
guidance global and put scoped guidance in the relevant project directory.
|
||||
guidance global and put scoped guidance in the relevant directory.
|
||||
|
||||
If the Location is outside the project root, only the global file is loaded.
|
||||
Setting `OPENCODE_DISABLE_PROJECT_CONFIG=1` also skips project `AGENTS.md`
|
||||
|
|
|
|||
|
|
@ -504,8 +504,8 @@ require a V2 rewrite. See [Skills](/skills).
|
|||
|
||||
### Instruction files
|
||||
|
||||
Existing `AGENTS.md` files stay in place. V2 discovers the global `~/.config/opencode/AGENTS.md` and project `AGENTS.md`
|
||||
files from the current directory up to the project root.
|
||||
Existing `AGENTS.md` files stay in place. V2 discovers the global `~/.config/opencode/AGENTS.md` and ambient `AGENTS.md`
|
||||
files from the current directory up to home. For projects outside home, discovery stops at the project root.
|
||||
|
||||
If a V1 setup relied on a `CLAUDE.md` fallback, move that guidance into the applicable `AGENTS.md`. V2 currently only
|
||||
discovers `AGENTS.md`; because non-API V1 behavior is intended to remain compatible, also run `/report` with the affected
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue