fix(agent): discover custom subagents from .claude/agents/#151
Merged
Conversation
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>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
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.~/.claude/agents/critic.mdno longer getsUnknown subagent_type: critic. Available: general-purpose, Explore, Plan—criticis now discovered and selectable.plugin:sub:dirs:basewith 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.gitancestor so parent-of-repo agents don't leak.Why
The consumer side (Agent tool resolver,
_available_agentsfor@agent-…mentions) was wired in to readcontext.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 foreffort,positive_int,permission_mode,string_list,hooksutils/markdown_config_loader.py— generic.claude/<subdir>walker with git-root boundary, dedup-by-realpathagent/parse_agent_markdown.py— frontmatter →AgentDefinition, supports kebab + camelCase keys, rejects non-stringnameagent/load_agents_dir.py— discovery orchestrator with last-wins merge[built-in, plugin, user, project, managed]; cached onos.path.realpath(cwd)agent/load_plugin_agents.py— recursive walk, namespaceplugin:sub:dirs:base, stripspermission_mode/hooks/mcp_serversagent/filter_agents_by_mcp.py— case-insensitive substring filter; built-ins exemptWire-up edits:
agent/agent_definitions.py—AgentSourceliteral extended with"project"/"managed"so policy-vs-user is distinguishabletool_system/tools/agent.py—_get_agent_definitionsand_agent_promptcall the loader;make_agent_toolaccepts aget_available_mcp_serversclosure for prompt-time filteringtool_system/defaults.py— threads the MCP closure throughbuild_default_registryrepl/core.py—_available_agentsno longer flattens a wrapping dict (the old code extended a list withdict.values(), producing a single nested list that the mention resolver silently dropped); registry build passes a late-binding closure readingtool_context.mcp_clientsplugins/types.py+plugins/loader.py—LoadedPlugin.agents_pathsread from manifestagentsPath/agentsPathsTest plan
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_agentsflat-list contract.tests/test_agent_*.py,tests/test_skills_*.py,tests/test_plugin_loader.py).~/.claude/agents/critic.md(viaCLAUDE_CONFIG_DIRoverride) makescriticappear inget_agent_definitions_with_overrides(), the rendered Agent tool prompt, andexpand_agent_mentions(\"@agent-critic …\")resolution.~/.claude/agents/<name>.mdand 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