Skip to content

fix(agent): discover custom subagents from .claude/agents/#151

Merged
ericleepi314 merged 1 commit into
mainfrom
fix/custom-subagent-discovery
May 16, 2026
Merged

fix(agent): discover custom subagents from .claude/agents/#151
ericleepi314 merged 1 commit into
mainfrom
fix/custom-subagent-discovery

Conversation

@ericleepi314
Copy link
Copy Markdown
Collaborator

Summary

  • Ports the TypeScript custom-agent discovery layer (loadAgentsDir.ts, markdownConfigLoader.ts, loadPluginAgents.ts) to the Python port so on-disk subagents are visible to the Agent tool, the @agent-… mention resolver, and the tool prompt the model sees.
  • Fixes the original symptom: a user adding ~/.claude/agents/critic.md no longer gets Unknown subagent_type: critic. Available: general-purpose, Explore, Plancritic is now discovered and selectable.
  • Full parity: managed/user/project sources, plugin agents (namespaced as plugin:sub:dirs:base with elevated capabilities stripped), every frontmatter field, MCP-requirement filtering at both prompt-build and per-call dispatch, realpath-keyed cache. Project walk stops at the nearest .git ancestor so parent-of-repo agents don't leak.

Why

The consumer side (Agent tool resolver, _available_agents for @agent-… mentions) was wired in to read context.options.agent_definitions["active_agents"], but nothing in the Python tree ever populated that dict and no loader existed. The producer was simply missing.

What's in the diff

New modules (under src/):

  • utils/frontmatter_validators.py — fail-open validators for effort, positive_int, permission_mode, string_list, hooks
  • utils/markdown_config_loader.py — generic .claude/<subdir> walker with git-root boundary, dedup-by-realpath
  • agent/parse_agent_markdown.py — frontmatter → AgentDefinition, supports kebab + camelCase keys, rejects non-string name
  • agent/load_agents_dir.py — discovery orchestrator with last-wins merge [built-in, plugin, user, project, managed]; cached on os.path.realpath(cwd)
  • agent/load_plugin_agents.py — recursive walk, namespace plugin:sub:dirs:base, strips permission_mode/hooks/mcp_servers
  • agent/filter_agents_by_mcp.py — case-insensitive substring filter; built-ins exempt

Wire-up edits:

  • agent/agent_definitions.pyAgentSource literal extended with "project"/"managed" so policy-vs-user is distinguishable
  • tool_system/tools/agent.py_get_agent_definitions and _agent_prompt call the loader; make_agent_tool accepts a get_available_mcp_servers closure for prompt-time filtering
  • tool_system/defaults.py — threads the MCP closure through build_default_registry
  • repl/core.py_available_agents no longer flattens a wrapping dict (the old code extended a list with dict.values(), producing a single nested list that the mention resolver silently dropped); registry build passes a late-binding closure reading tool_context.mcp_clients
  • plugins/types.py + plugins/loader.pyLoadedPlugin.agents_paths read from manifest agentsPath/agentsPaths

Test plan

  • 31 new tests in tests/agent/ cover: user/project/managed discovery, project-over-user override, built-in override, managed-wins-over-project, malformed-frontmatter isolation, MCP filter, cache invalidation + realpath dedup, git-root boundary, @agent-<custom> mention resolution, plugin namespace collision, plugin trust stripping, parser camelCase aliases, non-string-name fallback, all-fields mapping, _available_agents flat-list contract.
  • 293 prior agent/skills/plugin tests still pass (tests/test_agent_*.py, tests/test_skills_*.py, tests/test_plugin_loader.py).
  • End-to-end smoke: writing ~/.claude/agents/critic.md (via CLAUDE_CONFIG_DIR override) makes critic appear in get_agent_definitions_with_overrides(), the rendered Agent tool prompt, and expand_agent_mentions(\"@agent-critic …\") resolution.
  • Manual: drop a real ~/.claude/agents/<name>.md and exercise via the live REPL to confirm the model sees the agent in the tool description.

Review

Reviewed by `critic` subagent over two rounds: initial REQUEST CHANGES (2 blockers + 5 majors), all addressed, then APPROVE. Four nit follow-ups also applied.

🤖 Generated with Claude Code

The Python port had the consumer side wired in (Agent tool resolver,
@agent- mention expansion) but no producer — no markdown loader, no
frontmatter parser, no directory walker, no plugin agent surface, no
MCP-requirement filter. A user dropping ~/.claude/agents/critic.md
got "Unknown subagent_type: critic. Available: general-purpose,
Explore, Plan" because get_built_in_agents() was the only source.

Port loadAgentsDir.ts + markdownConfigLoader.ts + loadPluginAgents.ts
to Python with full parity: managed/user/project sources, plugin
agents (namespaced as plugin:sub:dirs:base, with elevated capabilities
stripped), every frontmatter field, MCP-requirement filtering at both
prompt-build and per-call dispatch, realpath-keyed cache. Project walk
stops at the nearest .git ancestor so parent-of-repo agents don't leak.

31 new tests; 293 prior tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant