-
Notifications
You must be signed in to change notification settings - Fork 4
feat(agents): add subagent filtering via frontmatter #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,62 @@ AGENT_TEMPS = { | |
| # Files to skip (not primary agents) | ||
| SKIP_FILES = {"AGENTS.md", "README.md"} | ||
|
|
||
| def parse_frontmatter(filepath): | ||
| """Parse YAML frontmatter from markdown file.""" | ||
| try: | ||
| with open(filepath, 'r', encoding='utf-8') as f: | ||
| content = f.read() | ||
|
|
||
| # Check for frontmatter | ||
| if not content.startswith('---'): | ||
| return {} | ||
|
|
||
| # Find end of frontmatter | ||
| end_idx = content.find('---', 3) | ||
| if end_idx == -1: | ||
| return {} | ||
|
|
||
| frontmatter = content[3:end_idx].strip() | ||
|
|
||
| # Simple YAML parsing for subagents list | ||
| result = {} | ||
| lines = frontmatter.split('\n') | ||
| current_key = None | ||
| current_list = [] | ||
|
|
||
| for line in lines: | ||
| stripped = line.strip() | ||
| # Ignore comments and empty lines | ||
| if not stripped or stripped.startswith('#'): | ||
| continue | ||
|
|
||
| if stripped.startswith('- ') and current_key: | ||
| # List item | ||
| current_list.append(stripped[2:].strip()) | ||
| elif ':' in stripped and not stripped.startswith('-'): | ||
| # Save previous list if any | ||
| if current_key and current_list: | ||
| result[current_key] = current_list | ||
| current_list = [] | ||
|
|
||
| # New key | ||
| key, value = stripped.split(':', 1) | ||
| current_key = key.strip() | ||
| value = value.strip() | ||
| if value: | ||
| result[current_key] = value | ||
| current_key = None | ||
|
|
||
| # Save final list | ||
| if current_key and current_list: | ||
| result[current_key] = current_list | ||
|
|
||
| return result | ||
| except (IOError, OSError, UnicodeDecodeError) as e: | ||
| import sys | ||
| print(f"Warning: Failed to parse frontmatter for {filepath}: {e}", file=sys.stderr) | ||
| return {} | ||
|
|
||
| def filename_to_display(filename): | ||
| """Convert filename to display name.""" | ||
| name = filename.replace(".md", "") | ||
|
|
@@ -190,8 +246,14 @@ def filename_to_display(filename): | |
| # Convert kebab-case to Title-Case | ||
| return "-".join(word.capitalize() for word in name.split("-")) | ||
|
|
||
| def get_agent_config(display_name, filename): | ||
| """Generate agent configuration.""" | ||
| def get_agent_config(display_name, filename, subagents=None): | ||
| """Generate agent configuration. | ||
|
|
||
| Args: | ||
| display_name: Agent display name | ||
| filename: Agent markdown filename | ||
| subagents: Optional list of allowed subagent names (from frontmatter) | ||
| """ | ||
| tools = AGENT_TOOLS.get(display_name, DEFAULT_TOOLS.copy()) | ||
| temp = AGENT_TEMPS.get(display_name, 0.2) | ||
|
|
||
|
|
@@ -209,19 +271,36 @@ def get_agent_config(display_name, filename): | |
| else: | ||
| config["permission"] = {"external_directory": "allow"} | ||
|
|
||
| # Add subagent filtering via permission.task if subagents specified | ||
| # This generates deny-all + allow-specific rules | ||
| if subagents and isinstance(subagents, list) and len(subagents) > 0: | ||
| task_perms = {"*": "deny"} | ||
| for subagent in subagents: | ||
| task_perms[subagent] = "allow" | ||
| config["permission"]["task"] = task_perms | ||
| print(f" {display_name}: filtered to {len(subagents)} subagents") | ||
|
|
||
| return config | ||
|
|
||
| # Discover all root-level .md files | ||
| primary_agents = {} | ||
| discovered = [] | ||
| subagent_filtered_count = 0 | ||
|
|
||
| for filepath in glob.glob(os.path.join(agents_dir, "*.md")): | ||
| filename = os.path.basename(filepath) | ||
| if filename in SKIP_FILES: | ||
| continue | ||
|
|
||
| display_name = filename_to_display(filename) | ||
| primary_agents[display_name] = get_agent_config(display_name, filename) | ||
|
|
||
| # Parse frontmatter for subagents list | ||
| frontmatter = parse_frontmatter(filepath) | ||
| subagents = frontmatter.get('subagents', None) | ||
| if subagents: | ||
| subagent_filtered_count += 1 | ||
|
|
||
| primary_agents[display_name] = get_agent_config(display_name, filename, subagents) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🤖 Was this useful? React with 👍 or 👎 |
||
| discovered.append(display_name) | ||
|
|
||
| # Sort agents: ordered ones first, then alphabetical | ||
|
|
@@ -288,6 +367,8 @@ config['agent'] = sorted_agents | |
|
|
||
| print(f" Auto-discovered {len(sorted_agents)} primary agents from {agents_dir}") | ||
| print(f" Order: {', '.join(list(sorted_agents.keys())[:5])}...") | ||
| if subagent_filtered_count > 0: | ||
| print(f" Subagent filtering: {subagent_filtered_count} agents have permission.task rules") | ||
|
|
||
| # ============================================================================= | ||
| # MCP SERVERS - Ensure required MCP servers are configured | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 696
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 330
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 392
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 654
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 736
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 4612
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 369
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 1073
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 1032
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 472
🏁 Script executed:
Repository: marcusquinn/aidevops
Length of output: 1359
Reject subagent declarations—most do not exist in the framework
The
subagentslist references agents that either don't exist or use incorrect paths. PerAGENTS.mddocumentation, valid agent paths are:seo/dataforseo.md(notdataforseo)seo/google-search-console.md(notgoogle-search-console)services/crm/fluentcrm.md(notfluentcrm)The following subagents are not defined in the framework and must be removed:
guidelines,summarize,keyword-research,serper,bird,general,exploreCorrect the subagent declarations to use valid paths from the
.agent/directory structure, or remove unsupported entries entirely.🤖 Prompt for AI Agents