aru-code 0.8.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {aru_code-0.8.0/aru_code.egg-info → aru_code-0.9.0}/PKG-INFO +26 -1
  2. {aru_code-0.8.0 → aru_code-0.9.0}/README.md +25 -0
  3. aru_code-0.9.0/aru/__init__.py +1 -0
  4. {aru_code-0.8.0 → aru_code-0.9.0}/aru/cli.py +2 -0
  5. {aru_code-0.8.0 → aru_code-0.9.0}/aru/config.py +97 -1
  6. {aru_code-0.8.0 → aru_code-0.9.0/aru_code.egg-info}/PKG-INFO +26 -1
  7. {aru_code-0.8.0 → aru_code-0.9.0}/pyproject.toml +1 -1
  8. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_config.py +153 -1
  9. aru_code-0.8.0/aru/__init__.py +0 -1
  10. {aru_code-0.8.0 → aru_code-0.9.0}/LICENSE +0 -0
  11. {aru_code-0.8.0 → aru_code-0.9.0}/aru/agent_factory.py +0 -0
  12. {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/__init__.py +0 -0
  13. {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/base.py +0 -0
  14. {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/executor.py +0 -0
  15. {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/planner.py +0 -0
  16. {aru_code-0.8.0 → aru_code-0.9.0}/aru/commands.py +0 -0
  17. {aru_code-0.8.0 → aru_code-0.9.0}/aru/completers.py +0 -0
  18. {aru_code-0.8.0 → aru_code-0.9.0}/aru/context.py +0 -0
  19. {aru_code-0.8.0 → aru_code-0.9.0}/aru/display.py +0 -0
  20. {aru_code-0.8.0 → aru_code-0.9.0}/aru/permissions.py +0 -0
  21. {aru_code-0.8.0 → aru_code-0.9.0}/aru/providers.py +0 -0
  22. {aru_code-0.8.0 → aru_code-0.9.0}/aru/runner.py +0 -0
  23. {aru_code-0.8.0 → aru_code-0.9.0}/aru/runtime.py +0 -0
  24. {aru_code-0.8.0 → aru_code-0.9.0}/aru/session.py +0 -0
  25. {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/__init__.py +0 -0
  26. {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/ast_tools.py +0 -0
  27. {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/codebase.py +0 -0
  28. {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/gitignore.py +0 -0
  29. {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/mcp_client.py +0 -0
  30. {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/ranker.py +0 -0
  31. {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/tasklist.py +0 -0
  32. {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/SOURCES.txt +0 -0
  33. {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/dependency_links.txt +0 -0
  34. {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/entry_points.txt +0 -0
  35. {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/requires.txt +0 -0
  36. {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/top_level.txt +0 -0
  37. {aru_code-0.8.0 → aru_code-0.9.0}/setup.cfg +0 -0
  38. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_agents_base.py +0 -0
  39. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_ast_tools.py +0 -0
  40. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli.py +0 -0
  41. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_advanced.py +0 -0
  42. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_base.py +0 -0
  43. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_completers.py +0 -0
  44. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_new.py +0 -0
  45. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_run_cli.py +0 -0
  46. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_session.py +0 -0
  47. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_shell.py +0 -0
  48. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_codebase.py +0 -0
  49. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_context.py +0 -0
  50. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_executor.py +0 -0
  51. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_gitignore.py +0 -0
  52. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_main.py +0 -0
  53. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_mcp_client.py +0 -0
  54. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_permissions.py +0 -0
  55. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_planner.py +0 -0
  56. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_providers.py +0 -0
  57. {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_ranker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -302,6 +302,31 @@ Without any `aru.json` config, aru applies safe defaults:
302
302
 
303
303
  Place an `AGENTS.md` file in your project root with custom instructions that will be appended to all agent system prompts.
304
304
 
305
+ ### Instructions (Rules)
306
+
307
+ You can load additional instructions from local files, glob patterns, or remote URLs via the `instructions` field in `aru.json`:
308
+
309
+ ```json
310
+ {
311
+ "instructions": [
312
+ "CONTRIBUTING.md",
313
+ "docs/coding-standards.md",
314
+ "packages/*/AGENTS.md",
315
+ "https://raw.githubusercontent.com/my-org/shared-rules/main/style.md"
316
+ ]
317
+ }
318
+ ```
319
+
320
+ Each entry is resolved as follows:
321
+
322
+ | Format | Example | Behavior |
323
+ |--------|---------|----------|
324
+ | **Local file** | `"CONTRIBUTING.md"` | Reads the file relative to the project root |
325
+ | **Glob pattern** | `"docs/**/*.md"` | Expands the pattern, respects `.gitignore` |
326
+ | **Remote URL** | `"https://example.com/rules.md"` | Fetches via HTTP (5s timeout, cached per session) |
327
+
328
+ All resolved content is combined and appended to the agent's system prompt alongside `AGENTS.md`. Individual files are capped at 10KB, and the total combined size is capped at 50KB to prevent context bloat. Missing files and failed URL fetches are skipped with a warning.
329
+
305
330
  ### `.agents/` Directory
306
331
 
307
332
  ```
@@ -255,6 +255,31 @@ Without any `aru.json` config, aru applies safe defaults:
255
255
 
256
256
  Place an `AGENTS.md` file in your project root with custom instructions that will be appended to all agent system prompts.
257
257
 
258
+ ### Instructions (Rules)
259
+
260
+ You can load additional instructions from local files, glob patterns, or remote URLs via the `instructions` field in `aru.json`:
261
+
262
+ ```json
263
+ {
264
+ "instructions": [
265
+ "CONTRIBUTING.md",
266
+ "docs/coding-standards.md",
267
+ "packages/*/AGENTS.md",
268
+ "https://raw.githubusercontent.com/my-org/shared-rules/main/style.md"
269
+ ]
270
+ }
271
+ ```
272
+
273
+ Each entry is resolved as follows:
274
+
275
+ | Format | Example | Behavior |
276
+ |--------|---------|----------|
277
+ | **Local file** | `"CONTRIBUTING.md"` | Reads the file relative to the project root |
278
+ | **Glob pattern** | `"docs/**/*.md"` | Expands the pattern, respects `.gitignore` |
279
+ | **Remote URL** | `"https://example.com/rules.md"` | Fetches via HTTP (5s timeout, cached per session) |
280
+
281
+ All resolved content is combined and appended to the agent's system prompt alongside `AGENTS.md`. Individual files are capped at 10KB, and the total combined size is capped at 50KB to prevent context bloat. Missing files and failed URL fetches are skipped with a warning.
282
+
258
283
  ### `.agents/` Directory
259
284
 
260
285
  ```
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -137,6 +137,8 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
137
137
  console.print(f"[dim]Loaded {len(config.commands)} custom command(s): {', '.join(f'/{k}' for k in config.commands)}[/dim]")
138
138
  if config.skills:
139
139
  console.print(f"[dim]Loaded {len(config.skills)} skill(s): {', '.join(config.skills.keys())}[/dim]")
140
+ if config.rules_instructions:
141
+ console.print("[dim]Loaded custom instructions from aru.json[/dim]")
140
142
  if config.custom_agents:
141
143
  primary = [k for k, v in config.custom_agents.items() if v.mode == "primary"]
142
144
  subagents = [k for k, v in config.custom_agents.items() if v.mode == "subagent"]
@@ -10,12 +10,15 @@ Supports:
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
+ import logging
13
14
  import os
14
15
  import re
15
16
  from dataclasses import dataclass, field
16
17
  from pathlib import Path
17
18
  from typing import Any
18
19
 
20
+ logger = logging.getLogger("aru.config")
21
+
19
22
 
20
23
  @dataclass
21
24
  class CustomCommand:
@@ -54,6 +57,92 @@ class CustomAgent:
54
57
 
55
58
 
56
59
  MAX_README_CHARS = 2000 # Reduced from 8000 to save ~1.7K tokens per request
60
+ MAX_RULE_FILE_SIZE = 10_000 # 10KB per rule file
61
+ URL_FETCH_TIMEOUT = 5 # seconds
62
+ MAX_TOTAL_RULES_SIZE = 50_000 # 50KB combined cap
63
+
64
+ # Module-level URL cache (session-scoped, persists for process lifetime)
65
+ _url_cache: dict[str, str | None] = {}
66
+
67
+
68
+ def _resolve_instructions(entries: list[str], root: Path) -> str:
69
+ """Resolve instruction entries (local files, glob patterns, URLs) into combined text.
70
+
71
+ Each entry is classified as:
72
+ - URL: starts with http:// or https://
73
+ - Glob: contains *, ?, or [
74
+ - File path: everything else (resolved relative to root)
75
+ """
76
+ from aru.tools.gitignore import is_ignored
77
+
78
+ parts: list[str] = []
79
+ total_size = 0
80
+
81
+ def _add_content(source: str, content: str) -> None:
82
+ nonlocal total_size
83
+ if not content.strip():
84
+ return
85
+ truncated = content[:MAX_RULE_FILE_SIZE]
86
+ if total_size + len(truncated) > MAX_TOTAL_RULES_SIZE:
87
+ remaining = MAX_TOTAL_RULES_SIZE - total_size
88
+ if remaining <= 0:
89
+ logger.warning("Total rules size cap reached, skipping: %s", source)
90
+ return
91
+ truncated = truncated[:remaining]
92
+ logger.warning("Total rules size cap reached, truncating: %s", source)
93
+ parts.append(f"## Rules: {source}\n\n{truncated}")
94
+ total_size += len(truncated)
95
+
96
+ def _read_file(filepath: Path, source_label: str) -> None:
97
+ try:
98
+ content = filepath.read_text(encoding="utf-8")
99
+ _add_content(source_label, content)
100
+ except (OSError, UnicodeDecodeError) as exc:
101
+ logger.warning("Failed to read instruction file %s: %s", filepath, exc)
102
+
103
+ for entry in entries:
104
+ if entry.startswith("http://") or entry.startswith("https://"):
105
+ # Remote URL
106
+ if entry in _url_cache:
107
+ cached = _url_cache[entry]
108
+ if cached is not None:
109
+ _add_content(entry, cached)
110
+ continue
111
+ try:
112
+ import httpx
113
+ with httpx.Client(timeout=URL_FETCH_TIMEOUT, follow_redirects=True) as client:
114
+ resp = client.get(entry)
115
+ resp.raise_for_status()
116
+ text = resp.text
117
+ _url_cache[entry] = text
118
+ _add_content(entry, text)
119
+ except Exception as exc:
120
+ _url_cache[entry] = None
121
+ logger.warning("Failed to fetch instruction URL %s: %s", entry, exc)
122
+
123
+ elif any(c in entry for c in ("*", "?", "[")):
124
+ # Glob pattern
125
+ matched = sorted(root.glob(entry))
126
+ for filepath in matched:
127
+ if not filepath.is_file():
128
+ continue
129
+ try:
130
+ rel = filepath.relative_to(root)
131
+ except ValueError:
132
+ continue
133
+ if is_ignored(str(rel), str(root)):
134
+ continue
135
+ _read_file(filepath, str(rel))
136
+
137
+ else:
138
+ # Local file path
139
+ filepath = root / entry
140
+ if filepath.is_file():
141
+ _read_file(filepath, entry)
142
+ else:
143
+ logger.warning("Instruction file not found: %s", filepath)
144
+
145
+ return "\n\n".join(parts)
57
146
 
58
147
 
59
148
  @dataclass
@@ -61,6 +150,7 @@ class AgentConfig:
61
150
  """Loaded configuration from AGENTS.md, README.md, and .agents/ directory."""
62
151
  readme_md: str = ""
63
152
  agents_md: str = ""
153
+ rules_instructions: str = ""
64
154
  commands: dict[str, CustomCommand] = field(default_factory=dict)
65
155
  skills: dict[str, Skill] = field(default_factory=dict)
66
156
  permissions: dict[str, Any] = field(default_factory=dict)
@@ -71,7 +161,7 @@ class AgentConfig:
71
161
 
72
162
  @property
73
163
  def has_instructions(self) -> bool:
74
- return bool(self.agents_md) or bool(self.skills)
164
+ return bool(self.agents_md) or bool(self.skills) or bool(self.rules_instructions)
75
165
 
76
166
  def get_extra_instructions(self, active_skills: list[str] | None = None, lightweight: bool = False) -> str:
77
167
  """Build extra instructions from README.md, AGENTS.md, and active skills.
@@ -85,6 +175,8 @@ class AgentConfig:
85
175
  parts.append(f"## Project Overview (README.md)\n\n{self.readme_md}")
86
176
  if self.agents_md:
87
177
  parts.append(f"## Project Instructions (AGENTS.md)\n\n{self.agents_md}")
178
+ if self.rules_instructions:
179
+ parts.append(self.rules_instructions)
88
180
  if active_skills:
89
181
  for name in active_skills:
90
182
  if name in self.skills:
@@ -409,6 +501,10 @@ def load_config(cwd: str | None = None) -> AgentConfig:
409
501
  config.model_aliases = data["model_aliases"]
410
502
  if "plan_reviewer" in data:
411
503
  config.plan_reviewer = bool(data["plan_reviewer"])
504
+ # Resolve instructions (local files, globs, URLs)
505
+ if "instructions" in data and isinstance(data["instructions"], list):
506
+ entries = [str(e) for e in data["instructions"] if isinstance(e, str)]
507
+ config.rules_instructions = _resolve_instructions(entries, root)
412
508
  # Agent-level permission overrides from aru.json
413
509
  if "agent" in data and isinstance(data["agent"], dict):
414
510
  for agent_name, agent_data in data["agent"].items():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -302,6 +302,31 @@ Without any `aru.json` config, aru applies safe defaults:
302
302
 
303
303
  Place an `AGENTS.md` file in your project root with custom instructions that will be appended to all agent system prompts.
304
304
 
305
+ ### Instructions (Rules)
306
+
307
+ You can load additional instructions from local files, glob patterns, or remote URLs via the `instructions` field in `aru.json`:
308
+
309
+ ```json
310
+ {
311
+ "instructions": [
312
+ "CONTRIBUTING.md",
313
+ "docs/coding-standards.md",
314
+ "packages/*/AGENTS.md",
315
+ "https://raw.githubusercontent.com/my-org/shared-rules/main/style.md"
316
+ ]
317
+ }
318
+ ```
319
+
320
+ Each entry is resolved as follows:
321
+
322
+ | Format | Example | Behavior |
323
+ |--------|---------|----------|
324
+ | **Local file** | `"CONTRIBUTING.md"` | Reads the file relative to the project root |
325
+ | **Glob pattern** | `"docs/**/*.md"` | Expands the pattern, respects `.gitignore` |
326
+ | **Remote URL** | `"https://example.com/rules.md"` | Fetches via HTTP (5s timeout, cached per session) |
327
+
328
+ All resolved content is combined and appended to the agent's system prompt alongside `AGENTS.md`. Individual files are capped at 10KB, and the total combined size is capped at 50KB to prevent context bloat. Missing files and failed URL fetches are skipped with a warning.
329
+
305
330
  ### `.agents/` Directory
306
331
 
307
332
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.8.0"
7
+ version = "0.9.0"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,16 +1,22 @@
1
1
  """Unit tests for aru.config module."""
2
2
 
3
+ import json
3
4
  import pytest
4
5
  from pathlib import Path
6
+ from unittest.mock import MagicMock, patch
5
7
  from aru.config import (
6
8
  AgentConfig,
7
9
  CustomAgent,
8
10
  CustomCommand,
11
+ MAX_RULE_FILE_SIZE,
12
+ MAX_TOTAL_RULES_SIZE,
9
13
  Skill,
10
14
  _discover_agents,
11
15
  _parse_agent_metadata,
12
16
  _parse_frontmatter,
13
17
  _parse_skill_metadata,
18
+ _resolve_instructions,
19
+ _url_cache,
14
20
  render_command_template,
15
21
  render_skill_template,
16
22
  load_config,
@@ -801,4 +807,150 @@ class TestAgentPermissions:
801
807
  }))
802
808
  config = load_config(str(tmp_path))
803
809
  # aru.json override should win
804
- assert config.custom_agents["worker"].permission == {"edit": "deny"}
810
+ assert config.custom_agents["worker"].permission == {"edit": "deny"}
811
+
812
+
813
+ class TestInstructionsLoading:
814
+ """Test the instructions field in aru.json (rules system)."""
815
+
816
+ def setup_method(self):
817
+ """Clear URL cache before each test."""
818
+ _url_cache.clear()
819
+
820
+ def test_instructions_local_file(self, tmp_path):
821
+ (tmp_path / "CONTRIBUTING.md").write_text("Follow these contributing guidelines.")
822
+ (tmp_path / "aru.json").write_text(json.dumps({
823
+ "instructions": ["CONTRIBUTING.md"]
824
+ }))
825
+ config = load_config(str(tmp_path))
826
+ assert "Follow these contributing guidelines." in config.rules_instructions
827
+ assert "## Rules: CONTRIBUTING.md" in config.rules_instructions
828
+
829
+ def test_instructions_glob_pattern(self, tmp_path):
830
+ docs = tmp_path / "docs"
831
+ docs.mkdir()
832
+ (docs / "style.md").write_text("Style guide content")
833
+ (docs / "api.md").write_text("API guidelines")
834
+ (tmp_path / "aru.json").write_text(json.dumps({
835
+ "instructions": ["docs/*.md"]
836
+ }))
837
+ config = load_config(str(tmp_path))
838
+ assert "Style guide content" in config.rules_instructions
839
+ assert "API guidelines" in config.rules_instructions
840
+
841
+ def test_instructions_missing_file(self, tmp_path):
842
+ (tmp_path / "aru.json").write_text(json.dumps({
843
+ "instructions": ["nonexistent.md"]
844
+ }))
845
+ config = load_config(str(tmp_path))
846
+ assert config.rules_instructions == ""
847
+
848
+ def test_instructions_file_size_cap(self, tmp_path):
849
+ large_content = "x" * (MAX_RULE_FILE_SIZE + 5000)
850
+ (tmp_path / "large.md").write_text(large_content)
851
+ (tmp_path / "aru.json").write_text(json.dumps({
852
+ "instructions": ["large.md"]
853
+ }))
854
+ config = load_config(str(tmp_path))
855
+ # Header + content, content should be truncated
856
+ assert len(config.rules_instructions) < MAX_RULE_FILE_SIZE + 200 # header overhead
857
+
858
+ def test_instructions_url(self, tmp_path):
859
+ mock_response = MagicMock()
860
+ mock_response.text = "Remote rule content"
861
+ mock_response.raise_for_status = MagicMock()
862
+
863
+ mock_client = MagicMock()
864
+ mock_client.__enter__ = MagicMock(return_value=mock_client)
865
+ mock_client.__exit__ = MagicMock(return_value=False)
866
+ mock_client.get.return_value = mock_response
867
+
868
+ with patch("httpx.Client", return_value=mock_client):
869
+ (tmp_path / "aru.json").write_text(json.dumps({
870
+ "instructions": ["https://example.com/rules.md"]
871
+ }))
872
+ config = load_config(str(tmp_path))
873
+ assert "Remote rule content" in config.rules_instructions
874
+
875
+ def test_instructions_url_timeout(self, tmp_path):
876
+ import httpx
877
+
878
+ mock_client = MagicMock()
879
+ mock_client.__enter__ = MagicMock(return_value=mock_client)
880
+ mock_client.__exit__ = MagicMock(return_value=False)
881
+ mock_client.get.side_effect = httpx.ConnectTimeout("timeout")
882
+
883
+ with patch("httpx.Client", return_value=mock_client):
884
+ (tmp_path / "aru.json").write_text(json.dumps({
885
+ "instructions": ["https://example.com/timeout.md"]
886
+ }))
887
+ config = load_config(str(tmp_path))
888
+ assert config.rules_instructions == ""
889
+
890
+ def test_instructions_combined_in_extra(self, tmp_path):
891
+ (tmp_path / "rules.md").write_text("Custom rule")
892
+ (tmp_path / "AGENTS.md").write_text("Agent instructions")
893
+ (tmp_path / "aru.json").write_text(json.dumps({
894
+ "instructions": ["rules.md"]
895
+ }))
896
+ config = load_config(str(tmp_path))
897
+ extra = config.get_extra_instructions()
898
+ # Both AGENTS.md and rules should be present
899
+ assert "Agent instructions" in extra
900
+ assert "Custom rule" in extra
901
+ # Rules should come after AGENTS.md
902
+ agents_pos = extra.find("Agent instructions")
903
+ rules_pos = extra.find("Custom rule")
904
+ assert agents_pos < rules_pos
905
+
906
+ def test_instructions_total_size_cap(self, tmp_path):
907
+ # Create files that together exceed MAX_TOTAL_RULES_SIZE
908
+ docs = tmp_path / "docs"
909
+ docs.mkdir()
910
+ chunk = "y" * (MAX_RULE_FILE_SIZE - 100)
911
+ num_files = (MAX_TOTAL_RULES_SIZE // MAX_RULE_FILE_SIZE) + 3
912
+ for i in range(num_files):
913
+ (docs / f"rule{i:02d}.md").write_text(chunk)
914
+ (tmp_path / "aru.json").write_text(json.dumps({
915
+ "instructions": ["docs/*.md"]
916
+ }))
917
+ config = load_config(str(tmp_path))
918
+ assert len(config.rules_instructions) <= MAX_TOTAL_RULES_SIZE + 5000 # headers overhead
919
+
920
+ def test_instructions_empty_list(self, tmp_path):
921
+ (tmp_path / "aru.json").write_text(json.dumps({
922
+ "instructions": []
923
+ }))
924
+ config = load_config(str(tmp_path))
925
+ assert config.rules_instructions == ""
926
+
927
+ def test_instructions_has_instructions_property(self):
928
+ config = AgentConfig(rules_instructions="some rules")
929
+ assert config.has_instructions is True
930
+
931
+ def test_resolve_instructions_directly(self, tmp_path):
932
+ (tmp_path / "a.md").write_text("Content A")
933
+ (tmp_path / "b.md").write_text("Content B")
934
+ result = _resolve_instructions(["a.md", "b.md"], tmp_path)
935
+ assert "Content A" in result
936
+ assert "Content B" in result
937
+
938
+ def test_instructions_url_caching(self, tmp_path):
939
+ """Second call should use cache, not fetch again."""
940
+ mock_response = MagicMock()
941
+ mock_response.text = "Cached content"
942
+ mock_response.raise_for_status = MagicMock()
943
+
944
+ mock_client = MagicMock()
945
+ mock_client.__enter__ = MagicMock(return_value=mock_client)
946
+ mock_client.__exit__ = MagicMock(return_value=False)
947
+ mock_client.get.return_value = mock_response
948
+
949
+ url = "https://example.com/cached.md"
950
+ with patch("httpx.Client", return_value=mock_client):
951
+ result1 = _resolve_instructions([url], tmp_path)
952
+ result2 = _resolve_instructions([url], tmp_path)
953
+ assert "Cached content" in result1
954
+ assert "Cached content" in result2
955
+ # httpx.Client should only be called once (second uses cache)
956
+ assert mock_client.get.call_count == 1
@@ -1 +0,0 @@
1
- __version__ = "0.8.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes