Skip to content

Commit 31a835a

Browse files
committed
Merge branch 'master' into query-file
2 parents 1f48c00 + 1e016b2 commit 31a835a

File tree

6 files changed

+196
-11
lines changed

6 files changed

+196
-11
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
## Unreleased
44

5+
## 0.67.0
6+
57
- Improved flaky test #150
68
- Obfuscate env vars in /doctor.
79
- Bump clj-otel to 0.2.10
810
- Rename $ARGS to $ARGUMENTS placeholder alias for custom commands.
11+
- Support recursive AGENTS.md file inclusions with @file mention. #140
912

1013
## 0.66.1
1114

docs/features.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ Here are the current supported contexts types:
7777

7878
#### AGENTS.md automatic context
7979

80-
ECA will always include if found the `AGENTS.md` file as context, searching for both `/project-root/AGENTS.md` and `~/.config/eca/AGENTS.md`.
80+
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.
8181

8282
You can ask ECA to create/update this file via `/init` command.
83+
you can check/debug what goes to final prompt with `/prompt-show` as well.
8384

8485
### Commands
8586

resources/ECA_VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.66.1
1+
0.67.0

src/eca/features/context.clj

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,50 @@
1515

1616
(def ^:private logger-tag "[CONTEXT]")
1717

18+
(defn ^:private extract-at-mentions
19+
"Extract all @path mentions from content. Supports relative and absolute paths with any extension."
20+
[content]
21+
(let [pattern #"@([^\s\)]+\.\w+)"
22+
matches (re-seq pattern content)]
23+
(map second matches)))
24+
25+
(defn ^:private parse-agents-file
26+
([path] (parse-agents-file path #{}))
27+
([path visited]
28+
(if (contains? visited path)
29+
[]
30+
(let [root-content (llm-api/refine-file-context path nil)
31+
visited' (conj visited path)
32+
at-mentions (extract-at-mentions root-content)
33+
parent-dir (str (fs/parent path))
34+
resolved-paths (map (fn [mention]
35+
(cond
36+
;; Absolute path
37+
(string/starts-with? mention "/")
38+
(str (fs/canonicalize (fs/file mention)))
39+
40+
;; Relative path (./... or ../...)
41+
(or (string/starts-with? mention "./")
42+
(string/starts-with? mention "../"))
43+
(str (fs/canonicalize (fs/file parent-dir mention)))
44+
45+
;; Simple filename, relative to current file's directory
46+
:else
47+
(str (fs/canonicalize (fs/file parent-dir mention)))))
48+
at-mentions)
49+
;; Deduplicate resolved paths
50+
unique-paths (distinct resolved-paths)
51+
;; Recursively parse all mentioned files
52+
nested-results (mapcat #(parse-agents-file % visited') unique-paths)]
53+
(concat [{:type :agents-file
54+
:path path
55+
:content root-content}]
56+
nested-results)))))
57+
1858
(defn agents-file-contexts
19-
"Search for AGENTS.md file both in workspaceRoot and global config dir."
59+
"Search for AGENTS.md file both in workspaceRoot and global config dir.
60+
Process any found @paths mentions recursively, supporting both relative and absolute paths.
61+
Deduplicates files to avoid reading the same file multiple times."
2062
[db]
2163
;; TODO make it customizable by behavior
2264
(let [agent-file "AGENTS.md"
@@ -28,13 +70,9 @@
2870
global-agent-file (let [agent-file (fs/path (config/global-config-dir) agent-file)]
2971
(when (fs/readable? agent-file)
3072
(fs/canonicalize agent-file)))]
31-
(mapv (fn [path]
32-
{:type :file
33-
:path (str path)
34-
:partial false
35-
:content (llm-api/refine-file-context (str path) nil)})
36-
(concat local-agent-files
37-
(when global-agent-file [global-agent-file])))))
73+
(->> (concat local-agent-files
74+
(when global-agent-file [global-agent-file]))
75+
(mapcat #(parse-agents-file (str %))))))
3876

3977
(defn ^:private file->refined-context [path lines-range]
4078
(let [ext (string/lower-case (fs/extension path))]

src/eca/features/prompt.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@
6868
:file (if partial
6969
(format "<file partial=true path=\"%s\">...\n%s\n...</file>\n" path content)
7070
(format "<file path=\"%s\">%s</file>\n" path content))
71+
:agents-file (multi-str
72+
(format "<agents-file description=\"Instructions following AGENTS.md spec.\" path=\"%s\">" path)
73+
content
74+
"</agents-file>\n")
7175
:repoMap (format "<repoMap description=\"Workspaces structure in a tree view, spaces represent file hierarchy\" >%s</repoMap>\n" @repo-map*)
7276
:cursor (format "<cursor description=\"User editor cursor position (line:character)\" path=\"%s\" start=\"%s\" end=\"%s\"/>\n"
7377
path

test/eca/features/context_test.clj

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
[eca.features.context :as f.context]
77
[eca.features.index :as f.index]
88
[eca.features.tools.mcp :as f.mcp]
9+
[eca.llm-api :as llm-api]
10+
[eca.shared :refer [multi-str]]
911
[eca.test-helper :as h]
10-
[matcher-combinators.test :refer [match?]]))
12+
[matcher-combinators.test :refer [match?]]
13+
[matcher-combinators.matchers :as m]))
1114

1215
(h/reset-components-before-test)
1316

@@ -182,3 +185,139 @@
182185
;; Entries from home listing
183186
(is (some #(= {:type "file" :path (str home "/.bashrc")} %) result))
184187
(is (some #(= {:type "directory" :path (str home "/projects")} %) result)))))))
188+
189+
(deftest parse-agents-file-test
190+
(testing "simple agents file with no path inclusion"
191+
(let [a-file (h/file-path "/fake/AGENTS.md")
192+
a-content (multi-str
193+
"- do foo")]
194+
(with-redefs [llm-api/refine-file-context (fn [p _l]
195+
(condp = p
196+
a-file a-content))]
197+
(is (match?
198+
(m/in-any-order
199+
[{:type :agents-file
200+
:path a-file
201+
:content a-content}])
202+
(#'f.context/parse-agents-file a-file))))))
203+
(testing "Single relative path inclusion"
204+
(let [a-file (h/file-path "/fake/AGENTS.md")
205+
a-content (multi-str
206+
"- do foo"
207+
"- follow @b.md")
208+
b-file (h/file-path "/fake/b.md")
209+
b-content (multi-str
210+
"- do bar")]
211+
(with-redefs [llm-api/refine-file-context (fn [p _l]
212+
(condp = p
213+
a-file a-content
214+
b-file b-content))]
215+
(is (match?
216+
(m/in-any-order
217+
[{:type :agents-file
218+
:path a-file
219+
:content a-content}
220+
{:type :agents-file
221+
:path b-file
222+
:content b-content}])
223+
(#'f.context/parse-agents-file a-file))))))
224+
(testing "Single absolute path inclusion"
225+
(let [a-file (h/file-path "/fake/AGENTS.md")
226+
a-content (multi-str
227+
"- do foo"
228+
"@/fake/src/b.md is where the nice things live")
229+
b-file (h/file-path "/fake/src/b.md")
230+
b-content (multi-str
231+
"- do bar")]
232+
(with-redefs [llm-api/refine-file-context (fn [p _l]
233+
(condp = p
234+
a-file a-content
235+
b-file b-content))]
236+
(is (match?
237+
(m/in-any-order
238+
[{:type :agents-file
239+
:path a-file
240+
:content a-content}
241+
{:type :agents-file
242+
:path b-file
243+
:content b-content}])
244+
(#'f.context/parse-agents-file a-file))))))
245+
(testing "Multiple path inclusions with different extensions"
246+
(let [a-file (h/file-path "/fake/AGENTS.md")
247+
a-content (multi-str
248+
"- do foo"
249+
"- check @./src/b.md for b things"
250+
"- also follow @../c.txt")
251+
b-file (h/file-path "/fake/src/b.md")
252+
b-content (multi-str
253+
"- do bar")
254+
c-file (h/file-path "/c.txt")
255+
c-content (multi-str
256+
"- do bazzz")]
257+
(with-redefs [llm-api/refine-file-context (fn [p _l]
258+
(condp = p
259+
a-file a-content
260+
b-file b-content
261+
c-file c-content))]
262+
(is (match?
263+
(m/in-any-order
264+
[{:type :agents-file
265+
:path a-file
266+
:content a-content}
267+
{:type :agents-file
268+
:path b-file
269+
:content b-content}
270+
{:type :agents-file
271+
:path c-file
272+
:content c-content}])
273+
(#'f.context/parse-agents-file a-file))))))
274+
(testing "Recursive path inclusions"
275+
(let [a-file (h/file-path "/fake/AGENTS.md")
276+
a-content (multi-str
277+
"- do foo"
278+
"- check @b.md for b things")
279+
b-file (h/file-path "/fake/b.md")
280+
b-content (multi-str
281+
"- check @../c.md")
282+
c-file (h/file-path "/c.md")
283+
c-content (multi-str
284+
"- do bazzz")]
285+
(with-redefs [llm-api/refine-file-context (fn [p _l]
286+
(condp = p
287+
a-file a-content
288+
b-file b-content
289+
c-file c-content))]
290+
(is (match?
291+
(m/in-any-order
292+
[{:type :agents-file
293+
:path a-file
294+
:content a-content}
295+
{:type :agents-file
296+
:path b-file
297+
:content b-content}
298+
{:type :agents-file
299+
:path c-file
300+
:content c-content}])
301+
(#'f.context/parse-agents-file a-file))))))
302+
(testing "Multiple mentions to same file include it once"
303+
(let [a-file (h/file-path "/fake/AGENTS.md")
304+
a-content (multi-str
305+
"- do foo"
306+
"- check @b.md for b things"
307+
"- make sure you check @b.md for sure")
308+
b-file (h/file-path "/fake/b.md")
309+
b-content (multi-str
310+
"- yeah")]
311+
(with-redefs [llm-api/refine-file-context (fn [p _l]
312+
(condp = p
313+
a-file a-content
314+
b-file b-content))]
315+
(is (match?
316+
(m/in-any-order
317+
[{:type :agents-file
318+
:path a-file
319+
:content a-content}
320+
{:type :agents-file
321+
:path b-file
322+
:content b-content}])
323+
(#'f.context/parse-agents-file a-file)))))))

0 commit comments

Comments
 (0)