Skip to content

Commit afb0c6c

Browse files
ericdalloeca-agent
andcommitted
Support AGENTS.md from parent directories via includeParentAgentsFiles flag
When working in nested workspaces (e.g. /a/b/c/d), project-wide guidance often lives in an AGENTS.md higher up the tree. Previously ECA only looked at the workspace root and the global config dir, forcing users to symlink or duplicate guidance. Adds a new top-level boolean config `includeParentAgentsFiles` (default false). When enabled, AGENTS.md files from each workspace folder's parent chain are included, ordered outermost parent first, then the workspace's own AGENTS.md, then the global file. A shared visited set deduplicates files across overlapping workspaces, and @-mention recursion is preserved. 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
1 parent 3e21116 commit afb0c6c

7 files changed

Lines changed: 183 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Support including `AGENTS.md` files from parent directories of each workspace folder via new `includeParentAgentsFiles` config flag (disabled by default), ordered outermost parent first.
6+
57
## 0.133.6
68

79
- Bugfix: `network.caCertFile` (and `clientCert`/`clientKey`/`clientKeyPassphrase`) set via `config.json` were silently ignored due to a key-case mismatch between config normalization and the network reader; only the env-var fallbacks worked. #457

docs/config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@
194194
"maximum": 100,
195195
"default": 75
196196
},
197+
"includeParentAgentsFiles": {
198+
"type": "boolean",
199+
"description": "When true, also include AGENTS.md files from each workspace folder's parent directories, ordered outermost parent first, then the workspace's own AGENTS.md.",
200+
"markdownDescription": "When `true`, also include `AGENTS.md` files from each workspace folder's parent directories, ordered outermost parent first, then the workspace's own `AGENTS.md`.",
201+
"default": false
202+
},
197203
"index": {
198204
"type": "object",
199205
"description": "Indexing configuration for workspace files.",

docs/features.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ So user can include those contexts in 3 different ways for different purposes:
111111

112112
ECA will always include if found the `AGENTS.md` file as context, searching for both `/project-root/AGENTS.md` and `~/.config/eca/AGENTS.md`, it will recursively check for any `@some-file.md` mention as well.
113113

114+
Set the config flag `includeParentAgentsFiles` to `true` to also include `AGENTS.md` files from each workspace's parent directories (up to the filesystem root). Files are emitted outermost parent first, then descending toward the workspace's own `AGENTS.md`, and duplicates across overlapping workspaces are skipped. The flag is disabled by default.
115+
114116
You can ask ECA to create/update this file via `/init` command.
115117
you can check/debug what goes to final prompt with `/prompt-show` as well.
116118

src/eca/config.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@
224224
:completion {:model "openai/gpt-4.1"}
225225
:netrcFile nil
226226
:autoCompactPercentage 75
227+
:includeParentAgentsFiles false
227228
:plugins {"eca" {:source "https://github.com/editor-code-assistant/eca-plugins.git"}}
228229
:remote {:enabled false}
229230
:env "prod"})

src/eca/features/chat.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@
11851185
:state :running
11861186
:text "Parsing given context"}))
11871187
refined-contexts (concat
1188-
(f.context/agents-file-contexts db)
1188+
(f.context/agents-file-contexts db config)
11891189
(f.context/raw-contexts->refined contexts db))
11901190
{static-rules :static path-scoped-rules :path-scoped} (f.rules/all-rules config (:workspace-folders db) agent full-model)
11911191
all-tools (f.tools/all-tools chat-id agent @db* config)

src/eca/features/context.clj

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,57 @@
5555
nested-results))
5656
[]))))
5757

