Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Fix inline completion crash when renewing auth tokens before completion requests. #437
- 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.
- 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.
- 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.

- 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`.

Expand Down
2 changes: 1 addition & 1 deletion docs/config/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Use this rule of thumb:
- Add `enforce: read` or `enforce: modify` when the model must fetch the matching rule before using the corresponding builtin file tool.
- Use `agent` and `model` filters when the rule is only relevant for specific chat modes or model families.

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:
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:
```markdown title=".eca/rules/copilot-ask-user.md"
---
model: "github-copilot/.*"
Expand Down
2 changes: 1 addition & 1 deletion resources/prompts/tools/fetch_rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Fetch the full content of a path-scoped rule by its exact id and the exact absolute target path you plan to work with.
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.
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`.
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.
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.
2 changes: 1 addition & 1 deletion src/eca/features/prompt.clj
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@
(remove #(= :global (:scope %)))
(map :content))
path-scoped-section (when (and fetch-rule-available? (seq path-scoped-rules))
["<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.\">"
["<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.\">"
(path-scoped-rule-catalog path-scoped-rules)
"</path-scoped-rules>"])
has-static-rules? (seq rendered-static-rules)]
Expand Down
6 changes: 3 additions & 3 deletions src/eca/features/tools/fetch_rule.clj
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@
:contents [{:type :text
:text (path-mismatch-message rule match-info)}]}

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

:else
(do
Expand All @@ -87,7 +87,7 @@
:text (str header "\n" content)}]}
{:error false
:contents [{:type :text
: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.")}]}))))))
: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.")}]}))))))

(defn ^:private describe-rule
[{rule-name :name :keys [id paths enforce scope workspace-root]}]
Expand Down
15 changes: 5 additions & 10 deletions src/eca/features/tools/path_rules.clj
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,12 @@
(tools.util/tool-available? all-tools "eca__fetch_rule"))

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

(defn validated-rule?
[db chat-id target-path rule-id]
(boolean
(get-in db [:chats chat-id validated-path-rules-key (shared/normalize-path target-path) rule-id])))
[db chat-id rule-id]
(contains? (get-in db [:chats chat-id validated-path-rules-key] #{}) rule-id))

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

Expand Down
4 changes: 2 additions & 2 deletions test/eca/features/prompt_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
(is (string/includes? static "<project-rules description=\"Rules loaded from the current workspace. Prefer these when they conflict with broader global rules.\">"))
(is (string/includes? static "<rule name=\"rule1\">First rule</rule>"))
(is (string/includes? static "<rule name=\"rule2\">Second rule</rule>"))
(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.\">"))
(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.\">"))
(is (string/includes? static "<global-path-scoped-rules description=\"Path-scoped rules loaded outside the current workspace.\">"))
(is (string/includes? static "<workspace-path-scoped-rules root=\"/workspace/a\">"))
(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\"/>"))
Expand Down Expand Up @@ -93,7 +93,7 @@
:workspace-root "/workspace/a"
:paths ["**/*.clj" "**/*.cljs"]}]
{:keys [static]} (build-instructions [] [] path-scoped-rules [] (delay "TREE") "code" {} nil [{:full-name "eca__fetch_rule"}] (h/db))]
(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.\">"))
(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.\">"))
(is (string/includes? static "<workspace-path-scoped-rules root=\"/workspace/a\">"))
(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\"/>"))
(is (not (string/includes? static "<global-rules")))
Expand Down
Loading
Loading