aru-code 0.9.0__tar.gz → 0.10.1__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.9.0/aru_code.egg-info → aru_code-0.10.1}/PKG-INFO +16 -5
  2. {aru_code-0.9.0 → aru_code-0.10.1}/README.md +15 -4
  3. aru_code-0.10.1/aru/__init__.py +1 -0
  4. {aru_code-0.9.0 → aru_code-0.10.1}/aru/agent_factory.py +7 -2
  5. {aru_code-0.9.0 → aru_code-0.10.1}/aru/cli.py +10 -1
  6. {aru_code-0.9.0 → aru_code-0.10.1}/aru/config.py +20 -15
  7. {aru_code-0.9.0 → aru_code-0.10.1}/aru/tools/codebase.py +17 -4
  8. {aru_code-0.9.0 → aru_code-0.10.1/aru_code.egg-info}/PKG-INFO +16 -5
  9. {aru_code-0.9.0 → aru_code-0.10.1}/pyproject.toml +1 -1
  10. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_config.py +78 -27
  11. aru_code-0.9.0/aru/__init__.py +0 -1
  12. {aru_code-0.9.0 → aru_code-0.10.1}/LICENSE +0 -0
  13. {aru_code-0.9.0 → aru_code-0.10.1}/aru/agents/__init__.py +0 -0
  14. {aru_code-0.9.0 → aru_code-0.10.1}/aru/agents/base.py +0 -0
  15. {aru_code-0.9.0 → aru_code-0.10.1}/aru/agents/executor.py +0 -0
  16. {aru_code-0.9.0 → aru_code-0.10.1}/aru/agents/planner.py +0 -0
  17. {aru_code-0.9.0 → aru_code-0.10.1}/aru/commands.py +0 -0
  18. {aru_code-0.9.0 → aru_code-0.10.1}/aru/completers.py +0 -0
  19. {aru_code-0.9.0 → aru_code-0.10.1}/aru/context.py +0 -0
  20. {aru_code-0.9.0 → aru_code-0.10.1}/aru/display.py +0 -0
  21. {aru_code-0.9.0 → aru_code-0.10.1}/aru/permissions.py +0 -0
  22. {aru_code-0.9.0 → aru_code-0.10.1}/aru/providers.py +0 -0
  23. {aru_code-0.9.0 → aru_code-0.10.1}/aru/runner.py +0 -0
  24. {aru_code-0.9.0 → aru_code-0.10.1}/aru/runtime.py +0 -0
  25. {aru_code-0.9.0 → aru_code-0.10.1}/aru/session.py +0 -0
  26. {aru_code-0.9.0 → aru_code-0.10.1}/aru/tools/__init__.py +0 -0
  27. {aru_code-0.9.0 → aru_code-0.10.1}/aru/tools/ast_tools.py +0 -0
  28. {aru_code-0.9.0 → aru_code-0.10.1}/aru/tools/gitignore.py +0 -0
  29. {aru_code-0.9.0 → aru_code-0.10.1}/aru/tools/mcp_client.py +0 -0
  30. {aru_code-0.9.0 → aru_code-0.10.1}/aru/tools/ranker.py +0 -0
  31. {aru_code-0.9.0 → aru_code-0.10.1}/aru/tools/tasklist.py +0 -0
  32. {aru_code-0.9.0 → aru_code-0.10.1}/aru_code.egg-info/SOURCES.txt +0 -0
  33. {aru_code-0.9.0 → aru_code-0.10.1}/aru_code.egg-info/dependency_links.txt +0 -0
  34. {aru_code-0.9.0 → aru_code-0.10.1}/aru_code.egg-info/entry_points.txt +0 -0
  35. {aru_code-0.9.0 → aru_code-0.10.1}/aru_code.egg-info/requires.txt +0 -0
  36. {aru_code-0.9.0 → aru_code-0.10.1}/aru_code.egg-info/top_level.txt +0 -0
  37. {aru_code-0.9.0 → aru_code-0.10.1}/setup.cfg +0 -0
  38. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_agents_base.py +0 -0
  39. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_ast_tools.py +0 -0
  40. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli.py +0 -0
  41. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli_advanced.py +0 -0
  42. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli_base.py +0 -0
  43. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli_completers.py +0 -0
  44. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli_new.py +0 -0
  45. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli_run_cli.py +0 -0
  46. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli_session.py +0 -0
  47. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_cli_shell.py +0 -0
  48. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_codebase.py +0 -0
  49. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_context.py +0 -0
  50. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_executor.py +0 -0
  51. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_gitignore.py +0 -0
  52. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_main.py +0 -0
  53. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_mcp_client.py +0 -0
  54. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_permissions.py +0 -0
  55. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_planner.py +0 -0
  56. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_providers.py +0 -0
  57. {aru_code-0.9.0 → aru_code-0.10.1}/tests/test_ranker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.9.0
