Skip to content

Commit 8adc38d

Browse files
authored
Merge pull request #438 from editor-code-assistant/fix/path-rule-fetch-one
Fix repeated path-scoped rule fetch enforcement
2 parents ae82a86 + 7cf11b3 commit 8adc38d

9 files changed

Lines changed: 134 additions & 96 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Fix inline completion crash when renewing auth tokens before completion requests. #437
1111
- Bugfix: avoid `Divide by zero` crash in chat auto-compact when models.dev reports `0` for a model's context/output limits (e.g. `openai/chatgpt-image-latest`); such limits are now normalized to `nil` and `auto-compact?` skips models without a known positive context window.
1212
- Bugfix: image edit follow-up turns no longer fail on the OpenAI Responses API when prior generations are replayed; generated images are now persisted under a dedicated `image_generation_call` history role and replayed as a user-role `input_image` data URL across providers.
13+
- Bugfix: path-scoped rule enforcement now treats a fetched rule as loaded for the current chat, so matching files do not require fetching the same rule again.
1314

1415
- Support regex patterns in markdown agent tool entries (e.g. `eca__shell_command(npm run .*)`) for fine-grained tool approval, currently limited to `eca__shell_command`.
1516

docs/config/rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ Use this rule of thumb:
181181
- Add `enforce: read` or `enforce: modify` when the model must fetch the matching rule before using the corresponding builtin file tool.
182182
- Use `agent` and `model` filters when the rule is only relevant for specific chat modes or model families.
183183