58+
(defn ^:private ancestor-paths
59+
"Return all ancestor directories of `path` in outermost-first order.
60+
Excludes `path` itself."
61+
[path]
62+
(loop [p (fs/parent path)
63+
acc '()]
64+
(if (nil? p)
65+
(vec acc)
66+
(recur (fs/parent p) (conj acc p)))))
67+
68+
(defn ^:private safe-canonicalize
69+
"Canonicalize `path`, falling back to a non-resolved `fs/path` if the
70+
path does not exist (e.g. a stale workspace folder or test fixture)."
71+
[path]
72+
(try (fs/canonicalize path)
73+
(catch Exception _ (fs/path path))))
74+
5875
(defn agents-file-contexts
5976
"Search for AGENTS.md file both in workspaceRoot and global config dir.
77+
When `:includeParentAgentsFiles` is true in `config`, also include
78+
AGENTS.md files from each workspace's parent directories, ordered
79+
outermost parent first, then the workspace's own AGENTS.md.
6080
Process any found @paths mentions recursively, supporting both relative and absolute paths.
6181
Deduplicates files to avoid reading the same file multiple times."
62-
[db]
82+
[db config]
6383
;; TODO make it customizable by agent
64-
(let [agent-file "AGENTS.md"
65-
local-agent-files (keep (fn [{:keys [uri]}]
66-
(let [agent-file (fs/path (shared/uri->filename uri) agent-file)]
67-
(when (fs/readable? agent-file)
68-
(fs/canonicalize agent-file))))
69-
(:workspace-folders db))
70-
global-agent-file (let [agent-file (fs/path (shared/global-config-dir) agent-file)]
71-
(when (fs/readable? agent-file)
72-
(fs/canonicalize agent-file)))]
73-
(->> (concat local-agent-files
74-
(when global-agent-file [global-agent-file]))
75-
(mapcat #(parse-agents-file (str %))))))
84+
(let [agent-file-name "AGENTS.md"
85+
include-parents? (boolean (:includeParentAgentsFiles config))
86+
readable-agent-file-in (fn [dir]
87+
(let [p (fs/path dir agent-file-name)]
88+
(when (fs/readable? p)
89+
(str (fs/canonicalize p)))))
90+
{:keys [paths seen]}
91+
(reduce
92+
(fn [{:keys [paths seen]} {:keys [uri]}]
93+
(let [ws-path (safe-canonicalize (shared/uri->filename uri))
94+
candidate-dirs (cond-> []
95+
include-parents? (into (ancestor-paths ws-path))
96+
true (conj ws-path))
97+
new-paths (->> candidate-dirs
98+
(keep readable-agent-file-in)
99+
(remove seen))]
100+
{:paths (into paths new-paths)
101+
:seen (into seen new-paths)}))
102+
{:paths [] :seen #{}}
103+
(:workspace-folders db))
104+
global-agent-file (readable-agent-file-in (shared/global-config-dir))
105+
all-paths (cond-> paths
106+
(and global-agent-file (not (contains? seen global-agent-file)))
107+
(conj global-agent-file))]
108+
(mapcat parse-agents-file all-paths)))
76109

77110
(defn ^:private file->refined-context [path lines-range]
78111
(if (fs/readable? path)

test/eca/features/context_test.clj

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[eca.features.index :as f.index]
88
[eca.features.tools.mcp :as f.mcp]
99
[eca.llm-api :as llm-api]
10-
[eca.shared :refer [multi-str]]
10+
[eca.shared :as shared :refer [multi-str]]
1111
[eca.test-helper :as h]
1212
[matcher-combinators.matchers :as m]
1313
[matcher-combinators.test :refer [match?]]))
@@ -338,6 +338,130 @@
338338
:content a-content}])
339339
(#'f.context/parse-agents-file a-file))))))
340340

341+
(deftest agents-file-contexts-test
342+
(testing "Flag disabled: only the workspace's own AGENTS.md plus global"
343+
(h/reset-components!)
344+
(let [ws "/a/b/c/d"
345+
ws-agents (str ws "/AGENTS.md")
346+
global-dir "/global"
347+
global-agents (str global-dir "/AGENTS.md")
348+
readable #{ws-agents global-agents}]
349+
(swap! (h/db*) assoc :workspace-folders [{:uri "file:///a/b/c/d"}])
350+
(with-redefs [shared/uri->filename (constantly ws)
351+
shared/global-config-dir (constantly global-dir)
352+
fs/canonicalize identity
353+
fs/path (fn [& parts] (string/join "/" (map str parts)))
354+
fs/readable? (fn [p] (contains? readable (str p)))
355+
llm-api/refine-file-context (constantly "content")]
356+
(is (match?
357+
[{:type :agents-file :path ws-agents :content "content"}
358+
{:type :agents-file :path global-agents :content "content"}]
359+
(f.context/agents-file-contexts (h/db) {:includeParentAgentsFiles false}))))))
360+
361+
(testing "Flag enabled: parents emitted outermost first, then workspace, then global"
362+
(h/reset-components!)
363+
(let [ws "/a/b/c/d"
364+
parent-ab-agents "/a/b/AGENTS.md"
365+
parent-abc-agents "/a/b/c/AGENTS.md"
366+
global-dir "/global"
367+
global-agents (str global-dir "/AGENTS.md")
368+
readable #{parent-ab-agents parent-abc-agents global-agents}
369+
parent-map {"/a/b/c/d" "/a/b/c"
370+
"/a/b/c" "/a/b"
371+
"/a/b" "/a"
372+
"/a" "/"
373+
"/" nil}]
374+
(swap! (h/db*) assoc :workspace-folders [{:uri "file:///a/b/c/d"}])
375+
(with-redefs [shared/uri->filename (constantly ws)
376+
shared/global-config-dir (constantly global-dir)
377+
fs/canonicalize identity
378+
fs/parent (fn [p] (get parent-map (str p)))
379+
fs/path (fn [& parts] (string/join "/" (map str parts)))
380+
fs/readable? (fn [p] (contains? readable (str p)))
381+
llm-api/refine-file-context (constantly "content")]
382+
(is (match?
383+
[{:type :agents-file :path parent-ab-agents :content "content"}
384+
{:type :agents-file :path parent-abc-agents :content "content"}
385+
{:type :agents-file :path global-agents :content "content"}]
386+
(f.context/agents-file-contexts (h/db) {:includeParentAgentsFiles true}))))))
387+
388+
(testing "Flag enabled but no parent has AGENTS.md: behaves like disabled"
389+
(h/reset-components!)
390+
(let [ws "/a/b/c/d"
391+
ws-agents (str ws "/AGENTS.md")
392+
global-dir "/global"
393+
global-agents (str global-dir "/AGENTS.md")
394+
readable #{ws-agents global-agents}
395+
parent-map {"/a/b/c/d" "/a/b/c"
396+
"/a/b/c" "/a/b"
397+
"/a/b" "/a"
398+
"/a" "/"
399+
"/" nil}]
400+
(swap! (h/db*) assoc :workspace-folders [{:uri "file:///a/b/c/d"}])
401+
(with-redefs [shared/uri->filename (constantly ws)
402+
shared/global-config-dir (constantly global-dir)
403+
fs/canonicalize identity
404+
fs/parent (fn [p] (get parent-map (str p)))
405+
fs/path (fn [& parts] (string/join "/" (map str parts)))
406+
fs/readable? (fn [p] (contains? readable (str p)))
407+
llm-api/refine-file-context (constantly "content")]
408+
(is (match?
409+
[{:type :agents-file :path ws-agents :content "content"}
410+
{:type :agents-file :path global-agents :content "content"}]
411+
(f.context/agents-file-contexts (h/db) {:includeParentAgentsFiles true}))))))
412+
413+
(testing "Two nested workspaces share ancestors via dedup"
414+
(h/reset-components!)
415+
(let [a-agents "/a/AGENTS.md"
416+
ab-agents "/a/b/AGENTS.md"
417+
abc-agents "/a/b/c/AGENTS.md"
418+
global-dir "/global"
419+
global-agents (str global-dir "/AGENTS.md")
420+
readable #{a-agents ab-agents abc-agents global-agents}
421+
parent-map {"/a/b/c/d" "/a/b/c"
422+
"/a/b/c" "/a/b"
423+
"/a/b" "/a"
424+
"/a" "/"
425+
"/" nil}
426+
uri->filename-map {"file:///a/b" "/a/b"
427+
"file:///a/b/c/d" "/a/b/c/d"}]
428+
(swap! (h/db*) assoc :workspace-folders [{:uri "file:///a/b"}
429+
{:uri "file:///a/b/c/d"}])
430+
(with-redefs [shared/uri->filename (fn [u] (get uri->filename-map u))
431+
shared/global-config-dir (constantly global-dir)
432+
fs/canonicalize identity
433+
fs/parent (fn [p] (get parent-map (str p)))
434+
fs/path (fn [& parts] (string/join "/" (map str parts)))
435+
fs/readable? (fn [p] (contains? readable (str p)))
436+
llm-api/refine-file-context (constantly "content")]
437+
(is (match?
438+
[{:type :agents-file :path a-agents}
439+
{:type :agents-file :path ab-agents}
440+
{:type :agents-file :path abc-agents}
441+
{:type :agents-file :path global-agents}]
442+
(f.context/agents-file-contexts (h/db) {:includeParentAgentsFiles true}))))))
443+
444+
(testing "Workspace at filesystem root: no parent walk"
445+
(h/reset-components!)
446+
(let [ws "/"
447+
ws-agents "//AGENTS.md"
448+
global-dir "/global"
449+
global-agents (str global-dir "/AGENTS.md")
450+
readable #{ws-agents global-agents}
451+
parent-map {"/" nil}]
452+
(swap! (h/db*) assoc :workspace-folders [{:uri "file:///"}])
453+
(with-redefs [shared/uri->filename (constantly ws)
454+
shared/global-config-dir (constantly global-dir)
455+
fs/canonicalize identity
456+
fs/parent (fn [p] (get parent-map (str p)))
457+
fs/path (fn [& parts] (string/join "/" (map str parts)))
458+
fs/readable? (fn [p] (contains? readable (str p)))
459+
llm-api/refine-file-context (constantly "content")]
460+
(is (match?
461+
[{:type :agents-file :path ws-agents}
462+
{:type :agents-file :path global-agents}]
463+
(f.context/agents-file-contexts (h/db) {:includeParentAgentsFiles true})))))))
464+
341465
(deftest contexts-str-from-prompt-test
342466
(testing "not context mention"
343467
(is (match?

0 commit comments

Comments
 (0)