3
+ Version: 0.10.1
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -69,7 +69,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
69
69
  pip install aru-code
70
70
  ```
71
71
 
72
- > **Requirements:** Python 3.13+
72
+ > **Requirements:** Python 3.11+
73
73
 
74
74
  ### 2. Configure the API Key
75
75
 
@@ -340,7 +340,7 @@ All resolved content is combined and appended to the agent's system prompt along
340
340
  └── SKILL.md
341
341
  ```
342
342
 
343
- Command files support frontmatter with `description` and the `$INPUT` template variable for arguments.
343
+ Command files support frontmatter with `description`, `agent`, and `model` fields, plus OpenCode-style argument placeholders: `$ARGUMENTS` (full string), `$1`/`$2` (positional), and `$ARGUMENTS[N]` (0-indexed).
344
344
 
345
345
  ### Custom Agents
346
346
 
@@ -374,11 +374,22 @@ performance, and readability. Do NOT modify files.
374
374
 
375
375
  #### Invocation
376
376
 
377
+ There are three ways to invoke a custom agent:
378
+
379
+ | Method | Syntax | When to use |
380
+ |--------|--------|-------------|
381
+ | **Slash command** | `/reviewer src/auth.py` | Directly invoke a `primary` agent by name |
382
+ | **@mention** | `@reviewer check this function` | Mention an agent anywhere in your message |
383
+ | **delegate_task** | Automatic (subagents only) | Subagent names and descriptions are injected into the `delegate_task` tool description, so the LLM sees them and can call `delegate_task(task="...", agent="name")` on its own when it judges the task fits |
384
+
377
385
  ```
378
- aru> /reviewer src/auth.py # invoke by slash + filename (without .md)
379
- aru> /agents # list all custom agents
386
+ aru> /reviewer src/auth.py # slash command (primary agents)
387
+ aru> @reviewer check the auth module # @mention (primary or subagent)
388
+ aru> /agents # list all custom agents
380
389
  ```
381
390
 
391
+ > **Note:** Slash commands (`/name`) are only available for `primary` agents — subagents are blocked with a warning. `@mention` works for any agent regardless of mode. Subagents can be invoked in two ways: automatically by the LLM via `delegate_task`, or manually by the user via `@name`.
392
+
382
393
  #### Discovery paths
383
394
 
384
395
  Agents are discovered from multiple locations (later overrides earlier):
@@ -22,7 +22,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
22
22
  pip install aru-code
23
23
  ```
24
24
 
25
- > **Requirements:** Python 3.13+
25
+ > **Requirements:** Python 3.11+
26
26
 
27
27
  ### 2. Configure the API Key
28
28
 
@@ -293,7 +293,7 @@ All resolved content is combined and appended to the agent's system prompt along
293
293
  └── SKILL.md
294
294
  ```
295
295
 
296
- Command files support frontmatter with `description` and the `$INPUT` template variable for arguments.
296
+ Command files support frontmatter with `description`, `agent`, and `model` fields, plus OpenCode-style argument placeholders: `$ARGUMENTS` (full string), `$1`/`$2` (positional), and `$ARGUMENTS[N]` (0-indexed).
297
297
 
298
298
  ### Custom Agents
