aru-code 0.7.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.7.0/aru_code.egg-info → aru_code-0.9.0}/PKG-INFO +26 -1
- {aru_code-0.7.0 → aru_code-0.9.0}/README.md +25 -0
- aru_code-0.9.0/aru/__init__.py +1 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/agent_factory.py +6 -4
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/agents/executor.py +3 -2
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/agents/planner.py +3 -3
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/cli.py +15 -19
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/config.py +97 -1
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/context.py +2 -2
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/permissions.py +39 -73
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/runner.py +12 -21
- aru_code-0.9.0/aru/runtime.py +158 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/tools/codebase.py +140 -195
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/tools/tasklist.py +20 -75
- {aru_code-0.7.0 → aru_code-0.9.0/aru_code.egg-info}/PKG-INFO +26 -1
- {aru_code-0.7.0 → aru_code-0.9.0}/aru_code.egg-info/SOURCES.txt +1 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/pyproject.toml +1 -1
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_codebase.py +16 -16
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_config.py +153 -1
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_permissions.py +26 -20
- aru_code-0.7.0/aru/__init__.py +0 -1
- {aru_code-0.7.0 → aru_code-0.9.0}/LICENSE +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/agents/base.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/commands.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/completers.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/display.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/providers.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/session.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/setup.cfg +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_ast_tools.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_context.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_executor.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_main.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_planner.py +0 -0
- {aru_code-0.7.0 → aru_code-0.9.0}/tests/test_providers.py +0 -0
- {aru_code-0.7.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"
|
|
@@ -13,7 +13,8 @@ def create_general_agent(session: Session, config: AgentConfig | None = None):
|
|
|
13
13
|
from agno.agent import Agent
|
|
14
14
|
from agno.compression.manager import CompressionManager
|
|
15
15
|
|
|
16
|
-
from aru.tools.codebase import GENERAL_TOOLS
|
|
16
|
+
from aru.tools.codebase import GENERAL_TOOLS
|
|
17
|
+
from aru.runtime import get_ctx
|
|
17
18
|
|
|
18
19
|
extra = config.get_extra_instructions() if config else ""
|
|
19
20
|
|
|
@@ -25,7 +26,7 @@ def create_general_agent(session: Session, config: AgentConfig | None = None):
|
|
|
25
26
|
markdown=True,
|
|
26
27
|
compress_tool_results=True,
|
|
27
28
|
compression_manager=CompressionManager(
|
|
28
|
-
model=create_model(
|
|
29
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=1024),
|
|
29
30
|
compress_tool_results=True,
|
|
30
31
|
compress_tool_results_limit=15,
|
|
31
32
|
),
|
|
@@ -39,7 +40,8 @@ def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
|
|
|
39
40
|
from agno.agent import Agent
|
|
40
41
|
from agno.compression.manager import CompressionManager
|
|
41
42
|
from aru.agents.base import BASE_INSTRUCTIONS
|
|
42
|
-
from aru.tools.codebase import resolve_tools
|
|
43
|
+
from aru.tools.codebase import resolve_tools
|
|
44
|
+
from aru.runtime import get_ctx
|
|
43
45
|
|
|
44
46
|
model_ref = agent_def.model or session.model_ref
|
|
45
47
|
tools = resolve_tools(agent_def.tools)
|
|
@@ -58,7 +60,7 @@ def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
|
|
|
58
60
|
markdown=True,
|
|
59
61
|
compress_tool_results=True,
|
|
60
62
|
compression_manager=CompressionManager(
|
|
61
|
-
model=create_model(
|
|
63
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=1024),
|
|
62
64
|
compress_tool_results=True,
|
|
63
65
|
compress_tool_results_limit=15,
|
|
64
66
|
),
|
|
@@ -6,7 +6,8 @@ from agno.utils.log import log_warning
|
|
|
6
6
|
|
|
7
7
|
from aru.agents.base import build_instructions
|
|
8
8
|
from aru.providers import create_model
|
|
9
|
-
from aru.tools.codebase import EXECUTOR_TOOLS
|
|
9
|
+
from aru.tools.codebase import EXECUTOR_TOOLS
|
|
10
|
+
from aru.runtime import get_ctx
|
|
10
11
|
|
|
11
12
|
# Max chars for truncation fallback when compression fails
|
|
12
13
|
_TRUNCATE_FALLBACK = 3000
|
|
@@ -50,7 +51,7 @@ def create_executor(model_ref: str = "anthropic/claude-sonnet-4-5", extra_instru
|
|
|
50
51
|
# Compress tool results after 5 uncompressed tool calls to save tokens
|
|
51
52
|
compress_tool_results=True,
|
|
52
53
|
compression_manager=_SafeCompressionManager(
|
|
53
|
-
model=create_model(
|
|
54
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=2048),
|
|
54
55
|
compress_tool_results=True,
|
|
55
56
|
compress_tool_results_limit=15,
|
|
56
57
|
),
|
|
@@ -6,9 +6,9 @@ from agno.compression.manager import CompressionManager
|
|
|
6
6
|
from aru.agents.base import build_instructions
|
|
7
7
|
from aru.providers import create_model
|
|
8
8
|
from aru.tools.codebase import (
|
|
9
|
-
_get_small_model_ref,
|
|
10
9
|
glob_search, grep_search, list_directory, read_file, read_file_smart,
|
|
11
10
|
)
|
|
11
|
+
from aru.runtime import get_ctx
|
|
12
12
|
|
|
13
13
|
REVIEWER_INSTRUCTIONS = """\
|
|
14
14
|
You are a plan scope reviewer. You receive a user request and a generated implementation plan.
|
|
@@ -47,7 +47,7 @@ async def review_plan(request: str, plan: str) -> str:
|
|
|
47
47
|
"""
|
|
48
48
|
reviewer = Agent(
|
|
49
49
|
name="Reviewer",
|
|
50
|
-
model=create_model(
|
|
50
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=2048),
|
|
51
51
|
instructions=REVIEWER_INSTRUCTIONS,
|
|
52
52
|
markdown=True,
|
|
53
53
|
)
|
|
@@ -77,7 +77,7 @@ def create_planner(model_ref: str = "anthropic/claude-sonnet-4-5", extra_instruc
|
|
|
77
77
|
# Compress tool results after 6 uncompressed tool calls to save tokens
|
|
78
78
|
compress_tool_results=True,
|
|
79
79
|
compression_manager=CompressionManager(
|
|
80
|
-
model=create_model(
|
|
80
|
+
model=create_model(get_ctx().small_model_ref, max_tokens=1024),
|
|
81
81
|
compress_tool_results=True,
|
|
82
82
|
compress_tool_results_limit=15,
|
|
83
83
|
),
|
|
@@ -104,24 +104,18 @@ from aru.providers import (
|
|
|
104
104
|
|
|
105
105
|
async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
106
106
|
"""Main REPL loop."""
|
|
107
|
-
|
|
108
|
-
from aru.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
parse_permission_config,
|
|
114
|
-
)
|
|
115
|
-
from aru.tools.codebase import set_console
|
|
116
|
-
set_console(console)
|
|
117
|
-
perm_set_console(console)
|
|
118
|
-
set_skip_permissions(skip_permissions)
|
|
107
|
+
import atexit
|
|
108
|
+
from aru.runtime import init_ctx, get_ctx
|
|
109
|
+
from aru.permissions import parse_permission_config, reset_session as perm_reset_session
|
|
110
|
+
from aru.tools.codebase import cleanup_processes
|
|
111
|
+
|
|
112
|
+
ctx = init_ctx(console=console, skip_permissions=skip_permissions)
|
|
119
113
|
|
|
120
114
|
store = SessionStore()
|
|
121
115
|
|
|
122
116
|
def _sync_model(sess: Session):
|
|
123
|
-
"""Sync the model IDs to the
|
|
124
|
-
|
|
117
|
+
"""Sync the model IDs to the RuntimeContext from the session's model_ref."""
|
|
118
|
+
ctx.model_id = sess.model_id
|
|
125
119
|
small_ref = config.model_aliases.get("small") if config else None
|
|
126
120
|
if not small_ref:
|
|
127
121
|
provider_key, _ = resolve_model_ref(sess.model_ref)
|
|
@@ -133,7 +127,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
133
127
|
"ollama": "ollama/llama3.1",
|
|
134
128
|
}
|
|
135
129
|
small_ref = _small_defaults.get(provider_key, sess.model_ref)
|
|
136
|
-
|
|
130
|
+
ctx.small_model_ref = small_ref
|
|
137
131
|
|
|
138
132
|
# Load project configuration
|
|
139
133
|
config = load_config()
|
|
@@ -143,6 +137,8 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
143
137
|
console.print(f"[dim]Loaded {len(config.commands)} custom command(s): {', '.join(f'/{k}' for k in config.commands)}[/dim]")
|
|
144
138
|
if config.skills:
|
|
145
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]")
|
|
146
142
|
if config.custom_agents:
|
|
147
143
|
primary = [k for k, v in config.custom_agents.items() if v.mode == "primary"]
|
|
148
144
|
subagents = [k for k, v in config.custom_agents.items() if v.mode == "subagent"]
|
|
@@ -155,8 +151,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
155
151
|
from aru.tools.codebase import set_custom_agents
|
|
156
152
|
set_custom_agents(config.custom_agents)
|
|
157
153
|
if config.permissions:
|
|
158
|
-
perm_config = parse_permission_config(config.permissions)
|
|
159
|
-
set_perm_config(perm_config)
|
|
154
|
+
ctx.perm_config = parse_permission_config(config.permissions)
|
|
160
155
|
console.print("[dim]Loaded permission config[/dim]")
|
|
161
156
|
|
|
162
157
|
extra_instructions = config.get_extra_instructions()
|
|
@@ -188,8 +183,9 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
188
183
|
_sync_model(session)
|
|
189
184
|
_render_home(session, skip_permissions)
|
|
190
185
|
|
|
191
|
-
# Wire file-mutation callback
|
|
192
|
-
|
|
186
|
+
# Wire file-mutation callback and atexit cleanup
|
|
187
|
+
ctx.on_file_mutation = session.invalidate_context_cache
|
|
188
|
+
atexit.register(lambda: cleanup_processes(ctx.tracked_processes))
|
|
193
189
|
|
|
194
190
|
planner = None
|
|
195
191
|
executor = None
|
|
@@ -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():
|
|
@@ -204,7 +204,7 @@ async def compact_conversation(
|
|
|
204
204
|
Uses a small/fast model for the summarization to minimize cost.
|
|
205
205
|
Falls back to simple truncation if the agent call fails.
|
|
206
206
|
"""
|
|
207
|
-
from aru.
|
|
207
|
+
from aru.runtime import get_ctx
|
|
208
208
|
from aru.providers import create_model
|
|
209
209
|
|
|
210
210
|
prompt = build_compaction_prompt(history, plan_task)
|
|
@@ -212,7 +212,7 @@ async def compact_conversation(
|
|
|
212
212
|
try:
|
|
213
213
|
from agno.agent import Agent
|
|
214
214
|
|
|
215
|
-
small_ref =
|
|
215
|
+
small_ref = get_ctx().small_model_ref
|
|
216
216
|
compactor = Agent(
|
|
217
217
|
name="Compactor",
|
|
218
218
|
model=create_model(small_ref, max_tokens=2048),
|
|
@@ -19,15 +19,16 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import fnmatch
|
|
21
21
|
import os
|
|
22
|
-
import threading
|
|
23
22
|
from contextlib import contextmanager
|
|
24
23
|
from dataclasses import dataclass, field
|
|
25
24
|
from typing import Any, Generator, Literal
|
|
26
25
|
|
|
27
|
-
from rich.console import
|
|
26
|
+
from rich.console import Group
|
|
28
27
|
from rich.panel import Panel
|
|
29
28
|
from rich.text import Text
|
|
30
29
|
|
|
30
|
+
from aru.runtime import get_ctx
|
|
31
|
+
|
|
31
32
|
PermissionAction = Literal["allow", "ask", "deny"]
|
|
32
33
|
|
|
33
34
|
VALID_ACTIONS: set[str] = {"allow", "ask", "deny"}
|
|
@@ -96,62 +97,24 @@ for _prefix in SAFE_COMMAND_PREFIXES:
|
|
|
96
97
|
|
|
97
98
|
|
|
98
99
|
# ---------------------------------------------------------------------------
|
|
99
|
-
#
|
|
100
|
+
# Thin wrappers over RuntimeContext (preserve public API for callers)
|
|
100
101
|
# ---------------------------------------------------------------------------
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
_skip_permissions: bool = False
|
|
105
|
-
_permission_lock = threading.Lock()
|
|
106
|
-
_live = None
|
|
107
|
-
_display = None
|
|
108
|
-
_console = Console()
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# ---------------------------------------------------------------------------
|
|
112
|
-
# Setters
|
|
113
|
-
# ---------------------------------------------------------------------------
|
|
103
|
+
def set_config(config: PermissionConfig) -> None:
|
|
104
|
+
get_ctx().perm_config = config
|
|
114
105
|
|
|
115
|
-
def set_config(config: PermissionConfig):
|
|
116
|
-
global _config
|
|
117
|
-
_config = config
|
|
118
106
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
global _skip_permissions
|
|
122
|
-
_skip_permissions = value
|
|
107
|
+
def set_skip_permissions(value: bool) -> None:
|
|
108
|
+
get_ctx().skip_permissions = value
|
|
123
109
|
|
|
124
110
|
|
|
125
111
|
def get_skip_permissions() -> bool:
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def set_live(live):
|
|
130
|
-
global _live
|
|
131
|
-
_live = live
|
|
132
|
-
|
|
112
|
+
return get_ctx().skip_permissions
|
|
133
113
|
|
|
134
|
-
def set_display(display):
|
|
135
|
-
global _display
|
|
136
|
-
_display = display
|
|
137
114
|
|
|
138
|
-
|
|
139
|
-
def set_console(console: Console):
|
|
140
|
-
global _console
|
|
141
|
-
_console = console
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def reset_session():
|
|
115
|
+
def reset_session() -> None:
|
|
145
116
|
"""Reset session-level permission state (call between conversations)."""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# ---------------------------------------------------------------------------
|
|
150
|
-
# Agent-level permission scoping
|
|
151
|
-
# ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
_config_stack: list[PermissionConfig] = []
|
|
154
|
-
_session_stack: list[set[tuple[str, str]]] = []
|
|
117
|
+
get_ctx().session_allowed.clear()
|
|
155
118
|
|
|
156
119
|
|
|
157
120
|
def merge_configs(base: PermissionConfig, overlay: PermissionConfig) -> PermissionConfig:
|
|
@@ -175,22 +138,22 @@ def permission_scope(overlay_raw: dict[str, Any] | None) -> Generator[None, None
|
|
|
175
138
|
Each scope gets its own fresh "always" session memory, so agent approvals
|
|
176
139
|
don't leak to the global scope or other agents.
|
|
177
140
|
"""
|
|
178
|
-
global _config, _session_allowed
|
|
179
141
|
if not overlay_raw:
|
|
180
142
|
yield
|
|
181
143
|
return
|
|
182
144
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
145
|
+
ctx = get_ctx()
|
|
146
|
+
ctx.config_stack.append(ctx.perm_config)
|
|
147
|
+
ctx.session_stack.append(ctx.session_allowed)
|
|
148
|
+
ctx.session_allowed = set()
|
|
186
149
|
|
|
187
150
|
overlay = parse_permission_config(overlay_raw)
|
|
188
|
-
|
|
151
|
+
ctx.perm_config = merge_configs(ctx.perm_config, overlay)
|
|
189
152
|
try:
|
|
190
153
|
yield
|
|
191
154
|
finally:
|
|
192
|
-
|
|
193
|
-
|
|
155
|
+
ctx.perm_config = ctx.config_stack.pop()
|
|
156
|
+
ctx.session_allowed = ctx.session_stack.pop()
|
|
194
157
|
|
|
195
158
|
|
|
196
159
|
# ---------------------------------------------------------------------------
|
|
@@ -304,8 +267,9 @@ def _build_rules(category: str) -> list[PermissionRule]:
|
|
|
304
267
|
rules.extend(_SENSITIVE_FILE_RULES)
|
|
305
268
|
|
|
306
269
|
# Add user-configured rules
|
|
307
|
-
|
|
308
|
-
|
|
270
|
+
ctx = get_ctx()
|
|
271
|
+
if category in ctx.perm_config.categories:
|
|
272
|
+
rules.extend(ctx.perm_config.categories[category])
|
|
309
273
|
|
|
310
274
|
return rules
|
|
311
275
|
|
|
@@ -382,7 +346,7 @@ def _resolve_bash_compound(command: str) -> tuple[PermissionAction, str]:
|
|
|
382
346
|
def _resolve_bash_single(command: str) -> tuple[PermissionAction, str]:
|
|
383
347
|
"""Resolve permission for a single (non-compound) bash command."""
|
|
384
348
|
rules = _build_rules("bash")
|
|
385
|
-
result: PermissionAction = CATEGORY_DEFAULTS.get("bash",
|
|
349
|
+
result: PermissionAction = CATEGORY_DEFAULTS.get("bash", get_ctx().perm_config.default)
|
|
386
350
|
matched_pattern = "*"
|
|
387
351
|
|
|
388
352
|
for rule in rules:
|
|
@@ -419,11 +383,12 @@ def resolve_permission(
|
|
|
419
383
|
4. For others: walk rules (defaults + user config), last-match-wins
|
|
420
384
|
5. Fallback: category default, then global default
|
|
421
385
|
"""
|
|
422
|
-
|
|
386
|
+
ctx = get_ctx()
|
|
387
|
+
if ctx.skip_permissions:
|
|
423
388
|
return ("allow", "*")
|
|
424
389
|
|
|
425
390
|
# Check session memory
|
|
426
|
-
for cat, pattern in
|
|
391
|
+
for cat, pattern in ctx.session_allowed:
|
|
427
392
|
if cat == category and _match_rule(pattern, subject):
|
|
428
393
|
return ("allow", pattern)
|
|
429
394
|
|
|
@@ -433,7 +398,7 @@ def resolve_permission(
|
|
|
433
398
|
|
|
434
399
|
# All other categories
|
|
435
400
|
rules = _build_rules(category)
|
|
436
|
-
result: PermissionAction = CATEGORY_DEFAULTS.get(category,
|
|
401
|
+
result: PermissionAction = CATEGORY_DEFAULTS.get(category, ctx.perm_config.default)
|
|
437
402
|
matched_pattern = "*"
|
|
438
403
|
|
|
439
404
|
for rule in rules:
|
|
@@ -465,7 +430,8 @@ def check_permission(
|
|
|
465
430
|
return False
|
|
466
431
|
|
|
467
432
|
# action == "ask" -> prompt user
|
|
468
|
-
|
|
433
|
+
ctx = get_ctx()
|
|
434
|
+
with ctx.permission_lock:
|
|
469
435
|
# Re-check after acquiring lock (another thread may have resolved it)
|
|
470
436
|
action2, pattern2 = resolve_permission(category, subject)
|
|
471
437
|
if action2 == "allow":
|
|
@@ -474,25 +440,25 @@ def check_permission(
|
|
|
474
440
|
return False
|
|
475
441
|
|
|
476
442
|
# Pause Live and flush already-streamed content
|
|
477
|
-
if
|
|
478
|
-
|
|
479
|
-
if
|
|
480
|
-
|
|
443
|
+
if ctx.live:
|
|
444
|
+
ctx.live.stop()
|
|
445
|
+
if ctx.display:
|
|
446
|
+
ctx.display.flush()
|
|
481
447
|
|
|
482
448
|
title = f"{category}: {subject}" if subject else category
|
|
483
|
-
|
|
484
|
-
|
|
449
|
+
ctx.console.print()
|
|
450
|
+
ctx.console.print(Panel(
|
|
485
451
|
display_details,
|
|
486
452
|
title=f"[bold yellow]{title}[/bold yellow]",
|
|
487
453
|
border_style="yellow",
|
|
488
454
|
expand=False,
|
|
489
455
|
))
|
|
490
456
|
try:
|
|
491
|
-
answer =
|
|
457
|
+
answer = ctx.console.input(
|
|
492
458
|
"[bold yellow]Allow? (y)es once / (a)lways / (n)o:[/bold yellow] "
|
|
493
459
|
).strip().lower()
|
|
494
460
|
if answer in ("a", "always", "all"):
|
|
495
|
-
|
|
461
|
+
ctx.session_allowed.add((category, matched_pattern))
|
|
496
462
|
allowed = True
|
|
497
463
|
else:
|
|
498
464
|
allowed = answer in ("y", "yes", "s", "sim")
|
|
@@ -500,8 +466,8 @@ def check_permission(
|
|
|
500
466
|
allowed = False
|
|
501
467
|
|
|
502
468
|
# Resume Live display
|
|
503
|
-
if
|
|
504
|
-
|
|
505
|
-
|
|
469
|
+
if ctx.live:
|
|
470
|
+
ctx.live.start()
|
|
471
|
+
ctx.live._live_render._shape = None
|
|
506
472
|
|
|
507
473
|
return allowed
|