-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat: Support multi-agent initialization via CLI #1283
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
Conversation
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.
Pull request overview
This PR adds support for initializing projects with multiple AI agents simultaneously through the specify init command. Users can now specify a comma-separated list of agents via the --ai flag (e.g., --ai claude,gemini), enabling multi-agent workflows in a single project.
Key Changes
- Modified CLI argument parsing to accept and validate comma-separated AI assistant names
- Enhanced template extraction logic with
allow_mergeparameter to support sequential merging of multiple agent templates - Implemented
AgentTrackerProxyto provide per-agent progress tracking during initialization
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| raise typer.Exit(1) | ||
| selected_ai = ai_assistant | ||
| # Split by comma and strip whitespace | ||
| requested_agents = [a.strip() for a in ai_assistant.split(",") if a.strip()] |
Copilot
AI
Dec 4, 2025
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.
When multiple agents are specified with --ai, duplicate agent names are not filtered. For example, --ai claude,claude,gemini would result in downloading and extracting the claude template twice, which is wasteful and could potentially cause issues with file overwrites or inconsistent state.
Consider deduplicating the list while preserving order:
requested_agents = []
seen = set()
for a in ai_assistant.split(","):
agent = a.strip()
if agent and agent not in seen:
requested_agents.append(agent)
seen.add(agent)| requested_agents = [a.strip() for a in ai_assistant.split(",") if a.strip()] | |
| requested_agents = [] | |
| seen = set() | |
| for a in ai_assistant.split(","): | |
| agent = a.strip() | |
| if agent and agent not in seen: | |
| requested_agents.append(agent) | |
| seen.add(agent) |
| for agent in selected_agents: | ||
| # Create a proxy tracker that maps standard keys to agent-specific keys | ||
| class AgentTrackerProxy: | ||
| def __init__(self, tracker, agent): | ||
| self.tracker = tracker | ||
| self.agent = agent | ||
|
|
||
| def start(self, key, detail=""): | ||
| if key in ["fetch", "download", "extract"]: | ||
| self.tracker.start(f"{key}-{self.agent}", detail) | ||
|
|
||
| def complete(self, key, detail=""): | ||
| if key in ["fetch", "download", "extract"]: | ||
| self.tracker.complete(f"{key}-{self.agent}", detail) | ||
|
|
||
| def add(self, key, label): | ||
| # We pre-added main keys, ignore dynamic adds for now or handle if needed | ||
| pass | ||
|
|
||
| def error(self, key, detail=""): | ||
| if key in ["fetch", "download", "extract"]: | ||
| self.tracker.error(f"{key}-{self.agent}", detail) | ||
|
|
Copilot
AI
Dec 4, 2025
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.
The AgentTrackerProxy class is defined inside a loop, which means it's redefined on each iteration. While functionally correct, this is inefficient and unconventional. Consider defining the class outside the loop (before line 1133) and reusing it by instantiating it with different agents within the loop.
| for agent in selected_agents: | |
| # Create a proxy tracker that maps standard keys to agent-specific keys | |
| class AgentTrackerProxy: | |
| def __init__(self, tracker, agent): | |
| self.tracker = tracker | |
| self.agent = agent | |
| def start(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.start(f"{key}-{self.agent}", detail) | |
| def complete(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.complete(f"{key}-{self.agent}", detail) | |
| def add(self, key, label): | |
| # We pre-added main keys, ignore dynamic adds for now or handle if needed | |
| pass | |
| def error(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.error(f"{key}-{self.agent}", detail) | |
| # Create a proxy tracker that maps standard keys to agent-specific keys | |
| class AgentTrackerProxy: | |
| def __init__(self, tracker, agent): | |
| self.tracker = tracker | |
| self.agent = agent | |
| def start(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.start(f"{key}-{self.agent}", detail) | |
| def complete(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.complete(f"{key}-{self.agent}", detail) | |
| def add(self, key, label): | |
| # We pre-added main keys, ignore dynamic adds for now or handle if needed | |
| pass | |
| def error(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.error(f"{key}-{self.agent}", detail) | |
| for agent in selected_agents: |
| for agent in selected_agents: | ||
| # Create a proxy tracker that maps standard keys to agent-specific keys | ||
| class AgentTrackerProxy: | ||
| def __init__(self, tracker, agent): | ||
| self.tracker = tracker | ||
| self.agent = agent | ||
|
|
||
| def start(self, key, detail=""): | ||
| if key in ["fetch", "download", "extract"]: | ||
| self.tracker.start(f"{key}-{self.agent}", detail) | ||
|
|
||
| def complete(self, key, detail=""): | ||
| if key in ["fetch", "download", "extract"]: | ||
| self.tracker.complete(f"{key}-{self.agent}", detail) | ||
|
|
||
| def add(self, key, label): | ||
| # We pre-added main keys, ignore dynamic adds for now or handle if needed | ||
| pass | ||
|
|
||
| def error(self, key, detail=""): | ||
| if key in ["fetch", "download", "extract"]: |
Copilot
AI
Dec 4, 2025
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.
The comment says "We pre-added main keys, ignore dynamic adds for now or handle if needed" but doesn't explain when additional keys might be needed or what the implications are of ignoring them. If the download_and_extract_template function tries to add keys like "zip-list", "extracted-summary", "flatten", or "cleanup" through the proxy, they will be silently ignored, potentially causing tracking inconsistencies.
Consider either:
- Explicitly handling these keys in the proxy if they're needed
- Pre-adding all possible keys for each agent in lines 1113-1116
- Providing clearer documentation about which keys are intentionally ignored and why
| for agent in selected_agents: | |
| # Create a proxy tracker that maps standard keys to agent-specific keys | |
| class AgentTrackerProxy: | |
| def __init__(self, tracker, agent): | |
| self.tracker = tracker | |
| self.agent = agent | |
| def start(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.start(f"{key}-{self.agent}", detail) | |
| def complete(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| self.tracker.complete(f"{key}-{self.agent}", detail) | |
| def add(self, key, label): | |
| # We pre-added main keys, ignore dynamic adds for now or handle if needed | |
| pass | |
| def error(self, key, detail=""): | |
| if key in ["fetch", "download", "extract"]: | |
| # Pre-add all possible keys that might be used for each agent | |
| # This ensures that dynamic adds (e.g., "zip-list", "extracted-summary", "flatten", "cleanup") are tracked per agent | |
| agent_keys = [ | |
| "fetch", "download", "extract", | |
| "zip-list", "extracted-summary", "flatten", "cleanup" | |
| ] | |
| for agent in selected_agents: | |
| for key in agent_keys: | |
| tracker.add(f"{key}-{agent}", f"{key.replace('-', ' ').title()} ({agent})") | |
| # Create a proxy tracker that maps standard keys to agent-specific keys | |
| class AgentTrackerProxy: | |
| def __init__(self, tracker, agent): | |
| self.tracker = tracker | |
| self.agent = agent | |
| self.agent_keys = [ | |
| "fetch", "download", "extract", | |
| "zip-list", "extracted-summary", "flatten", "cleanup" | |
| ] | |
| def start(self, key, detail=""): | |
| if key in self.agent_keys: | |
| self.tracker.start(f"{key}-{self.agent}", detail) | |
| def complete(self, key, detail=""): | |
| if key in self.agent_keys: | |
| self.tracker.complete(f"{key}-{self.agent}", detail) | |
| def add(self, key, label): | |
| # Map dynamic adds to agent-specific keys if recognized | |
| if key in self.agent_keys: | |
| self.tracker.add(f"{key}-{self.agent}", label) | |
| else: | |
| # If an unknown key is added, ignore it but log for debugging | |
| # (Optional: print(f"Warning: Unrecognized tracker key '{key}' for agent '{self.agent}'")) | |
| pass | |
| def error(self, key, detail=""): | |
| if key in self.agent_keys: |
| Examples: | ||
| specify init my-project | ||
| specify init my-project --ai claude | ||
| specify init my-project --ai claude,gemini # Initialize with multiple agents |
Copilot
AI
Dec 4, 2025
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.
[nitpick] The new example specify init my-project --ai claude,gemini # Initialize with multiple agents is excellent documentation of the multi-agent feature. However, consider adding an example or note about potential conflicts when combining certain agents (e.g., if some agents have overlapping configuration files). This would help users make informed decisions about which agents to combine.
| cmd = f"export CODEX_HOME={quoted_path}" | ||
|
|
||
| steps_lines.append(f"{step_num}. Set [cyan]CODEX_HOME[/cyan] environment variable before running Codex: [cyan]{cmd}[/cyan]") |
Copilot
AI
Dec 4, 2025
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.
[nitpick] The Codex-specific setup step (lines 1240-1249) now checks if "codex" is in the selected_agents list, which correctly handles the multi-agent case. However, if multiple agents are selected and one of them is Codex, users might be confused about why only Codex requires an environment variable setup. Consider adding a note in the output indicating this is specific to Codex, or updating the step message to clarify which agent requires this action.
| steps_lines.append(f"{step_num}. Set [cyan]CODEX_HOME[/cyan] environment variable before running Codex: [cyan]{cmd}[/cyan]") | |
| steps_lines.append(f"{step_num}. [bold yellow](Codex only)[/bold yellow] Set [cyan]CODEX_HOME[/cyan] environment variable before running Codex: [cyan]{cmd}[/cyan]\n [bright_black]This step is only required if you plan to use Codex.[/bright_black]") |
| self.tracker.error(f"{key}-{self.agent}", detail) | ||
|
|
||
| agent_proxy = AgentTrackerProxy(tracker, agent) | ||
| download_and_extract_template(project_path, agent, selected_script, here, verbose=False, tracker=agent_proxy, client=local_client, debug=debug, github_token=github_token, allow_merge=True) |
Copilot
AI
Dec 4, 2025
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.
The allow_merge=True parameter is always passed when processing multiple agents. However, this causes the first agent template to be extracted with merge mode enabled, which isn't necessary since the directory should be empty initially. Only subsequent agents need merge mode.
Consider passing allow_merge=(i > 0) or tracking whether this is the first agent iteration to optimize the extraction process.
| console.print(f"[cyan]ZIP contains {len(zip_contents)} items[/cyan]") | ||
|
|
||
| if is_current_dir: | ||
| if is_current_dir or allow_merge: |
Copilot
AI
Dec 4, 2025
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.
The allow_merge parameter enables merging when True, but the logic if is_current_dir or allow_merge: treats both conditions identically. When allow_merge=True for the first agent in a new directory, the merge path is taken unnecessarily. This could introduce overhead from the temporary directory extraction when a direct extraction would suffice for the first agent.
Consider refactoring to only use merge mode when actually needed (i.e., when the directory already contains files from a previous agent extraction).
| if is_current_dir or allow_merge: | |
| # Only use merge path if is_current_dir, or allow_merge and directory is non-empty | |
| use_merge = is_current_dir or (allow_merge and project_path.exists() and any(project_path.iterdir())) | |
| if use_merge: |
| def init( | ||
| project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), | ||
| ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, q, bob, or qoder "), | ||
| ai_assistant: str = typer.Option(None, "--ai", help="AI assistant(s) to use (comma-separated): claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, q, bob, or qoder"), |
Copilot
AI
Dec 4, 2025
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.
The help text mentions "qoder" as one of the AI assistants, but looking at the AGENT_CONFIG (lines 194-199), "qoder" exists with requires_cli: True. However, the help text should be consistent with the actual available agents. Consider verifying that all agents listed in the help text exist in AGENT_CONFIG and vice versa to prevent user confusion.
| ai_assistant: str = typer.Option(None, "--ai", help="AI assistant(s) to use (comma-separated): claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, q, bob, or qoder"), | |
| ai_assistant: str = typer.Option( | |
| None, | |
| "--ai", | |
| help=f"AI assistant(s) to use (comma-separated): {', '.join([k for k, v in AGENT_CONFIG.items() if not v.get('requires_cli')])}", | |
| ), |
| import importlib.metadata | ||
|
|
||
| import platform |
Copilot
AI
Dec 4, 2025
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.
[nitpick] The import statements for importlib.metadata and platform have been reordered (swapped lines 1319-1320). While this doesn't affect functionality, Python convention (PEP 8) suggests ordering imports alphabetically within their group. Both are standard library imports, so importlib.metadata should come before platform alphabetically. Consider reverting this change or ensuring there's a specific reason for this ordering.
| for agent in selected_agents: | ||
| agent_config = AGENT_CONFIG.get(agent) | ||
| if agent_config: | ||
| agent_folder = agent_config["folder"] | ||
| security_notice = Panel( | ||
| f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n" | ||
| f"Consider adding [cyan]{agent_folder}[/cyan] (or parts of it) to [cyan].gitignore[/cyan] to prevent accidental credential leakage.", | ||
| title=f"[yellow]Agent Folder Security ({agent_config['name']})[/yellow]", | ||
| border_style="yellow", | ||
| padding=(1, 2) | ||
| ) | ||
| console.print() | ||
| console.print(security_notice) |
Copilot
AI
Dec 4, 2025
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.
[nitpick] When multiple agents are selected, the security warning about agent folders is displayed for each agent individually (lines 1217-1229). While informative, this could become verbose with many agents. Consider consolidating the warnings into a single panel that lists all relevant agent folders, or add a note that multiple agents may have separate security considerations.
|
Thank you for the contribution, @ImBIOS. Please note that large-scale changes like this need to be discussed and accepted by Spec Kit maintainers. There are some design flaws here in terms of conventions (e.g., we do not want to rely on commas for multi-agent). This also doesn't address how conflicts will be resolved when multi-agent configuration is present in the project workspace. |
This PR introduces support for initializing a project with multiple AI assistants simultaneously using the
--aiflag.Changes
initcommand to accept a comma-separated list of agents (e.g.,--ai claude,gemini).Related Issues
Usage