299
299
 
@@ -327,11 +327,22 @@ performance, and readability. Do NOT modify files.
327
327
 
328
328
  #### Invocation
329
329
 
330
+ There are three ways to invoke a custom agent:
331
+
332
+ | Method | Syntax | When to use |
333
+ |--------|--------|-------------|
334
+ | **Slash command** | `/reviewer src/auth.py` | Directly invoke a `primary` agent by name |
335
+ | **@mention** | `@reviewer check this function` | Mention an agent anywhere in your message |
336
+ | **delegate_task** | Automatic (subagents only) | Subagent names and descriptions are injected into the `delegate_task` tool description, so the LLM sees them and can call `delegate_task(task="...", agent="name")` on its own when it judges the task fits |
337
+
330
338
  ```
331
- aru> /reviewer src/auth.py # invoke by slash + filename (without .md)
332
- aru> /agents # list all custom agents
339
+ aru> /reviewer src/auth.py # slash command (primary agents)
340
+ aru> @reviewer check the auth module # @mention (primary or subagent)
341
+ aru> /agents # list all custom agents
333
342
  ```
334
343
 
344
+ > **Note:** Slash commands (`/name`) are only available for `primary` agents — subagents are blocked with a warning. `@mention` works for any agent regardless of mode. Subagents can be invoked in two ways: automatically by the LLM via `delegate_task`, or manually by the user via `@name`.
345
+
335
346
  #### Discovery paths
336
347
 
337
348
  Agents are discovered from multiple locations (later overrides earlier):
@@ -0,0 +1 @@
1
+ __version__ = "0.10.1"
@@ -8,7 +8,11 @@ from aru.providers import create_model
8
8
  from aru.session import Session
9
9
 
10
10
 
11
- def create_general_agent(session: Session, config: AgentConfig | None = None):
11
+ def create_general_agent(
12
+ session: Session,
13
+ config: AgentConfig | None = None,
14
+ model_override: str | None = None,
15
+ ):
12
16
  """Create the general-purpose agent."""
13
17
  from agno.agent import Agent
14
18
  from agno.compression.manager import CompressionManager
@@ -17,10 +21,11 @@ def create_general_agent(session: Session, config: AgentConfig | None = None):
17
21
  from aru.runtime import get_ctx
18
22
 
19
23
  extra = config.get_extra_instructions() if config else ""
24
+ model_ref = model_override or session.model_ref
20
25
 
21
26
  return Agent(
22
27
  name="Aru",
23
- model=create_model(session.model_ref, max_tokens=8192),
28
+ model=create_model(model_ref, max_tokens=8192),
24
29
  tools=GENERAL_TOOLS,
25
30
  instructions=_build_instructions("general", extra),
26
31
  markdown=True,
@@ -430,7 +430,16 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
430
430
  prompt = render_command_template(cmd_def.template, cmd_args)
431
431
  console.print(f"[bold magenta]Running /{cmd_name}...[/bold magenta]")
432
432
 
433
- agent = create_general_agent(session, config)
433
+ if cmd_def.agent and cmd_def.agent in config.custom_agents:
434
+ agent_def = config.custom_agents[cmd_def.agent]
435
+ agent = create_custom_agent_instance(agent_def, session, config)
436
+ elif cmd_def.agent:
437
+ console.print(f"[yellow]Warning: agent '{cmd_def.agent}' not found, using default[/yellow]")
438
+ agent = create_general_agent(session, config, model_override=cmd_def.model)
439
+ elif cmd_def.model:
440
+ agent = create_general_agent(session, config, model_override=cmd_def.model)
441
+ else:
442
+ agent = create_general_agent(session, config)
434
443
  session.add_message("user", user_input)
435
444
  run_result = await run_agent_capture(agent, prompt, session)
436
445
  if run_result.content:
@@ -27,6 +27,8 @@ class CustomCommand:
27
27
  description: str
28
28
  template: str
29
29
  source_path: str
30
+ agent: str | None = None
31
+ model: str | None = None
30
32
 
31
33
 
32
34
  @dataclass
@@ -325,6 +327,8 @@ def _load_commands(agents_dir: Path) -> dict[str, CustomCommand]:
325
327
  description=description,
326
328
  template=body,
327
329
  source_path=str(filepath),
330
+ agent=metadata.get("agent") or None,
331
+ model=metadata.get("model") or None,
328
332
  )
