Files
github-spec-kit/src/specify_cli/commands/init.py
Manfred Riem 53d9543355 feat: make agent-context extension a full opt-in (#3097)
* docs: add Spec Kit spec for agent-context full opt-in

Use Spec Kit's own specify workflow to author the spec that makes the
agent-context extension a full opt-in, removing all agent-context
configuration/support from the Python codebase and removing the
deprecation message. Force-added despite specs/ being gitignored; the
generated artifact will be purged prior to merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: add Spec Kit plan artifacts for agent-context full opt-in

Phase 0/1 of the SDD plan workflow: plan.md, research.md, data-model.md,
quickstart.md, and contracts/cli-behavior.md. Constitution Check is a
documented no-op (repo has no ratified constitution). Force-added despite
specs/ being gitignored; generated artifacts will be purged prior to merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: correct Constitution Check against ratified v1.0.0

Earlier draft wrongly treated the gate as a no-op; the fork's main is 16
commits behind upstream/main, which carries .specify/memory/constitution.md.
Re-evaluate the feature against Principles I-V (all PASS) and note that
Principle I mandates keeping context_file as a declared class attribute,
validating the R1 metadata decision.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: refresh plan artifacts against synced upstream/main

After syncing fork main to upstream and rebasing, re-scan the current
agent-context surface. Upstream generalized the single context_file into a
plural context_files concept with new resolver helpers
(_resolve_context_files, _resolve_context_file_values,
_format_context_file_values) and upsert/remove now loop over multiple
files. Update research.md, data-model.md, contracts, quickstart grep
guards, and the plan summary to cover the expanded removal scope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: add Spec Kit tasks for agent-context full opt-in

Phase 2 of SDD: dependency-ordered tasks.md (30 tasks) organized by the
three user stories, with mandatory test tasks (Constitution Principle II)
and a foundational phase decoupling __CONTEXT_FILE__ resolution from the
extension config. Includes the extension self-seeding task (T015) and a
static guard test (T002) enforcing zero agent-context references in the CLI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat!: remove agent-context lifecycle from the Specify CLI

Make the agent-context extension a full opt-in. The CLI no longer
installs the extension during init, writes agent-context-config.yml,
or creates/updates/removes the managed Spec Kit section in agent
context files. Context-section upsert/remove, marker resolution,
extension-enabled gating, the config helpers, and the obsolete inline
deprecation warning are all removed. Integration context_file stays as
inert metadata; __CONTEXT_FILE__ now resolves from registry metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(agent-context): self-seed context file from the active integration

When agent-context-config.yml has no context_file/context_files, the
bundled bash and PowerShell update scripts now resolve the context file
from the active integration in .specify/init-options.json via the
integration registry, so the extension no longer depends on the CLI
writing its config.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test+docs: update suite and docs for agent-context opt-in

Update integration/extension tests to expect no agent-context install,
config, or context-section writes during init. Add a static guard test
(test_agent_context_cli_free.py) asserting the CLI source is free of
agent-context lifecycle symbols, plus backward-compatibility tests for
legacy projects. Refresh AGENTS.md, the extension README, and add a
CHANGELOG entry describing the opt-in behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(agent-context): warn on self-seed failure, correct docs, speed up guard test

Address PR review feedback:
- Self-seed scripts (bash + PowerShell) now emit an actionable warning when
  an active integration is configured but specify_cli cannot be imported by
  the chosen Python (e.g. pipx installs), or when the integration declares no
  context file, instead of silently falling through to 'nothing to do'.
- Correct the extension README disable note: command rendering never reads the
  extension config; __CONTEXT_FILE__ is always substituted from integration
  metadata, so a stale context_files value cannot affect rendering.
- Cache CLI source reads in the static guard test via a module-scoped fixture
  so the directory walk happens once instead of once per forbidden symbol.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(agent-context): ship self-owned per-agent context-file defaults

The extension now bundles agent-context-defaults.json (key→context_file
map) and self-seeds from it, dropping any dependency on the Specify CLI
registry. Both the bash and PowerShell update scripts read the bundled
JSON map keyed by the active integration from init-options.json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat!: remove all agent-context state from the Specify CLI

Strip every context_file reference from the CLI: the field on all 35
integration classes, the IntegrationBase plumbing (process_template
param/step, _context_file_display, docstrings), the __CONTEXT_FILE__
resolution in agents.py, the legacy context_file/context_markers
popping in _helpers.py, and the context_file template in
integration_scaffold.py. Also drop the Agent context update step and
__CONTEXT_FILE__ placeholder from templates/commands/plan.md.

The agent-context extension now solely owns all context-file knowledge,
including the per-agent default mapping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: drop context_file coverage and guard against CLI reintroduction

Remove CONTEXT_FILE attrs and context_file assertions across the base
mixins, all 35 per-integration test files, shared integration tests, and
conftest stubs. Rewrite the base-mixin context tests to assert no managed
section is written and no __CONTEXT_FILE__ placeholder survives. Extend
the CLI-free static guard to forbid context_file, __CONTEXT_FILE__, and
_context_file_display in src/specify_cli, and have the extension tests
copy the bundled defaults JSON so self-seed runs without the CLI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: reflect full removal of agent-context state from the CLI

Update AGENTS.md (integration examples, required-fields table, context
behavior section, pitfalls), CHANGELOG, and the SDD spec artifacts
(FR-007, SC-002, data-model) to state that the CLI carries no
context_file and the extension fully owns the per-agent default mapping
via agent-context-defaults.json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: align SDD artifacts with full context_file removal

Update research.md (R1, R2, R4, summary table), contracts/cli-behavior.md
(C3, C5), tasks.md (Phase 2, T026, notes), plan.md (Principle I, source
map), and checklists/requirements.md so the spec artifacts reflect the
implemented decision: the CLI carries no context_file attribute or
__CONTEXT_FILE__ resolution, and the per-agent defaults map lives in the
extension. Resolves PR review #4548130110.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: scrub stale context-file mentions from CLI docstrings

Update the multi_install_safe docstring (drop the removed "context file"
invariant), the RovoDev setup docstring (no longer upserts a context
section), the Copilot module docstring (drop the context-file line), and
tighten the _update_init_options_for_integration note. Pure docstring
changes — no behavioral impact. Resolves PR review #4548237085.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test+docs: harden agent-context test helper and fix stale docs

- base.py: document multi_install_safe as an optional subclass attribute
  in the IntegrationBase docstring.
- test_cli.py: clarify the init-options assertion is guarding against
  leftover legacy agent-context keys, not relocation.
- test_extension_agent_context.py: _install_agent_context_config now
  asserts the bundled agent-context-defaults.json exists and always
  copies it, so self-seeding tests fail loudly instead of silently
  skipping when the map is missing.
- test_integration_cursor_agent.py: drop Path/IntegrationManifest imports
  left unused after removing the context-section frontmatter tests.

Resolves PR review #4548293116.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove gitignored SDD artifacts from specs/

The specs/001-agent-context-full-optin/ artifacts were force-added for
dogfooding visibility, but specs/ is gitignored and these were always
intended to be purged before merge. Remove them so merging does not add
an intentionally-untracked directory to repo history.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: keep CHANGELOG.md identical to upstream

CHANGELOG.md is auto-generated at release time, so the branch should not
carry a manual entry. Restore it to match upstream/main exactly.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: preserve Cursor .mdc frontmatter in agent-context updater scripts

The bundled agent-context updater scripts wrote the managed section as
plain text. For Cursor-style `.mdc` targets this dropped the required
`---\nalwaysApply: true\n---` frontmatter, reintroducing the rule-loading
bug originally fixed in #1699. Port the `_ensure_mdc_frontmatter` logic
into both the bash and PowerShell updaters: prepend frontmatter when
missing, repair `alwaysApply` when set to the wrong value, and leave
non-`.mdc` targets untouched. Add regression tests covering both shells.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: scope CLI-free guard to agent-context-specific symbols

Drop the bare "context_file" substring from FORBIDDEN_SYMBOLS so the
guard no longer fails on unrelated future CLI fields named context_file.
The list still covers agent-context-specific identifiers (__CONTEXT_FILE__,
_context_file_display, _resolve_context_files, _resolve_context_file_values).

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: harden agent-context bash self-seed against malformed init JSON

Two robustness fixes in the embedded Python self-seed logic:
- Coerce the integration value from init-options.json to a string only when
  it is actually a string; otherwise treat it as unset so a corrupted
  dict/list value degrades to the existing nothing-to-do behavior instead of
  breaking the agents-map lookup.
- Normalize agent-context-defaults.json: only use 'agents' when both the JSON
  root and the 'agents' value are dicts, so a wrong-shaped (but valid) JSON
  falls back to the warning path instead of raising on .get.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: correct PowerShell hyphenated key lookup and regex replace count

- Self-seed now reads the defaults mapping via
  $defaults.agents.PSObject.Properties[$integrationKey].Value instead of
  member access ($defaults.agents.$integrationKey), which parsed hyphenated
  keys like 'cursor-agent'/'kiro-cli' as subtraction and failed to resolve.
- Replace the static [regex]::Replace(..., 1) call, whose trailing 1 was
  interpreted as RegexOptions.IgnoreCase rather than a replacement count, with
  an instance Regex whose Replace(input, replacement, 1) limits to the first
  match as intended.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: make bash .mdc frontmatter guard case-insensitive

The bash updater only injected Cursor .mdc frontmatter when ctx_path ended
in lowercase '.mdc', so a mixed/upper-case extension (e.g. specify-rules.MDC)
was skipped and Cursor would not auto-load the rule file. Compare against the
casefolded path. The PowerShell variant already uses -match, which is
case-insensitive by default, so no change is needed there.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: document separator-agnostic agent-context update invocation

The README hard-coded the dot-notation slash command
(/speckit.agent-context.update), which hyphen-separator agents like Forge and
Cline do not recognize. Document the canonical command ID plus both slash
invocations so users copy the form their agent accepts.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-29 15:27:26 -05:00

786 lines
33 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_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,
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"),
("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)
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
zcode_skill_mode = selected_ai == "zcode" 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 zcode_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 zcode_skill_mode:
steps_lines.append(
f"{step_num}. Start ZCode in this project directory; spec-kit skills were installed to [cyan].zcode/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_dollar_skills_agent as _is_dollar_skills_agent,
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 _is_dollar_skills_agent(selected_ai, _ai_skills_enabled):
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)