diff --git a/docs/agent-profile.md b/docs/agent-profile.md index 2088d4f67..bc278d2da 100644 --- a/docs/agent-profile.md +++ b/docs/agent-profile.md @@ -34,6 +34,7 @@ Define the agent's role, responsibilities, and behavior here. - `model` (string): AI model to use - `permissionMode` (string, `claude_code` only): One of `"default"`, `"acceptEdits"`, `"plan"`, `"auto"`, `"bypassPermissions"`. When set, the `claude_code` provider passes `--permission-mode ` instead of `--dangerously-skip-permissions`. `cao launch --yolo` overrides this and forces bypass. See [Claude Code permission modes](https://code.claude.com/docs/en/permission-modes). - `native_agent` (string, `claude_code` only): Name of a native Claude Code agent (`~/.claude/agents/`). When set, the provider passes `--agent ` directly and skips system prompt / MCP config decomposition (thin-wrapper mode). See [Claude Code native agent routing](claude-code.md#native-agent-routing). +- `claudeConfig` (object, `claude_code` only): Per-agent Claude Code knobs mapped to CLI flags at launch. `{"effort": ""}` adds `--effort ` and `{"fallback_model": ""}` adds `--fallback-model `. Lets a profile set per-agent reasoning effort without relying on the machine-global `effortLevel` in `~/.claude/settings.json`. The Claude analog of `codexConfig`; the top-level `model` field still maps to `--model`. - `hermesProfile` (string, `hermes` only): Optional Hermes profile wrapper command CAO should launch instead of the default `hermes`, for example one created with `hermes profile alias test-worker`. This is intentionally separate from `codexProfile`: Codex consumes profile names via `codex --profile `, while Hermes aliases are executable commands launched directly as ` chat ...`. See [Hermes Provider](hermes.md). - `prompt` (string): Additional prompt text diff --git a/src/cli_agent_orchestrator/models/agent_profile.py b/src/cli_agent_orchestrator/models/agent_profile.py index 75a81b568..bb8a8ba20 100644 --- a/src/cli_agent_orchestrator/models/agent_profile.py +++ b/src/cli_agent_orchestrator/models/agent_profile.py @@ -51,3 +51,12 @@ class AgentProfile(BaseModel): # example one created by `hermes profile alias `). When omitted, # the Hermes provider launches the default `hermes` command. hermesProfile: Optional[str] = Field(default=None, min_length=1) + + # Claude Code-only. Per-agent Claude Code knobs mapped to CLI flags at + # launch: {"effort": ""} -> `--effort ` and + # {"fallback_model": ""} -> `--fallback-model `. Lets a + # profile set per-agent reasoning effort without relying on the + # machine-global `effortLevel` in ~/.claude/settings.json. This is the + # Claude analog of codexConfig for the codex provider; the top-level + # `model` field still maps to `--model`. + claudeConfig: Optional[Dict[str, Any]] = None diff --git a/src/cli_agent_orchestrator/providers/claude_code.py b/src/cli_agent_orchestrator/providers/claude_code.py index 7c406acf7..bdd0457ba 100644 --- a/src/cli_agent_orchestrator/providers/claude_code.py +++ b/src/cli_agent_orchestrator/providers/claude_code.py @@ -120,6 +120,21 @@ def _build_claude_command(self) -> str: if profile.model: command_parts.extend(["--model", profile.model]) + # Apply Claude Code-only per-agent knobs from claudeConfig: + # effort -> --effort + # fallback_model -> --fallback-model + # Claude analog of codexConfig: per-agent reasoning effort without + # depending on the machine-global effortLevel in + # ~/.claude/settings.json. + claude_config = getattr(profile, "claudeConfig", None) + if isinstance(claude_config, dict): + effort = claude_config.get("effort") + if effort: + command_parts.extend(["--effort", str(effort)]) + fallback_model = claude_config.get("fallback_model") + if fallback_model: + command_parts.extend(["--fallback-model", str(fallback_model)]) + # Add system prompt - escape newlines to prevent tmux chunking issues system_prompt = profile.system_prompt if profile.system_prompt is not None else "" system_prompt = self._apply_skill_prompt(system_prompt) diff --git a/test/providers/test_claude_code_unit.py b/test/providers/test_claude_code_unit.py index bab0e5145..9a1334b77 100644 --- a/test/providers/test_claude_code_unit.py +++ b/test/providers/test_claude_code_unit.py @@ -756,6 +756,55 @@ def test_build_command_omits_model_when_unset(self, mock_load): assert "--model" not in command +class TestClaudeCodeProviderClaudeConfig: + """Tests that profile.claudeConfig maps to Claude Code CLI flags.""" + + @patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile") + def test_build_command_appends_effort_from_claude_config(self, mock_load): + mock_profile = MagicMock() + mock_profile.model = None + mock_profile.system_prompt = None + mock_profile.mcpServers = None + mock_profile.permissionMode = None + mock_profile.claudeConfig = {"effort": "xhigh"} + mock_load.return_value = mock_profile + + provider = ClaudeCodeProvider("tid", "sess", "win", "agent") + command = provider._build_claude_command() + + assert "--effort xhigh" in command + + @patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile") + def test_build_command_appends_fallback_model_from_claude_config(self, mock_load): + mock_profile = MagicMock() + mock_profile.model = None + mock_profile.system_prompt = None + mock_profile.mcpServers = None + mock_profile.permissionMode = None + mock_profile.claudeConfig = {"fallback_model": "sonnet"} + mock_load.return_value = mock_profile + + provider = ClaudeCodeProvider("tid", "sess", "win", "agent") + command = provider._build_claude_command() + + assert "--fallback-model sonnet" in command + + @patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile") + def test_build_command_omits_effort_when_claude_config_absent(self, mock_load): + mock_profile = MagicMock() + mock_profile.model = None + mock_profile.system_prompt = None + mock_profile.mcpServers = None + mock_profile.permissionMode = None + mock_profile.claudeConfig = None + mock_load.return_value = mock_profile + + provider = ClaudeCodeProvider("tid", "sess", "win", "agent") + command = provider._build_claude_command() + + assert "--effort" not in command + + class TestClaudeCodeProviderPermissionMode: @patch("cli_agent_orchestrator.providers.claude_code.load_agent_profile")