329
333
 
330
334
  return commands
@@ -518,26 +522,17 @@ def load_config(cwd: str | None = None) -> AgentConfig:
518
522
  return config
519
523
 
520
524
 
521
- def render_command_template(template: str, user_input: str) -> str:
522
- """Render a command template with user input.
523
-
524
- Replaces $INPUT with the user's arguments.
525
- Also supports $SELECTION (empty if not provided) for future use.
526
- """
527
- result = template.replace("$INPUT", user_input)
528
- result = result.replace("$SELECTION", "")
529
- return result
530
-
531
-
532
- def render_skill_template(content: str, arguments: str) -> str:
533
- """Render a skill template with argument substitution (agentskills.io).
525
+ def render_template_arguments(
526
+ content: str, arguments: str, *, context_label: str = "Argument",
527
+ ) -> str:
528
+ """Render a template with $ARGUMENTS / $1 / $2 substitution.
534
529
 
535
530
  Supports:
536
531
  - $ARGUMENTS: Full argument string
537
532
  - $ARGUMENTS[N]: Nth argument (0-indexed)
538
533
  - $1, $2, ...: Nth argument (1-indexed, shell-style)
539
534
 
540
- Also prepends an explicit argument context block so the agent cannot
535
+ Also prepends an explicit context block so the agent cannot
541
536
  miss or misread the user-supplied value.
