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.
- {aru_code-0.8.0/aru_code.egg-info → aru_code-0.9.0}/PKG-INFO +26 -1
- {aru_code-0.8.0 → aru_code-0.9.0}/README.md +25 -0
- aru_code-0.9.0/aru/__init__.py +1 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/cli.py +2 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/config.py +97 -1
- {aru_code-0.8.0 → aru_code-0.9.0/aru_code.egg-info}/PKG-INFO +26 -1
- {aru_code-0.8.0 → aru_code-0.9.0}/pyproject.toml +1 -1
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_config.py +153 -1
- aru_code-0.8.0/aru/__init__.py +0 -1
- {aru_code-0.8.0 → aru_code-0.9.0}/LICENSE +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/agent_factory.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/base.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/executor.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/agents/planner.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/commands.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/completers.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/context.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/display.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/permissions.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/providers.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/runner.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/runtime.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/session.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/SOURCES.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/setup.cfg +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_ast_tools.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_codebase.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_context.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_executor.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_main.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_permissions.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_planner.py +0 -0
- {aru_code-0.8.0 → aru_code-0.9.0}/tests/test_providers.py +0 -0
- {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.
|
|
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.
|
|
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
|
```
|
|
@@ -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
|
aru_code-0.8.0/aru/__init__.py
DELETED
|
@@ -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
|
|
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
|