184-
Path-scoped rules keep the base prompt smaller while still making file-specific guidance available. When the model calls `fetch_rule`, ECA validates the exact rule id and absolute target path, renders the rule content, and records that the rule was fetched for that path in the current chat. You can also use this to influence behavior for a specific provider. For example, if you want more tool calls instead of user prompts, you can do something like:
184+
Path-scoped rules keep the base prompt smaller while still making file-specific guidance available. When the model calls `fetch_rule`, ECA validates the exact rule id and absolute target path, renders the rule content, and records that the rule was fetched in the current chat. You can also use this to influence behavior for a specific provider. For example, if you want more tool calls instead of user prompts, you can do something like:
185185
```markdown title=".eca/rules/copilot-ask-user.md"
186186
---
187187
model: "github-copilot/.*"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
Fetch the full content of a path-scoped rule by its exact id and the exact absolute target path you plan to work with.
22
Path-scoped rules are listed in the system prompt and also repeated in this tool description with their id, name, scope, workspace root when relevant, path patterns, and enforce attribute.
33
Path matching uses Java NIO `PathMatcher` glob syntax against workspace-relative paths. Unlike most editor and shell-style glob matchers, patterns containing `**/` do not match the zero-directory case: `**/*.clj` does not match `foo.clj`, and `src/**/*.clj` matches nested files under `src/` but not `src/foo.clj`.
4-
Each rule has an enforce attribute that determines when you must fetch it: `modify` (default) means fetch before editing a matching file; `read` means fetch before reading; `modify,read` means fetch before both. Copy the exact rule id from the catalog, pass the exact absolute target path, and call this tool to validate the match and get the rule's full content. If the tool reports a mismatch, choose a different rule or correct the path. Fetch each matching rule only once per target path per chat — once you have the tool output, you don't need to fetch it again. Re-fetching a previously fetched rule for the same path will return a short confirmation instead of the full content.
4+
Each rule has an enforce attribute that determines when you must fetch it: `modify` (default) means fetch before editing a matching file; `read` means fetch before reading; `modify,read` means fetch before both. Copy the exact rule id from the catalog, pass the exact absolute target path, and call this tool to validate the match and get the rule's full content. If the tool reports a mismatch, choose a different rule or correct the path. Fetch each matching rule only once per chat — once you have the tool output, the same rule does not need to be fetched again for other paths it matches. Re-fetching a previously fetched rule will return a short confirmation instead of the full content.

src/eca/features/prompt.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@
202202
(remove #(= :global (:scope %)))
203203
(map :content))
204204
path-scoped-section (when (and fetch-rule-available? (seq path-scoped-rules))
205-
["<path-scoped-rules description=\"Rules that apply to matching file paths. Use fetch_rule before actions required by enforce (read, modify, or both). Each rule only needs to be fetched once per target path.\">"
205+
["<path-scoped-rules description=\"Rules that apply to matching file paths. Use fetch_rule before actions required by enforce (read, modify, or both). Each rule only needs to be fetched once per chat.\">"
206206
(path-scoped-rule-catalog path-scoped-rules)
207207
"</path-scoped-rules>"])
208208
has-static-rules? (seq rendered-static-rules)]

src/eca/features/tools/fetch_rule.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@
7070
:contents [{:type :text
7171
:text (path-mismatch-message rule match-info)}]}
7272

73-
(f.tools.path-rules/validated-rule? db chat-id (:path match-info) rule-id)
73+
(f.tools.path-rules/validated-rule? db chat-id rule-id)
7474
{:error false
7575
:contents [{:type :text
76-
:text (str "**" (:name rule) "** — already loaded for this path, reuse the previously fetched content.")}]}
76+
:text (str "**" (:name rule) "** — already loaded in this chat, reuse the previously fetched content.")}]}
7777

7878
:else
7979
(do
@@ -87,7 +87,7 @@
8787
:text (str header "\n" content)}]}
8888
{:error false
8989
:contents [{:type :text
90-
:text (str header "\nThis rule contains no usable content for the current chat context and does not need to be loaded again for this path.")}]}))))))
90+
:text (str header "\nThis rule contains no usable content for the current chat context and does not need to be loaded again in this chat.")}]}))))))
9191

9292
(defn ^:private describe-rule
9393
[{rule-name :name :keys [id paths enforce scope workspace-root]}]

src/eca/features/tools/path_rules.clj

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,12 @@
1414
(tools.util/tool-available? all-tools "eca__fetch_rule"))
1515

1616
(defn record-validated-rule!
17-
[db* chat-id rule match-info]
18-
(when-let [path (some-> (:path match-info) shared/normalize-path)]
19-
(swap! db* assoc-in [:chats chat-id validated-path-rules-key path (:id rule)]
20-
{:matched-pattern (:matched-pattern match-info)
21-
:rule-path (:path rule)
22-
:workspace-root (:workspace-root match-info)})))
17+
[db* chat-id rule _match-info]
18+
(swap! db* update-in [:chats chat-id validated-path-rules-key] (fnil conj #{}) (:id rule)))
2319

2420
(defn validated-rule?
25-
[db chat-id target-path rule-id]
26-
(boolean
27-
(get-in db [:chats chat-id validated-path-rules-key (shared/normalize-path target-path) rule-id])))
21+
[db chat-id rule-id]
22+
(contains? (get-in db [:chats chat-id validated-path-rules-key] #{}) rule-id))
2823

2924
(defn enforce-on-modify?
3025
"Returns true if the rule should be enforced before file modification.
@@ -57,7 +52,7 @@
5752
(when target-path
5853
(->> (applicable-path-scoped-rules config db chat-id agent all-tools target-path)
5954
(filter (fn [{:keys [rule]}] (enforce? rule)))
60-
(remove (fn [{:keys [rule]}] (validated-rule? db chat-id target-path (:id rule))))
55+
(remove (fn [{:keys [rule]}] (validated-rule? db chat-id (:id rule))))
6156
vec
6257
not-empty))))
6358

test/eca/features/prompt_test.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
(is (string/includes? static "<project-rules description=\"Rules loaded from the current workspace. Prefer these when they conflict with broader global rules.\">"))
3838
(is (string/includes? static "<rule name=\"rule1\">First rule</rule>"))
3939
(is (string/includes? static "<rule name=\"rule2\">Second rule</rule>"))
40-
(is (string/includes? static "<path-scoped-rules description=\"Rules that apply to matching file paths. Use fetch_rule before actions required by enforce (read, modify, or both). Each rule only needs to be fetched once per target path.\">"))
40+
(is (string/includes? static "<path-scoped-rules description=\"Rules that apply to matching file paths. Use fetch_rule before actions required by enforce (read, modify, or both). Each rule only needs to be fetched once per chat.\">"))
4141
(is (string/includes? static "<global-path-scoped-rules description=\"Path-scoped rules loaded outside the current workspace.\">"))
4242
(is (string/includes? static "<workspace-path-scoped-rules root=\"/workspace/a\">"))
4343
(is (string/includes? static "<rule id=\"/workspace/a/.eca/rules/format.md\" name=\"format.md\" scope=\"project\" workspace-root=\"/workspace/a\" paths=\"**/*.clj\" enforce=\"modify\"/>"))
@@ -93,7 +93,7 @@
9393
:workspace-root "/workspace/a"
9494
:paths ["**/*.clj" "**/*.cljs"]}]
9595
{:keys [static]} (build-instructions [] [] path-scoped-rules [] (delay "TREE") "code" {} nil [{:full-name "eca__fetch_rule"}] (h/db))]
96-
(is (string/includes? static "<path-scoped-rules description=\"Rules that apply to matching file paths. Use fetch_rule before actions required by enforce (read, modify, or both). Each rule only needs to be fetched once per target path.\">"))
96+
(is (string/includes? static "<path-scoped-rules description=\"Rules that apply to matching file paths. Use fetch_rule before actions required by enforce (read, modify, or both). Each rule only needs to be fetched once per chat.\">"))
9797
(is (string/includes? static "<workspace-path-scoped-rules root=\"/workspace/a\">"))
9898
(is (string/includes? static "<rule id=\"/workspace/a/.eca/rules/format.md\" name=\"format.md\" scope=\"project\" workspace-root=\"/workspace/a\" paths=\"**/*.clj,**/*.cljs\" enforce=\"modify\"/>"))
9999
(is (not (string/includes? static "<global-rules")))

0 commit comments

Comments
 (0)