542
537
  """
543
538
  parts = arguments.split() if arguments else []
@@ -561,7 +556,17 @@ def render_skill_template(content: str, arguments: str) -> str:
561
556
 
562
557
  # Prepend an explicit context block so the agent cannot miss the argument
563
558
  if arguments and arguments.strip():
564
- header = f"> **Skill argument:** `{arguments.strip()}`\n> Use this value exactly where the skill instructions reference the argument.\n\n"
559
+ header = f"> **{context_label}:** `{arguments.strip()}`\n> Use this value exactly where the instructions reference the argument.\n\n"
565
560
  result = header + result
566
561
 
567
562
  return result
563
+
564
+
565
+ def render_command_template(template: str, user_input: str) -> str:
566
+ """Render a command template with OpenCode-style argument substitution."""
567
+ return render_template_arguments(template, user_input, context_label="Command argument")
568
+
569
+
570
+ def render_skill_template(content: str, arguments: str) -> str:
571
+ """Render a skill template with argument substitution (agentskills.io)."""
572
+ return render_template_arguments(content, arguments, context_label="Skill argument")
@@ -1023,8 +1023,19 @@ async def delegate_task(task: str, context: str = "", agent: str = "") -> str:
1023
1023
 
1024
1024
  agent_perm = None
1025
1025
  custom_agent_defs = get_ctx().custom_agent_defs
1026
- if agent and agent in custom_agent_defs:
1027
- agent_def = custom_agent_defs[agent]
1026
+ # Agno may pass the caller Agent object instead of a string — coerce to str
1027
+ agent_name = str(agent) if agent and isinstance(agent, str) else ""
1028
+
1029
+ # Print delegation info so the user sees what's happening
1030
+ from rich.console import Console
1031
+ _console = Console()
1032
+ if agent_name and agent_name in custom_agent_defs:
1033
+ _console.print(f"[dim] → Delegating to agent [bold]{agent_name}[/bold] (task: {task[:80]}{'...' if len(task) > 80 else ''})[/dim]")
1034
+ else:
1035
+ _console.print(f"[dim] → Delegating to sub-agent #{agent_id} (task: {task[:80]}{'...' if len(task) > 80 else ''})[/dim]")
1036
+
1037
+ if agent_name and agent_name in custom_agent_defs:
1038
+ agent_def = custom_agent_defs[agent_name]
1028
1039
  agent_perm = agent_def.permission
1029
1040
  tools = resolve_tools(agent_def.tools) if agent_def.tools else list(_SUBAGENT_TOOLS)
1030
1041
  tools = [t for t in tools if t is not delegate_task]
@@ -1186,13 +1197,15 @@ def _update_delegate_task_docstring():
1186
1197
  Args:
1187
1198
  task: What the sub-agent should do.
1188
1199
  context: Optional extra context (file paths, constraints).
1189
- agent: Optional custom agent name to use instead of the generic sub-agent."""
1200
+ agent: Name of a specialized agent to use. ALWAYS prefer a specialized agent when one matches the task."""
1190
1201
 
1191
1202
  custom_agent_defs = get_ctx().custom_agent_defs
1192
1203
  if custom_agent_defs:
1193
- lines = [f"\n\n Available specialized agents (use the agent parameter to invoke):"]
1204
+ lines = [f"\n\n IMPORTANT: When a specialized agent matches the task, you MUST pass its name in the agent parameter."]
1205
+ lines.append(f" Available specialized agents:")
1194
1206
  for name, agent_def in custom_agent_defs.items():
1195
1207
  lines.append(f" - agent=\"{name}\": {agent_def.description}")
1208
+ lines.append(f"\n If no specialized agent fits, omit the agent parameter to use a generic sub-agent.")
1196
1209
  base_doc += "\n".join(lines)
1197
1210
 
1198
1211
  delegate_task.__doc__ = base_doc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.9.0
3
+ Version: 0.10.1
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -69,7 +69,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
69
69
  pip install aru-code
70
70
  ```
71
71
 
72
- > **Requirements:** Python 3.13+
72
+ > **Requirements:** Python 3.11+
73
73
 
74
74
  ### 2. Configure the API Key
75
75
 
@@ -340,7 +340,7 @@ All resolved content is combined and appended to the agent's system prompt along
340
340
  └── SKILL.md
341
341
  ```
342
342
 
343
- Command files support frontmatter with `description` and the `$INPUT` template variable for arguments.
343
+ Command files support frontmatter with `description`, `agent`, and `model` fields, plus OpenCode-style argument placeholders: `$ARGUMENTS` (full string), `$1`/`$2` (positional), and `$ARGUMENTS[N]` (0-indexed).
344
344
 
345
345
  ### Custom Agents
346
346
 
@@ -374,11 +374,22 @@ performance, and readability. Do NOT modify files.
374
374
 
375
375
  #### Invocation
376
376
 
377
+ There are three ways to invoke a custom agent:
378
+
379
+ | Method | Syntax | When to use |
380
+ |--------|--------|-------------|
381
+ | **Slash command** | `/reviewer src/auth.py` | Directly invoke a `primary` agent by name |
382
+ | **@mention** | `@reviewer check this function` | Mention an agent anywhere in your message |
383
+ | **delegate_task** | Automatic (subagents only) | Subagent names and descriptions are injected into the `delegate_task` tool description, so the LLM sees them and can call `delegate_task(task="...", agent="name")` on its own when it judges the task fits |
384
+
377
385
  ```
378
- aru> /reviewer src/auth.py # invoke by slash + filename (without .md)
379
- aru> /agents # list all custom agents
386
+ aru> /reviewer src/auth.py # slash command (primary agents)
387
+ aru> @reviewer check the auth module # @mention (primary or subagent)
388
+ aru> /agents # list all custom agents
380
389
  ```
381
390
 
391
+ > **Note:** Slash commands (`/name`) are only available for `primary` agents — subagents are blocked with a warning. `@mention` works for any agent regardless of mode. Subagents can be invoked in two ways: automatically by the LLM via `delegate_task`, or manually by the user via `@name`.
392
+
382
393
  #### Discovery paths
383
394
 
384
395
  Agents are discovered from multiple locations (later overrides earlier):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.9.0"
7
+ version = "0.10.1"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -19,6 +19,7 @@ from aru.config import (
19
19
  _url_cache,
20
20
  render_command_template,
21
21
  render_skill_template,
22
+ render_template_arguments,
22
23
  load_config,
23
24
  MAX_README_CHARS,
24
25
  )
@@ -31,13 +32,27 @@ class TestDataClasses:
31
32
  cmd = CustomCommand(
32
33
  name="test",
33
34
  description="Test command",
34
- template="Do $INPUT",
35
+ template="Do $ARGUMENTS",
35
36
  source_path="/path/to/test.md",
36
37
  )
37
38
  assert cmd.name == "test"
38
39
  assert cmd.description == "Test command"
39
- assert cmd.template == "Do $INPUT"
40
+ assert cmd.template == "Do $ARGUMENTS"
40
41
  assert cmd.source_path == "/path/to/test.md"
42
+ assert cmd.agent is None
43
+ assert cmd.model is None
44
+
45
+ def test_custom_command_with_agent_and_model(self):
46
+ cmd = CustomCommand(
47
+ name="review",
48
+ description="Review code",
49
+ template="Review $ARGUMENTS",
50
+ source_path="/path/to/review.md",
51
+ agent="reviewer",
52
+ model="anthropic/claude-sonnet-4-5",
53
+ )
54
+ assert cmd.agent == "reviewer"
55
+ assert cmd.model == "anthropic/claude-sonnet-4-5"
41
56
 
42
57
  def test_skill_creation(self):
43
58
  skill = Skill(
@@ -198,37 +213,57 @@ class TestParseFrontmatter:
198
213
 
199
214
 
200
215
  class TestRenderCommandTemplate:
201
- """Test command template rendering."""
216
+ """Test command template rendering with OpenCode-style arguments."""
202
217
 
203
- def test_render_with_input(self):
204
- template = "Execute $INPUT in the codebase"
205
- result = render_command_template(template, "refactoring")
206
- assert result == "Execute refactoring in the codebase"
218
+ def test_render_with_arguments(self):
219
+ result = render_command_template("Execute $ARGUMENTS in the codebase", "refactoring")
220
+ assert "Execute refactoring in the codebase" in result
207
221
 
208
- def test_render_multiple_input_placeholders(self):
209
- template = "Run $INPUT and verify $INPUT works"
210
- result = render_command_template(template, "tests")
211
- assert result == "Run tests and verify tests works"
222
+ def test_render_positional_args(self):
223
+ result = render_command_template("File: $1, Line: $2", "main.py 42")
224
+ assert "File: main.py, Line: 42" in result
212
225
 
213
- def test_render_removes_selection(self):
214
- template = "Process $INPUT and $SELECTION"
215
- result = render_command_template(template, "data")
216
- assert result == "Process data and "
226
+ def test_render_indexed_args(self):
227
+ result = render_command_template("$ARGUMENTS[0] and $ARGUMENTS[1]", "foo bar")
228
+ assert "foo and bar" in result
217
229
 
218
230
  def test_render_no_placeholders(self):
219
231
  template = "Just plain text"
220
232
  result = render_command_template(template, "input")
221
- assert result == "Just plain text"
233
+ # Header is prepended but template body is unchanged
234
+ assert "Just plain text" in result
222
235
 
223
236
  def test_render_empty_input(self):
224
- template = "Do $INPUT now"
225
- result = render_command_template(template, "")
237
+ result = render_command_template("Do $ARGUMENTS now", "")
226
238
  assert result == "Do now"
227
239
 
228
240
  def test_render_with_special_characters(self):
229
- template = "Run $INPUT"
230
- result = render_command_template(template, "pytest -v --cov")
231
- assert result == "Run pytest -v --cov"
241
+ result = render_command_template("Run $ARGUMENTS", "pytest -v --cov")
242
+ assert "Run pytest -v --cov" in result
243
+
244
+ def test_render_missing_positional(self):
245
+ result = render_command_template("$1 and $3", "first second")
246
+ assert "first and " in result
247
+
248
+ def test_command_context_header(self):
249
+ result = render_command_template("Do $ARGUMENTS", "something")
250
+ assert result.startswith("> **Command argument:**")
251
+
252
+ def test_no_header_when_empty_args(self):
253
+ result = render_command_template("Do $ARGUMENTS", "")
254
+ assert not result.startswith(">")
255
+
256
+
257
+ class TestRenderTemplateArguments:
258
+ """Test the shared render_template_arguments function."""
259
+
260
+ def test_custom_context_label(self):
261
+ result = render_template_arguments("Do $ARGUMENTS", "test", context_label="Custom label")
262
+ assert "> **Custom label:** `test`" in result
263
+
264
+ def test_delegates_correctly(self):
265
+ result = render_template_arguments("$1 + $2 = $ARGUMENTS", "a b")
266
+ assert "a + b = a b" in result
232
267
 
233
268
 
234
269
  class TestLoadConfig:
@@ -274,16 +309,32 @@ class TestLoadConfig:
274
309
  def test_load_config_with_commands(self, tmp_path):
275
310
  commands_dir = tmp_path / ".agents" / "commands"
276
311
  commands_dir.mkdir(parents=True)
277
-
312
+
278
313
  cmd_file = commands_dir / "deploy.md"
279
- cmd_file.write_text("---\ndescription: Deploy the app\n---\nDeploy $INPUT")
280
-
314
+ cmd_file.write_text("---\ndescription: Deploy the app\n---\nDeploy $ARGUMENTS")
315
+
281
316
  config = load_config(str(tmp_path))
282
317
  assert "deploy" in config.commands
283
318
  cmd = config.commands["deploy"]
284
319
  assert cmd.name == "deploy"
285
320
  assert cmd.description == "Deploy the app"
286
- assert cmd.template == "Deploy $INPUT"
321
+ assert cmd.template == "Deploy $ARGUMENTS"
322
+ assert cmd.agent is None
323
+ assert cmd.model is None
324
+
325
+ def test_load_config_command_with_agent_and_model(self, tmp_path):
326
+ commands_dir = tmp_path / ".agents" / "commands"
327
+ commands_dir.mkdir(parents=True)
328
+
329
+ cmd_file = commands_dir / "review.md"
330
+ cmd_file.write_text(
331
+ "---\ndescription: Review code\nagent: reviewer\nmodel: anthropic/claude-sonnet-4-5\n---\nReview $ARGUMENTS"
332
+ )
333
+
334
+ config = load_config(str(tmp_path))
335
+ cmd = config.commands["review"]
336
+ assert cmd.agent == "reviewer"
337
+ assert cmd.model == "anthropic/claude-sonnet-4-5"
287
338
 
288
339
  def test_load_config_command_without_frontmatter(self, tmp_path):
289
340
  commands_dir = tmp_path / ".agents" / "commands"
@@ -409,7 +460,7 @@ class TestLoadConfig:
409
460
 
410
461
  commands_dir = tmp_path / ".agents" / "commands"
411
462
  commands_dir.mkdir(parents=True)
412
- (commands_dir / "test.md").write_text("---\ndescription: Test\n---\nRun $INPUT")
463
+ (commands_dir / "test.md").write_text("---\ndescription: Test\n---\nRun $ARGUMENTS")
413
464
 
414
465
  skill_dir = tmp_path / ".agents" / "skills" / "review"
415
466
  skill_dir.mkdir(parents=True)
@@ -529,7 +580,7 @@ class TestParseSkillMetadata:
529
580
  class TestRenderSkillTemplate:
530
581
  """Test render_skill_template with argument substitution."""
531
582
 
532
- _HEADER = "> **Skill argument:** `{arg}`\n> Use this value exactly where the skill instructions reference the argument.\n\n"
583
+ _HEADER = "> **Skill argument:** `{arg}`\n> Use this value exactly where the instructions reference the argument.\n\n"
533
584
 
534
585
  def test_arguments_substitution(self):
535
586
  result = render_skill_template("Review $ARGUMENTS carefully", "src/main.py")
@@ -1 +0,0 @@
1
- __version__ = "0.9.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