Skip to content

Commit 881d2b4

Browse files
authored
Merge pull request #429 from editor-code-assistant/rules_improvement_and_tool
Improve rules with filters, conditions, and scoped loading
2 parents 6fe08a4 + 86cef74 commit 881d2b4

28 files changed

Lines changed: 2205 additions & 243 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Improve rules with frontmatter filters, condition variables, path-scoped loading, enforcement support, and clearer documentation. #222
56
- `preToolCall` hooks now receive `approval: "ask"` for the native `ask_user` tool so notification hooks (e.g. matching `.approval == "ask"`) also fire when the chat is blocked waiting for a user answer, regardless of trust mode.
67

78
## 0.129.2

docs/config/agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ This is useful when you want a variant of a built-in or custom agent with small
5555

5656
## Custom agents and prompts
5757

58-
You can create an agent and define its prompt, tool call approval and default model.
58+
You can create an agent and define its prompt, tool call approval and default model. Custom prompts can use [template variables](template.md#condition-variables).
5959

6060
=== "Example: my-agent"
6161

docs/config/rules.md

Lines changed: 170 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,193 @@ description: "Configure ECA rules: define coding standards, conventions, and con
44

55
# Rules
66

7-
Rules are contexts that are passed to the LLM during a prompt and are useful to tune prompts or LLM responses.
8-
Rules are text files (typically `.md`, but any format works):
7+
Rules let you influence how the model behaves without changing an agent prompt. Use them for coding standards, review checklists, project conventions, safety constraints, or file-specific guidance.
98

10-
There are 3 possible ways to configure rules following this order of priority:
9+
A rule is a Markdown file. By default, rule content is rendered into the system prompt for every chat where the rule matches. Add YAML frontmatter when a rule should apply only to specific agents, models, or paths.
10+
11+
## Rules vs Skills
12+
13+
- Use rules to make ECA automatically follow project conventions, safety constraints, or guidance for matching paths.
14+
- Use [skills](./skills.md) to teach ECA reusable workflows, tools, or domain knowledge it can load on demand for specific tasks.
15+
16+
## Rule locations
17+
18+
ECA loads rules from 3 sources:
1119

1220
=== "Project file"
1321

14-
A `.eca/rules` folder from the workspace root containing `.md` files with the rules.
22+
Files inside `.eca/rules` under a workspace root.
1523

16-
```markdown title=".eca/rules/talk_funny.md"
17-
- Talk funny like Mickey!
24+
```markdown title=".eca/rules/code-style.md"
25+
Prefer small, focused functions and idiomatic Clojure.
1826
```
1927

2028
=== "Global file"
2129

22-
A `$XDG_CONFIG_HOME/eca/rules` or `~/.config/eca/rules` folder containing `.md` files with the rules.
30+
Files inside `$XDG_CONFIG_HOME/eca/rules` or `~/.config/eca/rules`.
2331

24-
```markdown title="~/.config/eca/rules/talk_funny.md"
25-
- Talk funny like Mickey!
32+
```markdown title="~/.config/eca/rules/answers.md"
33+
Be concise and explain trade-offs when suggesting code changes.
2634
```
2735

2836
=== "Config"
2937

30-
Just add to your config the `:rules` pointing to `.md` files that will be searched from the workspace root if not an absolute path:
38+
Paths listed in the `rules` config key. Relative paths are searched from each workspace root. Absolute paths inside a workspace behave as project rules; absolute paths outside workspaces behave as global rules.
3139

3240
```javascript title="~/.config/eca/config.json"
3341
{
3442
"rules": [{"path": "my-rule.md"}]
3543
}
3644
```
45+
46+
## Static and path-scoped rules
47+
48+
Most rules should be **static rules**: rules without `paths`. Their full content is automatically included in the system prompt. Use them for guidance that should always be available, such as coding style, response tone, or repository-wide conventions.
49+
50+
```markdown title=".eca/rules/clojure-style.md"
51+
Prefer immutable data, kebab-case names, and small functions.
52+
```
53+
54+
Use **path-scoped rules** when the guidance only matters for matching files. Add `paths` in frontmatter. Instead of injecting the full rule into every prompt, ECA lists a small catalog of matching path-scoped rules. The model can then call `fetch_rule` with the exact rule id and target path to load the full content when needed.
55+
56+
```markdown title=".eca/rules/html-style.md"
57+
---
58+
paths: "**.html"
59+
---
60+
61+
Use semantic HTML and keep accessibility in mind.
62+
```
63+
64+
If `fetch_rule` is unavailable in the current chat, path-scoped rules are treated as inactive and are omitted from the prompt. Use the `/rules` command to inspect which rules are available for the current agent and model.
65+
66+
## Frontmatter
67+
68+
Rules support YAML frontmatter. Recognized fields are `agent`, `model`, `paths`, and `enforce`. Rules without frontmatter are static rules that apply to all agents and models.
69+
70+
### `agent`
71+
72+
Restricts a rule to one agent or a list of agents.
73+
74+
```markdown title=".eca/rules/code-only.md"
75+
---
76+
agent: code
77+
---
78+
79+
Prefer making the smallest safe code change.
80+
```
81+
82+
```markdown title=".eca/rules/shared.md"
83+
---
84+
agent:
85+
- code
86+
- plan
87+
---
88+
89+
Call out risky assumptions before proceeding.
90+
```
91+
92+
### `model`
93+
94+
Restricts a rule to models whose full model identifier matches a regex pattern. The full identifier includes the provider, such as `anthropic/claude-sonnet-4-20250514` or `openai/gpt-5.2`.
95+
96+
```markdown title=".eca/rules/high-reasoning-models.md"
97+
---
98+
model:
99+
- ".*claude-sonnet-4.*"
100+
- ".*gpt-5.*"
101+
---
102+
103+
Spend more time validating edge cases before editing.
104+
```
105+
106+
Patterns are partial-match regexes using `re-find`, so `gpt-4` also matches `openai/gpt-4o-mini`. Use anchors (`^...$`) when you need an exact match.
107+
108+
### `paths`
109+
110+
Marks a rule as path-scoped. It accepts one glob pattern or a list of patterns matched against workspace-relative paths.
111+
112+
```markdown title=".eca/rules/frontend.md"
113+
---
114+
paths:
115+
- "src/**.{ts,tsx}"
116+
- "lib/**.ts"
117+
---
118+
119+
Follow the project's frontend conventions.
120+
```
121+
122+
For project rules, patterns are matched relative to the workspace root that owns the rule. For global rules, patterns can match files inside any current workspace root.
123+
124+
Path matching uses Java NIO `PathMatcher` glob syntax. Common patterns:
125+
126+
| Pattern | Matches | Does not match |
127+
|---------|---------|----------------|
128+
| `src/*.clj` | `src/foo.clj` | `src/nested/foo.clj` |
129+
| `src/**/*.clj` | `src/nested/foo.clj` | `src/foo.clj` |
130+
| `src/**.clj` | `src/foo.clj`, `src/nested/foo.clj` | `test/foo.clj` |
131+
| `docs/**.md` | `docs/index.md`, `docs/config/rules.md` | `src/docs.md` |
132+
133+
Unlike many shell glob matchers, `**/` does not match the zero-directory case. For example, `src/**/*.clj` matches `src/nested/foo.clj`, but not `src/foo.clj`. Use `src/**.clj` or `{src/*.clj,src/**/*.clj}` when you need both.
134+
135+
### `enforce`
136+
137+
`enforce` only applies to path-scoped rules. It has no effect without `paths`, because static rules are already included in the system prompt.
138+
139+
For matching path-scoped rules, `enforce` controls when ECA requires the rule to be fetched before a builtin file tool proceeds.
140+
141+
| Value | Meaning |
142+
|-------|---------|
143+
| `modify` | Fetch before modifying a matching file. This is the default. |
144+
| `read` | Fetch before reading a matching file with `read_file`. |
145+
| `[read, modify]` | Fetch before both reading and modifying. |
146+
147+
```markdown title=".eca/rules/api-style.md"
148+
---
149+
paths: "src/api/**.ts"
150+
---
151+
152+
Follow the API naming and error-response conventions.
153+
```
154+
155+
```markdown title=".eca/rules/sensitive-data.md"
156+
---
157+
paths: "src/data/**.clj"
158+
enforce:
159+
- read
160+
- modify
161+
---
162+
163+
Never expose PII fields in API responses.
164+
```
165+
166+
When `enforce` is omitted, it defaults to `modify`.
167+
168+
## How to use rules
169+
170+
Use this rule of thumb:
171+
172+
- Use a static rule when the model should always know the instruction.
173+
- Use a path-scoped rule when the instruction only matters for certain files.
174+
- Add `enforce: read` or `enforce: modify` when the model must fetch the matching rule before using the corresponding builtin file tool.
175+
- Use `agent` and `model` filters when the rule is only relevant for specific chat modes or model families.
176+
177+
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:
178+
```markdown title=".eca/rules/copilot-ask-user.md"
179+
---
180+
model: "github-copilot/.*"
181+
---
182+
183+
Strongly prefer using the `eca__ask_user` tool over ending your turn or asking questions inline in your response.
184+
```
185+
186+
## Condition variables
187+
188+
Rules support [template variables](template.md#condition-variables) using [Selmer](https://github.com/yogthos/Selmer). If a rule renders to an empty string, ECA skips it.
189+
190+
```markdown title=".eca/rules/context-aware.md"
191+
{% if isSubagent %}
192+
Be concise and return only the final result.
193+
{% else %}
194+
Provide explanations and mention important trade-offs.
195+
{% endif %}
196+
```

docs/config/template.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
description: "Use templates and condition variables in ECA configuration files, prompts, and rules."
3+
---
4+
5+
# Templates
6+
7+
ECA supports templating in configurable prompts and rules, so instructions can adapt to the current chat context.
8+
9+
For loading files, environment variables, classpath resources, or netrc credentials in config strings, see [Dynamic string contents](introduction.md#dynamic-string-contents).
10+
11+
## Condition variables
12+
13+
Custom agent prompts and [rules](rules.md) are rendered with [Selmer](https://github.com/yogthos/Selmer). Use condition variables when one prompt or rule should behave differently depending on the current chat.
14+
15+
Available variables:
16+
17+
| Variable | Type | Description |
18+
|----------|------|-------------|
19+
| `isSubagent` | boolean | `true` when the chat is running as a subagent |
20+
| `workspaceRoots` | string | The current workspace root paths |
21+
| `toolEnabled_<tool-name>` | boolean | `true` when a tool is enabled, using its exact full name, e.g. `toolEnabled_eca__shell_command` |
22+
23+
```markdown title="Prompt or rule"
24+
{% if isSubagent %}
25+
Be concise and return only the final result.
26+
{% else %}
27+
Explain important trade-offs and assumptions.
28+
{% endif %}
29+
30+
{% if toolEnabled_eca__shell_command %}
31+
You can run shell commands to verify your work.
32+
{% endif %}
33+
34+
Current workspace roots: {{ workspaceRoots }}
35+
```
36+
37+
If a rule renders to an empty string, ECA skips it and does not add an empty rule block to the system prompt.

integration-test/integration/chat/commands_test.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
{:name "config" :arguments []}
3636
{:name "doctor" :arguments []}
3737
{:name "repo-map-show" :arguments []}
38+
{:name "rules" :arguments []}
3839
{:name "prompt-show" :arguments [{:name "optional-prompt"}]}
3940
{:name "subagents" :arguments []}
4041
{:name "plugins" :arguments []}

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ nav:
1515
- install.md
1616
- Configuration:
1717
- Introduction: config/introduction.md
18+
- Templates: config/template.md
1819
- Providers / Models: config/models.md
1920
- Tools / Approval: config/tools.md
2021
- Variants: config/variants.md

resources/META-INF/native-image/eca/eca/native-image.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Args=-J-Dborkdude.dynaload.aot=true \
3939
-H:IncludeResources=prompts/tools/skill.md \
4040
-H:IncludeResources=prompts/tools/spawn_agent.md \
4141
-H:IncludeResources=prompts/tools/write_file.md \
42+
-H:IncludeResources=prompts/tools/fetch_rule.md \
4243
-H:IncludeResources=webpages/oauth.html \
4344
-H:IncludeResources=logo.svg \
4445
-H:IncludeResources=tls/local-eca-dev-fullchain.pem \
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fetch the full content of a path-scoped rule by its exact id and the exact absolute target path you plan to work with.
2+
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.
3+
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.

src/eca/config.clj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
"eca__editor_diagnostics" {}
124124
"eca__skill" {}
125125
"eca__task" {}
126+
"eca__fetch_rule" {}
126127
"eca__spawn_agent" {}}
127128
:deny {"eca__shell_command"
128129
{:argsMatchers {"command" dangerous-commands-regexes}}}}}}
@@ -144,7 +145,8 @@
144145
"eca__grep" {}
145146
"eca__editor_diagnostics" {}
146147
"eca__skill" {}
147-
"eca__task" {}}
148+
"eca__task" {}
149+
"eca__fetch_rule" {}}
148150
:deny {"eca__shell_command"
149151
{:argsMatchers {"command" dangerous-commands-regexes}}}}}}
150152
"general" {:mode "subagent"
@@ -175,6 +177,7 @@
175177
"eca__skill" {}
176178
"eca__task" {}
177179
"eca__ask_user" {}
180+
"eca__fetch_rule" {}
178181
"eca__spawn_agent" {}}
179182
:ask {}
180183
:deny {}}

