Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion agentdescent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,13 +892,31 @@ def _starter_model_warnings(spec: Dict[str, Any], out: str) -> List[str]:
_PLAIN_REFS = ("openai_compatible", "host_model", "claude", "echo")


def fold_agent_ref(ref: str) -> str:
"""Canonical built-in agent short ref when ``ref`` is a case/hyphen alias.

``claude-code`` and ``CLAUDE_CODE`` become ``claude_code``. A value that
is not such an alias — including ``module:attribute`` and install-only
host names like ``deepseek-harness`` — is returned unchanged, so
:func:`init_agent_unknown` can still hint or refuse it.
"""
if not isinstance(ref, str) or ":" in ref:
return ref
folded = ref.strip().casefold().replace("-", "_")
if folded in _WORKSPACE_REFS + _PLAIN_REFS:
return folded
return ref


def init_agent_unknown(ref: str) -> Optional[str]:
"""Why ``--agent ref`` can never resolve, or None (#745).

``plan`` would refuse a name that is neither a built-in agent short ref nor
``module:attribute``; say so before a spec is written, list only the agent
refs (not the policy classes ``SHORT_REFS`` also holds), and point a host
name such as ``claude-code`` at its ref spelling.
name such as ``deepseek-harness`` at its worker spelling. Callers should
run :func:`fold_agent_ref` first so ``claude-code`` is stored as
``claude_code`` rather than rejected (#884).
"""
agents = _WORKSPACE_REFS + _PLAIN_REFS
if ref in agents or ":" in ref:
Expand Down Expand Up @@ -1113,6 +1131,7 @@ def write_starter_spec(
agent, found = default_agent_ref()
else:
found = True
agent = fold_agent_ref(agent)
# Refuse a missing PATH before kind guess / agent mismatch / writing (#706).
missing = require_existing_target(path, kind)
if missing:
Expand Down
4 changes: 4 additions & 0 deletions changelog.d/884-agent-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed
`--agent` now folds case and `-`/`_` (`claude-code` → `claude_code`,
`openai-compatible` → `openai_compatible`), matching how algorithm names,
parameter names, and `--kind` already resolve (#884).
66 changes: 52 additions & 14 deletions tests/test_init_honor_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,43 @@ def test_policy_slot_keeps_explicit_agent(ws):
assert json.loads((ws / "spec.json").read_text())["agent"] == {"ref": "host_model"}


@pytest.mark.parametrize("ref,hint", [("claude-code", "claude_code"),
("grok-bot", None), ("bogus", None)])
def test_unknown_agent_refused_before_writing(ws, capsys, ref, hint):
# A name `plan` can never resolve (here: install's host spelling) used to be
# written anyway, with an error listing policy classes as agents (#745).
@pytest.mark.parametrize("raw,canonical,target,kind", [
("claude-code", "claude_code", "my-skill", []),
("CLAUDE_CODE", "claude_code", "my-skill", []),
("cli-agent", "cli_agent", "my-skill", []),
("openai-compatible", "openai_compatible", "prompt.txt", ["--kind", "text"]),
("Host-Model", "host_model", "prompt.txt", ["--kind", "text"]),
])
def test_agent_hyphen_case_aliases_fold(ws, raw, canonical, target, kind):
# Same case / "-" / "_" fold as --kind and algorithm params (#884).
path = "prompt.txt" if target == "prompt.txt" else str(ws / target)
assert _init(path, *kind, "--agent", raw) == 0
assert json.loads((ws / "spec.json").read_text())["agent"]["ref"] == canonical


def test_algorithms_run_folds_host_spelling(ws):
rc = cli.main(["algorithms", "run", "archive_selection", str(ws / "my-skill"),
"--agent", "claude-code", "--out", "spec.json"])
# Plan may still fail later (no cases / placeholder model). The alias must
# no longer be refused before write; the spec stores the canonical ref.
assert (ws / "spec.json").exists()
assert json.loads((ws / "spec.json").read_text())["agent"]["ref"] == "claude_code"
assert rc in (0, 2)


def test_text_refuses_folded_workspace_host_spelling(ws, capsys):
# Folding must not bypass the kind mismatch gate (#456).
assert _init("prompt.txt", "--kind", "text", "--agent", "claude-code") == 2
assert not (ws / "spec.json").exists()
err = capsys.readouterr().err
assert "kind='text'" in err and "claude_code" in err


@pytest.mark.parametrize("ref", ["grok-bot", "bogus"])
def test_unknown_agent_refused_before_writing(ws, capsys, ref):
# A name `plan` can never resolve used to be written anyway, with an
# error listing policy classes as agents (#745). Hyphen/case aliases of
# a real worker now fold (#884); leftover unknowns still refuse first.
(ws / "spec.json").write_text('{"keep": 1}')
assert _init(str(ws / "my-skill"), "--agent", ref) == 2
assert json.loads((ws / "spec.json").read_text()) == {"keep": 1}
Expand All @@ -87,15 +119,7 @@ def test_unknown_agent_refused_before_writing(ws, capsys, ref, hint):
assert f"--agent {ref}: not a known agent" in err
assert "claude_code" in err and "opencode" in err
assert "Beam" not in err and "DefaultFusion" not in err
assert ("did you mean `--agent claude_code`" in err) == (hint is not None)


def test_algorithms_run_refuses_unknown_agent(ws, capsys):
rc = cli.main(["algorithms", "run", "archive_selection", str(ws / "my-skill"),
"--agent", "claude-code", "--out", "spec.json"])
assert rc == 2
assert not (ws / "spec.json").exists()
assert "did you mean `--agent claude_code`" in capsys.readouterr().err
assert "did you mean `--agent claude_code`" not in err


@pytest.mark.parametrize("ref", ["claude_code", "dsh", "codex", "cli_agent", "echo"])
Expand Down Expand Up @@ -131,3 +155,17 @@ def test_plain_unknown_agent_gets_no_host_hint(ws, capsys):
assert _init(str(ws / "my-skill"), "--agent", "bogus") == 2
err = capsys.readouterr().err
assert "install host" not in err and "did you mean" not in err


@pytest.mark.parametrize("raw,canonical", [
("claude-code", "claude_code"),
("CLAUDE_CODE", "claude_code"),
("openai-compatible", "openai_compatible"),
("cli-agent", "cli_agent"),
("deepseek-harness", "deepseek-harness"),
("mypkg.models:mine", "mypkg.models:mine"),
("claude_code", "claude_code"),
])
def test_fold_agent_ref(raw, canonical):
assert cli.fold_agent_ref(raw) == canonical

Loading