mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
* Add /speckit.converge SDD artifacts and project scaffolding Dogfood the converge feature through Spec Kit's own workflow: - spec.md, plan.md, tasks.md, research, data-model, contracts, quickstart - requirements checklist for the feature - ratified constitution v1.0.0 (.specify/memory) - Specify project scaffolding (.specify/, .github agent + prompt files) Defines a built-in /speckit.converge command that assesses spec/plan/tasks against the codebase and appends remaining work as new tasks (no git, no change tracking, append-only). Implementation not yet started. Excludes unrelated working-tree changes to agents.py, extensions.py, test_extensions.py, catalog.community.json, and README.md. * Implement /speckit.converge command Add the built-in converge command that assesses the codebase against a feature's spec.md, plan.md, and tasks.md and appends remaining unbuilt work as new traceable tasks to tasks.md (append-only; no git, no change tracking). - templates/commands/converge.md: full command body (load artifacts, assess code, classify findings missing/partial/contradicts/unrequested, append '## Phase N — Convergence' tasks with source-ref + gap-type, read-only guardrails, converged branch, handoff, before/after_converge hooks) - Register converge as a core command across all enumeration sites (SKILL_DESCRIPTIONS, _FALLBACK_CORE_COMMAND_NAMES, ARGUMENT_HINTS, and the integration test command lists incl. copilot/generic file inventories) - init.py Next Steps panel + README Core Commands table - tasks.md: T001-T024 complete (T025 manual quickstart pending) Full suite green: 2343 passed. * Record quickstart validation results for /speckit.converge (T025) All six quickstart scenarios validated (GitHub Copilot agent, macOS/zsh): S1 gap->appended traceable task, S2 implement+re-converge, S3 converged leaves tasks.md unchanged, S4 read-only boundaries, S5 missing-prereq stop, S6 cross- integration install (copilot + windsurf). Automated suite: 2343 passed. * Record 2026-06-16 re-verification results for /speckit.converge (T025) * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix integration upgrade deleting settings.json and dropping script +x Two upgrade-path bugs surfaced during converge E2E validation: - copilot upgrade stale-deleted .vscode/settings.json because setup() only tracks the file when it creates it; on upgrade the pre-existing file is merged and left untracked, so Phase 2 stale cleanup removed it. Add an integration-level stale_cleanup_exclusions() hook (CopilotIntegration returns {.vscode/settings.json}) and subtract it from stale_keys. - shared .specify/scripts/*.sh lost their execute bit because the managed refresh rewrites them with the bundled source mode (often 0o644) and nothing restored perms. Call ensure_executable_scripts() after the managed-refresh block (POSIX only). Add regression tests in TestIntegrationUpgrade covering both fixes (validated to fail without the fixes). * fix: resolve markdownlint errors in PR files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: clean up runtime state files from PR Remove .specify state files that are per-project runtime artifacts: - feature.json, init-options.json, integration.json - manifest files, extension registry, bug artifacts These are generated by 'specify init' and should not be committed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: fold converge artifacts from #3003 and #3005 - Add speckit.converge Copilot agent and prompt files (#3003) - Add regression test for Claude argument hints (#3005) - Remove invalid converge entry from Claude argument hints - Fix documentation removing branch-prefix fallback claims Supersedes: #3003, #3005 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove non-converge specify scaffolding from PR Remove .specify/ artifacts, non-converge .github/agents and prompts, and copilot-instructions.md that were generated by 'specify init' and are not part of the converge command feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove SDD spec artifacts from PR Remove specs/001-converge-command/ — the spec/plan/tasks/research SDD artifacts produced while building this feature. spec-kit does not track a specs/ directory on main (those are outputs of running the workflow on the repo, not part of the shipped tool). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove generated Copilot converge command files Remove .github/agents/speckit.converge.agent.md and .github/prompts/speckit.converge.prompt.md — these are generated by 'specify init --integration copilot' from templates/commands/converge.md (all __SPECKIT_COMMAND_*__/{SCRIPT} tokens are resolved). main tracks no .github/agents or .github/prompts files; the template is the source of truth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: split out unrelated integration-upgrade fix Move the stale_cleanup_exclusions / executable-bit upgrade fix (base.py, copilot, _migrate_commands.py, test_integration_subcommand.py) out of this PR into its own change. This PR is now scoped purely to the /speckit.converge command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add converge to core command template ordering converge is a core command in SKILL_DESCRIPTIONS but was missing from _CORE_COMMAND_TEMPLATE_ORDER, so it sorted with the fallback rank. Add it after 'implement' to keep core-command ordering consistent across integrations. Addresses review feedback on #3001. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: make converge findings example neutral Replace the self-referential sample evidence text in the Convergence Findings table with a neutral placeholder so agents are less likely to copy nonsensical template-specific findings into real output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * docs: clarify converge scope and hook outcome wording - Remove FR-specific parenthetical from code-scope rule so it doesn't imply a hard FR-001 reference exists in every feature - Replace unsupported 'pass outcome to hook context' instruction with explicit in-session outcome reporting before hook listing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: align converge task example with tasks format Use (no colon) in the convergence task example so it matches tasks-template formatting and downstream expectations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarification of usage Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * docs: align converge phase/task-id format with tasks template - Use (colon) for consistency with tasks template - Clarify appended task IDs must be zero-padded ( style) - Update checklist example to a concrete zero-padded ID () Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: standardize converge phase heading format Use consistently in converge.md (including the append-only contract section) to match Step 7 and tasks template style. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
820 lines
35 KiB
Python
820 lines
35 KiB
Python
"""specify init command."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import typer
|
|
from rich.live import Live
|
|
from rich.panel import Panel
|
|
|
|
from .._agent_config import (
|
|
AGENT_CONFIG,
|
|
DEFAULT_INIT_INTEGRATION,
|
|
SCRIPT_TYPE_CHOICES,
|
|
)
|
|
from .._assets import (
|
|
_locate_bundled_extension,
|
|
_locate_bundled_preset,
|
|
_locate_bundled_workflow,
|
|
get_speckit_version,
|
|
)
|
|
from .._console import StepTracker, console, select_with_arrows, show_banner
|
|
from .._utils import check_tool
|
|
|
|
|
|
def _stdin_is_interactive() -> bool:
|
|
return sys.stdin.isatty()
|
|
|
|
|
|
def ensure_constitution_from_template(
|
|
project_path: Path, tracker: StepTracker | None = None
|
|
) -> None:
|
|
"""Copy constitution template to memory if it doesn't exist."""
|
|
memory_constitution = project_path / ".specify" / "memory" / "constitution.md"
|
|
template_constitution = (
|
|
project_path / ".specify" / "templates" / "constitution-template.md"
|
|
)
|
|
|
|
if memory_constitution.exists():
|
|
if tracker:
|
|
tracker.add("constitution", "Constitution setup")
|
|
tracker.skip("constitution", "existing file preserved")
|
|
return
|
|
|
|
if not template_constitution.exists():
|
|
if tracker:
|
|
tracker.add("constitution", "Constitution setup")
|
|
tracker.error("constitution", "template not found")
|
|
return
|
|
|
|
try:
|
|
memory_constitution.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(template_constitution, memory_constitution)
|
|
if tracker:
|
|
tracker.add("constitution", "Constitution setup")
|
|
tracker.complete("constitution", "copied from template")
|
|
else:
|
|
console.print("[cyan]Initialized constitution from template[/cyan]")
|
|
except Exception as e:
|
|
if tracker:
|
|
tracker.add("constitution", "Constitution setup")
|
|
tracker.error("constitution", str(e))
|
|
else:
|
|
console.print(
|
|
f"[yellow]Warning: Could not initialize constitution: {e}[/yellow]"
|
|
)
|
|
|
|
|
|
def register(app: typer.Typer) -> None:
|
|
@app.command()
|
|
def init(
|
|
project_name: str = typer.Argument(
|
|
None,
|
|
help="Name for your new project directory (optional if using --here, or use '.' for current directory)",
|
|
),
|
|
script_type: str = typer.Option(
|
|
None, "--script", help="Script type to use: sh or ps"
|
|
),
|
|
ignore_agent_tools: bool = typer.Option(
|
|
False,
|
|
"--ignore-agent-tools",
|
|
help="Skip checks for coding agent tools like Claude Code",
|
|
),
|
|
here: bool = typer.Option(
|
|
False,
|
|
"--here",
|
|
help="Initialize project in the current directory instead of creating a new one",
|
|
),
|
|
force: bool = typer.Option(
|
|
False,
|
|
"--force",
|
|
help="Force merge/overwrite when using --here (skip confirmation)",
|
|
),
|
|
skip_tls: bool = typer.Option(
|
|
False,
|
|
"--skip-tls",
|
|
help="Deprecated (no-op). Previously: skip SSL/TLS verification.",
|
|
hidden=True,
|
|
),
|
|
debug: bool = typer.Option(
|
|
False,
|
|
"--debug",
|
|
help="Deprecated. Previously: show verbose diagnostic output; currently only prints additional diagnostic details on failure.",
|
|
hidden=True,
|
|
),
|
|
github_token: str = typer.Option(
|
|
None,
|
|
"--github-token",
|
|
help="Deprecated (no-op). Previously: GitHub token for API requests.",
|
|
hidden=True,
|
|
),
|
|
offline: bool = typer.Option(
|
|
False,
|
|
"--offline",
|
|
help="Deprecated (no-op). All scaffolding now uses bundled assets.",
|
|
hidden=True,
|
|
),
|
|
preset: str = typer.Option(
|
|
None,
|
|
"--preset",
|
|
help="Install a preset during initialization (by preset ID)",
|
|
),
|
|
integration: str = typer.Option(
|
|
None,
|
|
"--integration",
|
|
help="AI coding agent integration to use (e.g. --integration copilot). See 'specify check' for available integrations.",
|
|
),
|
|
integration_options: str = typer.Option(
|
|
None,
|
|
"--integration-options",
|
|
help='Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")',
|
|
),
|
|
):
|
|
"""
|
|
Initialize a new Specify project.
|
|
|
|
Project files are scaffolded from assets bundled inside the specify-cli
|
|
package, so initialization does not need network access and templates
|
|
match the installed CLI version.
|
|
|
|
This command will:
|
|
1. Check that required tools are installed
|
|
2. Let you choose your coding agent integration, or default to Copilot
|
|
in non-interactive sessions
|
|
3. Install bundled Spec Kit templates, scripts, workflow, and shared
|
|
project infrastructure
|
|
4. Set up coding agent integration commands and optional presets
|
|
|
|
Examples:
|
|
specify init my-project
|
|
specify init my-project --integration claude
|
|
specify init --ignore-agent-tools my-project
|
|
specify init . --integration claude # Initialize in current directory
|
|
specify init . # Initialize in current directory (interactive integration selection)
|
|
specify init --here --integration claude # Alternative syntax for current directory
|
|
specify init --here --integration codex --integration-options="--skills"
|
|
specify init --here --integration codebuddy
|
|
specify init --here --integration vibe # Initialize with Mistral Vibe support
|
|
specify init --here
|
|
specify init --here --force # Skip confirmation when current directory not empty
|
|
specify init my-project --integration claude # Claude installs skills by default
|
|
specify init --here --integration gemini
|
|
specify init my-project --integration generic --integration-options="--commands-dir .myagent/commands/" # Bring your own agent; requires --commands-dir
|
|
specify init my-project --integration claude --preset healthcare-compliance # With preset
|
|
"""
|
|
# Lazy imports to avoid circular dependency — __init__.py imports this module
|
|
from .. import (
|
|
_install_shared_infra_or_exit,
|
|
_print_cli_warning,
|
|
_update_agent_context_config_file,
|
|
ensure_executable_scripts,
|
|
save_init_options,
|
|
)
|
|
from ..integration_runtime import (
|
|
with_integration_setting as _with_integration_setting,
|
|
)
|
|
from ..integrations._commands import (
|
|
_parse_integration_options,
|
|
_write_integration_json,
|
|
)
|
|
|
|
show_banner()
|
|
|
|
from ..integrations import INTEGRATION_REGISTRY, get_integration
|
|
|
|
if integration:
|
|
resolved_integration = get_integration(integration)
|
|
if not resolved_integration:
|
|
console.print(f"[red]Error:[/red] Unknown integration: '{integration}'")
|
|
available = ", ".join(sorted(INTEGRATION_REGISTRY))
|
|
console.print(f"[yellow]Available integrations:[/yellow] {available}")
|
|
raise typer.Exit(1)
|
|
|
|
if project_name == ".":
|
|
here = True
|
|
project_name = None
|
|
|
|
if here and project_name:
|
|
console.print(
|
|
"[red]Error:[/red] Cannot specify both project name and --here flag"
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
if not here and not project_name:
|
|
console.print(
|
|
"[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag"
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
dir_existed_before = False
|
|
if here:
|
|
project_name = Path.cwd().name
|
|
project_path = Path.cwd()
|
|
dir_existed_before = True
|
|
|
|
existing_items = list(project_path.iterdir())
|
|
if existing_items:
|
|
console.print(
|
|
f"[yellow]Warning:[/yellow] Current directory is not empty ({len(existing_items)} items)"
|
|
)
|
|
console.print(
|
|
"[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]"
|
|
)
|
|
if force:
|
|
console.print(
|
|
"[cyan]--force supplied: skipping confirmation and proceeding with merge[/cyan]"
|
|
)
|
|
else:
|
|
response = typer.confirm("Do you want to continue?")
|
|
if not response:
|
|
console.print("[yellow]Operation cancelled[/yellow]")
|
|
raise typer.Exit(0)
|
|
else:
|
|
project_path = Path(project_name).resolve()
|
|
dir_existed_before = project_path.exists()
|
|
if project_path.exists():
|
|
if not project_path.is_dir():
|
|
console.print(
|
|
f"[red]Error:[/red] '{project_name}' exists but is not a directory."
|
|
)
|
|
raise typer.Exit(1)
|
|
existing_items = list(project_path.iterdir())
|
|
if force:
|
|
if existing_items:
|
|
console.print(
|
|
f"[yellow]Warning:[/yellow] Directory '{project_name}' is not empty ({len(existing_items)} items)"
|
|
)
|
|
console.print(
|
|
"[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]"
|
|
)
|
|
console.print(
|
|
f"[cyan]--force supplied: merging into existing directory '[cyan]{project_name}[/cyan]'[/cyan]"
|
|
)
|
|
else:
|
|
error_panel = Panel(
|
|
f"Directory already exists: '[cyan]{project_name}[/cyan]'\n"
|
|
"Please choose a different project name or remove the existing directory.\n"
|
|
"Use [bold]--force[/bold] to merge into the existing directory.",
|
|
title="[red]Directory Conflict[/red]",
|
|
border_style="red",
|
|
padding=(1, 2),
|
|
)
|
|
console.print()
|
|
console.print(error_panel)
|
|
raise typer.Exit(1)
|
|
|
|
if integration:
|
|
if integration not in AGENT_CONFIG:
|
|
console.print(
|
|
f"[red]Error:[/red] Invalid integration '{integration}'. Choose from: {', '.join(AGENT_CONFIG.keys())}"
|
|
)
|
|
raise typer.Exit(1)
|
|
selected_ai = integration
|
|
elif not _stdin_is_interactive():
|
|
console.print(
|
|
f"[dim]Non-interactive session detected: defaulting to '{DEFAULT_INIT_INTEGRATION}'. "
|
|
"Use --integration to choose a different agent.[/dim]"
|
|
)
|
|
selected_ai = DEFAULT_INIT_INTEGRATION
|
|
else:
|
|
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
|
|
selected_ai = select_with_arrows(
|
|
ai_choices,
|
|
"Choose your coding agent integration:",
|
|
DEFAULT_INIT_INTEGRATION,
|
|
)
|
|
|
|
if not integration:
|
|
resolved_integration = get_integration(selected_ai)
|
|
if not resolved_integration:
|
|
console.print(f"[red]Error:[/red] Unknown agent '{selected_ai}'")
|
|
raise typer.Exit(1)
|
|
|
|
if selected_ai == "generic" and not integration_options:
|
|
console.print(
|
|
"[red]Error:[/red] --integration generic requires --integration-options with --commands-dir"
|
|
)
|
|
console.print(
|
|
'[dim]Example: specify init my-project --integration generic --integration-options="--commands-dir .myagent/commands/"[/dim]'
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
current_dir = Path.cwd()
|
|
|
|
setup_lines = [
|
|
"[cyan]Specify Project Setup[/cyan]",
|
|
"",
|
|
f"{'Project':<15} [green]{project_path.name}[/green]",
|
|
f"{'Working Path':<15} [dim]{current_dir}[/dim]",
|
|
]
|
|
|
|
if not here:
|
|
setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]")
|
|
|
|
console.print(
|
|
Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2))
|
|
)
|
|
|
|
if not ignore_agent_tools:
|
|
agent_config = AGENT_CONFIG.get(selected_ai)
|
|
if agent_config and agent_config["requires_cli"]:
|
|
install_url = agent_config["install_url"]
|
|
if not check_tool(selected_ai):
|
|
error_panel = Panel(
|
|
f"[cyan]{selected_ai}[/cyan] not found\n"
|
|
f"Install from: [cyan]{install_url}[/cyan]\n"
|
|
f"{agent_config['name']} is required to continue with this project type.\n\n"
|
|
"Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check",
|
|
title="[red]Agent Detection Error[/red]",
|
|
border_style="red",
|
|
padding=(1, 2),
|
|
)
|
|
console.print()
|
|
console.print(error_panel)
|
|
raise typer.Exit(1)
|
|
|
|
if script_type:
|
|
if script_type not in SCRIPT_TYPE_CHOICES:
|
|
console.print(
|
|
f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}"
|
|
)
|
|
raise typer.Exit(1)
|
|
selected_script = script_type
|
|
else:
|
|
default_script = "ps" if os.name == "nt" else "sh"
|
|
|
|
if _stdin_is_interactive():
|
|
selected_script = select_with_arrows(
|
|
SCRIPT_TYPE_CHOICES,
|
|
"Choose script type (or press Enter)",
|
|
default_script,
|
|
)
|
|
else:
|
|
selected_script = default_script
|
|
|
|
console.print(f"[cyan]Selected coding agent integration:[/cyan] {selected_ai}")
|
|
console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")
|
|
|
|
tracker = StepTracker("Initialize Specify Project")
|
|
|
|
tracker.add("precheck", "Check required tools")
|
|
tracker.complete("precheck", "ok")
|
|
tracker.add("ai-select", "Select coding agent integration")
|
|
tracker.complete("ai-select", f"{selected_ai}")
|
|
tracker.add("script-select", "Select script type")
|
|
tracker.complete("script-select", selected_script)
|
|
|
|
tracker.add("integration", "Install integration")
|
|
tracker.add("shared-infra", "Install shared infrastructure")
|
|
|
|
for key, label in [
|
|
("chmod", "Ensure scripts executable"),
|
|
("constitution", "Constitution setup"),
|
|
("workflow", "Install bundled workflow"),
|
|
("agent-context", "Install agent-context extension"),
|
|
("final", "Finalize"),
|
|
]:
|
|
tracker.add(key, label)
|
|
|
|
# Disable transient mode on Windows: PowerShell 5.1's legacy console
|
|
# hangs when Rich tries to restore cursor state via VT escape sequences.
|
|
_transient = sys.platform != "win32"
|
|
|
|
with Live(
|
|
tracker.render(), console=console, refresh_per_second=8, transient=_transient
|
|
) as live:
|
|
tracker.attach_refresh(lambda: live.update(tracker.render()))
|
|
try:
|
|
from ..integrations.manifest import IntegrationManifest
|
|
|
|
tracker.start("integration")
|
|
manifest = IntegrationManifest(
|
|
resolved_integration.key,
|
|
project_path,
|
|
version=get_speckit_version(),
|
|
)
|
|
|
|
integration_parsed_options: dict[str, Any] = {}
|
|
if integration_options:
|
|
extra = _parse_integration_options(
|
|
resolved_integration, integration_options
|
|
)
|
|
if extra:
|
|
integration_parsed_options.update(extra)
|
|
|
|
resolved_integration.setup(
|
|
project_path,
|
|
manifest,
|
|
parsed_options=integration_parsed_options or None,
|
|
script_type=selected_script,
|
|
raw_options=integration_options,
|
|
)
|
|
manifest.save()
|
|
|
|
integration_settings = _with_integration_setting(
|
|
{},
|
|
resolved_integration.key,
|
|
resolved_integration,
|
|
script_type=selected_script,
|
|
raw_options=integration_options,
|
|
parsed_options=integration_parsed_options or None,
|
|
)
|
|
_write_integration_json(
|
|
project_path,
|
|
resolved_integration.key,
|
|
[resolved_integration.key],
|
|
integration_settings,
|
|
)
|
|
|
|
tracker.complete(
|
|
"integration",
|
|
resolved_integration.config.get("name", resolved_integration.key),
|
|
)
|
|
|
|
tracker.start("shared-infra")
|
|
_install_shared_infra_or_exit(
|
|
project_path,
|
|
selected_script,
|
|
tracker=tracker,
|
|
force=force,
|
|
invoke_separator=resolved_integration.effective_invoke_separator(
|
|
integration_parsed_options
|
|
),
|
|
)
|
|
tracker.complete(
|
|
"shared-infra", f"scripts ({selected_script}) + templates"
|
|
)
|
|
|
|
ensure_constitution_from_template(project_path, tracker=tracker)
|
|
|
|
try:
|
|
bundled_wf = _locate_bundled_workflow("speckit")
|
|
if bundled_wf:
|
|
from ..workflows.catalog import WorkflowRegistry
|
|
from ..workflows.engine import WorkflowDefinition
|
|
|
|
wf_registry = WorkflowRegistry(project_path)
|
|
if wf_registry.is_installed("speckit"):
|
|
tracker.complete("workflow", "already installed")
|
|
else:
|
|
import shutil as _shutil
|
|
|
|
dest_wf = (
|
|
project_path / ".specify" / "workflows" / "speckit"
|
|
)
|
|
dest_wf.mkdir(parents=True, exist_ok=True)
|
|
_shutil.copy2(
|
|
bundled_wf / "workflow.yml",
|
|
dest_wf / "workflow.yml",
|
|
)
|
|
definition = WorkflowDefinition.from_yaml(
|
|
dest_wf / "workflow.yml"
|
|
)
|
|
wf_registry.add(
|
|
"speckit",
|
|
{
|
|
"name": definition.name,
|
|
"version": definition.version,
|
|
"description": definition.description,
|
|
"source": "bundled",
|
|
},
|
|
)
|
|
tracker.complete("workflow", "speckit installed")
|
|
else:
|
|
tracker.skip("workflow", "bundled workflow not found")
|
|
except Exception as wf_err:
|
|
sanitized_wf = str(wf_err).replace("\n", " ").strip()
|
|
tracker.error("workflow", f"install failed: {sanitized_wf[:120]}")
|
|
|
|
init_opts = {
|
|
"ai": selected_ai,
|
|
"integration": resolved_integration.key,
|
|
"here": here,
|
|
"script": selected_script,
|
|
"feature_numbering": "sequential",
|
|
"speckit_version": get_speckit_version(),
|
|
}
|
|
from ..integrations.base import SkillsIntegration as _SkillsPersist
|
|
|
|
if isinstance(resolved_integration, _SkillsPersist) or getattr(
|
|
resolved_integration, "_skills_mode", False
|
|
):
|
|
init_opts["ai_skills"] = True
|
|
save_init_options(project_path, init_opts)
|
|
|
|
# --- agent-context extension (bundled, auto-installed) ---
|
|
# Installed after init-options.json is written so that skill
|
|
# registration can read ai_skills + integration key.
|
|
try:
|
|
from ..extensions import ExtensionManager as _ExtMgr
|
|
|
|
bundled_ac = _locate_bundled_extension("agent-context")
|
|
if bundled_ac:
|
|
ac_mgr = _ExtMgr(project_path)
|
|
if ac_mgr.registry.is_installed("agent-context"):
|
|
tracker.complete("agent-context", "already installed")
|
|
else:
|
|
ac_mgr.install_from_directory(
|
|
bundled_ac, get_speckit_version()
|
|
)
|
|
tracker.complete("agent-context", "extension installed")
|
|
else:
|
|
from ..extensions import REINSTALL_COMMAND as _ac_reinstall
|
|
|
|
tracker.error(
|
|
"agent-context",
|
|
f"bundled extension not found — installation may be "
|
|
f"incomplete. Run: {_ac_reinstall}",
|
|
)
|
|
except Exception as ac_err:
|
|
sanitized_ac = str(ac_err).replace("\n", " ").strip()
|
|
tracker.error(
|
|
"agent-context",
|
|
f"extension install failed: {sanitized_ac[:120]}",
|
|
)
|
|
|
|
# Write context_file to the agent-context extension config
|
|
# AFTER the extension install (which copies the template config
|
|
# with an empty context_file).
|
|
if resolved_integration.context_file:
|
|
_update_agent_context_config_file(
|
|
project_path,
|
|
resolved_integration.context_file,
|
|
preserve_markers=True,
|
|
)
|
|
|
|
ensure_executable_scripts(project_path, tracker=tracker)
|
|
|
|
if preset:
|
|
try:
|
|
from ..presets import PresetCatalog, PresetError, PresetManager
|
|
|
|
preset_manager = PresetManager(project_path)
|
|
speckit_ver = get_speckit_version()
|
|
|
|
local_path = Path(preset).resolve()
|
|
if local_path.is_dir() and (local_path / "preset.yml").exists():
|
|
preset_manager.install_from_directory(
|
|
local_path, speckit_ver
|
|
)
|
|
else:
|
|
bundled_path = _locate_bundled_preset(preset)
|
|
if bundled_path:
|
|
preset_manager.install_from_directory(
|
|
bundled_path, speckit_ver
|
|
)
|
|
else:
|
|
preset_catalog = PresetCatalog(project_path)
|
|
pack_info = preset_catalog.get_pack_info(preset)
|
|
if not pack_info:
|
|
console.print(
|
|
f"[yellow]Warning:[/yellow] Preset '{preset}' not found in catalog. Skipping."
|
|
)
|
|
elif pack_info.get("bundled") and not pack_info.get(
|
|
"download_url"
|
|
):
|
|
from ..extensions import REINSTALL_COMMAND
|
|
|
|
console.print(
|
|
f"[yellow]Warning:[/yellow] Preset '{preset}' is bundled with spec-kit "
|
|
f"but could not be found in the installed package."
|
|
)
|
|
console.print(
|
|
"This usually means the spec-kit installation is incomplete or corrupted."
|
|
)
|
|
console.print(
|
|
f"Try reinstalling: {REINSTALL_COMMAND}"
|
|
)
|
|
else:
|
|
zip_path = None
|
|
try:
|
|
zip_path = preset_catalog.download_pack(preset)
|
|
preset_manager.install_from_zip(
|
|
zip_path, speckit_ver
|
|
)
|
|
except PresetError as preset_err:
|
|
_print_cli_warning(
|
|
"install",
|
|
"preset",
|
|
preset,
|
|
preset_err,
|
|
continuing="Continuing without the optional preset.",
|
|
)
|
|
finally:
|
|
if zip_path is not None:
|
|
try:
|
|
zip_path.unlink(missing_ok=True)
|
|
except OSError:
|
|
pass
|
|
except Exception as preset_err:
|
|
_print_cli_warning(
|
|
"install",
|
|
"preset",
|
|
preset,
|
|
preset_err,
|
|
continuing="Continuing without the optional preset.",
|
|
)
|
|
|
|
tracker.complete("final", "project ready")
|
|
except (typer.Exit, SystemExit):
|
|
raise
|
|
except Exception as e:
|
|
tracker.error("final", str(e))
|
|
console.print(
|
|
Panel(
|
|
f"Initialization failed: {e}",
|
|
title="Failure",
|
|
border_style="red",
|
|
)
|
|
)
|
|
if debug:
|
|
_env_pairs = [
|
|
("Python", sys.version.split()[0]),
|
|
("Platform", sys.platform),
|
|
("CWD", str(Path.cwd())),
|
|
]
|
|
_label_width = max(len(k) for k, _ in _env_pairs)
|
|
env_lines = [
|
|
f"{k.ljust(_label_width)} → [bright_black]{v}[/bright_black]"
|
|
for k, v in _env_pairs
|
|
]
|
|
console.print(
|
|
Panel(
|
|
"\n".join(env_lines),
|
|
title="Debug Environment",
|
|
border_style="magenta",
|
|
)
|
|
)
|
|
if not here and project_path.exists() and not dir_existed_before:
|
|
shutil.rmtree(project_path)
|
|
raise typer.Exit(1)
|
|
finally:
|
|
pass
|
|
|
|
if _transient:
|
|
console.print(tracker.render())
|
|
console.print("\n[bold green]Project ready.[/bold green]")
|
|
|
|
agent_config = AGENT_CONFIG.get(selected_ai)
|
|
if agent_config:
|
|
agent_folder = agent_config["folder"] or integration_parsed_options.get(
|
|
"commands_dir"
|
|
)
|
|
if agent_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="[yellow]Agent Folder Security[/yellow]",
|
|
border_style="yellow",
|
|
padding=(1, 2),
|
|
)
|
|
console.print()
|
|
console.print(security_notice)
|
|
|
|
steps_lines = []
|
|
if not here:
|
|
steps_lines.append(
|
|
f"1. Go to the project folder: [cyan]cd {project_name}[/cyan]"
|
|
)
|
|
step_num = 2
|
|
else:
|
|
steps_lines.append("1. You're already in the project directory!")
|
|
step_num = 2
|
|
|
|
from ..integrations.base import SkillsIntegration as _SkillsInt
|
|
|
|
_is_skills_integration = isinstance(
|
|
resolved_integration, _SkillsInt
|
|
) or getattr(resolved_integration, "_skills_mode", False)
|
|
|
|
codex_skill_mode = selected_ai == "codex" and _is_skills_integration
|
|
claude_skill_mode = selected_ai == "claude" and _is_skills_integration
|
|
kimi_skill_mode = selected_ai == "kimi"
|
|
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
|
|
trae_skill_mode = selected_ai == "trae"
|
|
cursor_agent_skill_mode = (
|
|
selected_ai == "cursor-agent" and _is_skills_integration
|
|
)
|
|
copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration
|
|
devin_skill_mode = selected_ai == "devin"
|
|
zed_skill_mode = selected_ai == "zed" and _is_skills_integration
|
|
cline_skill_mode = selected_ai == "cline"
|
|
native_skill_mode = (
|
|
codex_skill_mode
|
|
or claude_skill_mode
|
|
or kimi_skill_mode
|
|
or agy_skill_mode
|
|
or trae_skill_mode
|
|
or cursor_agent_skill_mode
|
|
or copilot_skill_mode
|
|
or devin_skill_mode
|
|
or zed_skill_mode
|
|
)
|
|
|
|
if codex_skill_mode:
|
|
steps_lines.append(
|
|
f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]"
|
|
)
|
|
step_num += 1
|
|
if claude_skill_mode:
|
|
steps_lines.append(
|
|
f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]"
|
|
)
|
|
step_num += 1
|
|
if cursor_agent_skill_mode:
|
|
steps_lines.append(
|
|
f"{step_num}. Start Cursor Agent in this project directory; spec-kit skills were installed to [cyan].cursor/skills[/cyan]"
|
|
)
|
|
step_num += 1
|
|
if devin_skill_mode:
|
|
steps_lines.append(
|
|
f"{step_num}. Start Devin in this project directory; spec-kit skills were installed to [cyan].devin/skills[/cyan]"
|
|
)
|
|
step_num += 1
|
|
if zed_skill_mode:
|
|
steps_lines.append(
|
|
f"{step_num}. Start Zed in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]"
|
|
)
|
|
step_num += 1
|
|
usage_label = "skills" if native_skill_mode else "slash commands"
|
|
|
|
from .._invocation_style import is_slash_skills_agent as _is_slash_skills_agent
|
|
|
|
# `_is_skills_integration` means the integration is installed in
|
|
# skills mode, which is the semantic equivalent of `ai_skills_enabled`
|
|
# used by `is_slash_skills_agent()`.
|
|
_ai_skills_enabled = _is_skills_integration
|
|
|
|
def _display_cmd(name: str) -> str:
|
|
if codex_skill_mode:
|
|
return f"$speckit-{name}"
|
|
if kimi_skill_mode:
|
|
return f"/skill:speckit-{name}"
|
|
if (
|
|
_is_slash_skills_agent(selected_ai, _ai_skills_enabled)
|
|
or cline_skill_mode
|
|
):
|
|
return f"/speckit-{name}"
|
|
return f"/speckit.{name}"
|
|
|
|
steps_lines.append(
|
|
f"{step_num}. Start using {usage_label} with your coding agent:"
|
|
)
|
|
|
|
steps_lines.append(
|
|
f" {step_num}.1 [cyan]{_display_cmd('constitution')}[/] - Establish project principles"
|
|
)
|
|
steps_lines.append(
|
|
f" {step_num}.2 [cyan]{_display_cmd('specify')}[/] - Create baseline specification"
|
|
)
|
|
steps_lines.append(
|
|
f" {step_num}.3 [cyan]{_display_cmd('plan')}[/] - Create implementation plan"
|
|
)
|
|
steps_lines.append(
|
|
f" {step_num}.4 [cyan]{_display_cmd('tasks')}[/] - Generate actionable tasks"
|
|
)
|
|
steps_lines.append(
|
|
f" {step_num}.5 [cyan]{_display_cmd('implement')}[/] - Execute implementation"
|
|
)
|
|
steps_lines.append(
|
|
f" {step_num}.6 [cyan]{_display_cmd('converge')}[/] - Assess the codebase and append remaining work as tasks"
|
|
)
|
|
|
|
steps_panel = Panel(
|
|
"\n".join(steps_lines),
|
|
title="Next Steps",
|
|
border_style="cyan",
|
|
padding=(1, 2),
|
|
)
|
|
console.print()
|
|
console.print(steps_panel)
|
|
|
|
enhancement_intro = (
|
|
"Optional skills that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]"
|
|
if native_skill_mode
|
|
else "Optional commands that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]"
|
|
)
|
|
enhancement_lines = [
|
|
enhancement_intro,
|
|
"",
|
|
f"○ [cyan]{_display_cmd('clarify')}[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]{_display_cmd('plan')}[/] if used)",
|
|
f"○ [cyan]{_display_cmd('analyze')}[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]{_display_cmd('tasks')}[/], before [cyan]{_display_cmd('implement')}[/])",
|
|
f"○ [cyan]{_display_cmd('checklist')}[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]{_display_cmd('plan')}[/])",
|
|
]
|
|
enhancements_title = (
|
|
"Enhancement Skills" if native_skill_mode else "Enhancement Commands"
|
|
)
|
|
enhancements_panel = Panel(
|
|
"\n".join(enhancement_lines),
|
|
title=enhancements_title,
|
|
border_style="cyan",
|
|
padding=(1, 2),
|
|
)
|
|
console.print()
|
|
console.print(enhancements_panel)
|