Skip to content

Conversation

@ImBIOS
Copy link

@ImBIOS ImBIOS commented Dec 4, 2025

This PR introduces support for initializing a project with multiple AI assistants simultaneously using the --ai flag.

Changes

  • Modified init command to accept a comma-separated list of agents (e.g., --ai claude,gemini).
  • Updated template download/extraction logic to support merging multiple agent templates into the same project.
  • Updated CLI progress tracking to handle multiple agents.

Related Issues

Usage

specify init my-project --ai claude,gemini

@ImBIOS ImBIOS requested a review from localden as a code owner December 4, 2025 04:36
Copilot AI review requested due to automatic review settings December 4, 2025 04:36
Copilot finished reviewing on behalf of ImBIOS December 4, 2025 04:40
Copy link
Contributor

Copilot AI left a 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_merge parameter to support sequential merging of multiple agent templates
  • Implemented AgentTrackerProxy to 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()]
Copy link

Copilot AI Dec 4, 2025

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)
Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +1133 to +1155
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)

Copy link

Copilot AI Dec 4, 2025

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.

Suggested change
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:

Copilot uses AI. Check for mistakes.
Comment on lines +1133 to +1153
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"]:
Copy link

Copilot AI Dec 4, 2025

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:

  1. Explicitly handling these keys in the proxy if they're needed
  2. Pre-adding all possible keys for each agent in lines 1113-1116
  3. Providing clearer documentation about which keys are intentionally ignored and why
Suggested change
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:

Copilot uses AI. Check for mistakes.
Examples:
specify init my-project
specify init my-project --ai claude
specify init my-project --ai claude,gemini # Initialize with multiple agents
Copy link

Copilot AI Dec 4, 2025

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.

Copilot uses AI. Check for mistakes.
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]")
Copy link

Copilot AI Dec 4, 2025

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.

Suggested change
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]")

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 4, 2025

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.

Copilot uses AI. Check for mistakes.
console.print(f"[cyan]ZIP contains {len(zip_contents)} items[/cyan]")

if is_current_dir:
if is_current_dir or allow_merge:
Copy link

Copilot AI Dec 4, 2025

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).

Suggested change
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:

Copilot uses AI. Check for mistakes.
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"),
Copy link

Copilot AI Dec 4, 2025

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.

Suggested change
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')])}",
),

Copilot uses AI. Check for mistakes.
Comment on lines 1319 to +1320
import importlib.metadata

import platform
Copy link

Copilot AI Dec 4, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1217 to +1229
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)
Copy link

Copilot AI Dec 4, 2025

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.

Copilot uses AI. Check for mistakes.
@localden
Copy link
Collaborator

localden commented Dec 4, 2025

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.

@localden localden closed this Dec 4, 2025
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.

2 participants