src/eca/features/chat.clj

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,14 @@
10761076

10771077
:else
10781078
(default-model db config))))
1079-
rules (f.rules/all config (:workspace-folders db))
1079+
_ (when (seq contexts)
1080+
(lifecycle/send-content! {:messenger messenger :chat-id chat-id} :system {:type :progress
1081+
:state :running
1082+
:text "Parsing given context"}))
1083+
refined-contexts (concat
1084+
(f.context/agents-file-contexts db)
1085+
(f.context/raw-contexts->refined contexts db))
1086+
{static-rules :static path-scoped-rules :path-scoped} (f.rules/all-rules config (:workspace-folders db) agent full-model)
10801087
all-tools (f.tools/all-tools chat-id agent @db* config)
10811088
skills (->> (f.skills/all config (:workspace-folders db))
10821089
(remove
@@ -1087,25 +1094,20 @@
10871094
db
10881095
config
10891096
agent)))))
1090-
_ (when (seq contexts)
1091-
(lifecycle/send-content! {:messenger messenger :chat-id chat-id} :system {:type :progress
1092-
:state :running
1093-
:text "Parsing given context"}))
1094-
refined-contexts (concat
1095-
(f.context/agents-file-contexts db)
1096-
(f.context/raw-contexts->refined contexts db))
10971097
repo-map* (delay (f.index/repo-map db config {:as-string? true}))
10981098
prompt-cache (get-in db [:chats chat-id :prompt-cache])
1099-
cached-static (when (= (:agent prompt-cache) agent)
1100-
(:static prompt-cache))
1101-
instructions (if cached-static
1102-
{:static cached-static
1099+
instructions (if (and prompt-cache
1100+
(= (:agent prompt-cache) agent)
1101+
(= (:model prompt-cache) full-model))
1102+
{:static (:static prompt-cache)
11031103
:dynamic (f.prompt/build-dynamic-instructions refined-contexts db)}
11041104
(let [result (f.prompt/build-chat-instructions
1105-
refined-contexts rules skills repo-map*
1105+
refined-contexts static-rules path-scoped-rules skills repo-map*
11061106
agent config chat-id all-tools db)]
1107-
(swap! db* update-in [:chats chat-id :prompt-cache]
1108-
assoc :static (:static result) :agent agent)
1107+
(swap! db* assoc-in [:chats chat-id :prompt-cache]
1108+
{:static (:static result)
1109+
:agent agent
1110+
:model full-model})
11091111
result))
11101112
image-contents (->> refined-contexts
11111113
(filter #(= :image (:type %))))
@@ -1355,7 +1357,6 @@
13551357
:messenger messenger}))
13561358
{}))
13571359

1358-
13591360
(defn ^:private find-last-message-idx
13601361
"Find the last message index matching content-id by checking both
13611362
:content-id (user messages) and [:content :id] (tool calls, etc)."
@@ -1446,7 +1447,7 @@
14461447
(remove :subagent)
14471448
(sort-by (fn [c] (or (get c primary) (get c secondary) 0)) >)
14481449
(mapv (fn [{:keys [id title status created-at updated-at model messages]}]
1449-
(shared/assoc-some
1450+
(assoc-some
14501451
{:id id
14511452
:title title
14521453
:status (or status :idle)
@@ -1480,4 +1481,4 @@
14801481
(lifecycle/send-content! chat-ctx :system (assoc-some {:type :metadata} :title title))
14811482
(config/notify-selected-model-changed! (:model chat) db* messenger config)
14821483
(config/notify-selected-trust-changed! (:trust chat) db* messenger)
1483-
{:found? true :chat-id chat-id :title title}))))
1484+
{:found? true :chat-id chat-id :title title}))))

0 commit comments

Comments
 (0)