mirror of
https://github.com/github/spec-kit.git
synced 2026-07-07 06:35:06 +08:00
* Stage 5: Skills, Generic & Option-Driven Integrations (#1924) Add SkillsIntegration base class and migrate codex, kimi, agy, and generic to the integration system. Integrations: - SkillsIntegration(IntegrationBase) in base.py — creates speckit-<name>/SKILL.md layout matching release ZIP output byte-for-byte - CodexIntegration — .agents/skills/, --skills default=True - KimiIntegration — .kimi/skills/, --skills + --migrate-legacy options, dotted→hyphenated skill directory migration - AgyIntegration — .agent/skills/, skills-only (commands deprecated v1.20.5) - GenericIntegration — user-specified --commands-dir, MarkdownIntegration - All four have update-context.sh/.ps1 scripts - All four registered in INTEGRATION_REGISTRY CLI changes: - --ai <agent> auto-promotes to integration path for all registered agents - Interactive agent selection also auto-promotes (bug fix) - --ai-skills and --ai-commands-dir show deprecation notices on integration path - Next-steps display shows correct skill invocation syntax for skills integrations - agy added to CommandRegistrar.AGENT_CONFIGS Tests: - test_integration_base_skills.py — reusable mixin with setup, frontmatter, directory structure, scripts, CLI auto-promote, and complete file inventory (sh+ps) tests - Per-agent test files: test_integration_{codex,kimi,agy,generic}.py - Kimi legacy migration tests, generic --commands-dir validation - Registry updated with Stage 5 keys - Removed 9 dead-mock tests, moved 4 integration tests to proper locations - Fixed all bare project-name tests to use tmp_path - Fixed 6 pre-existing ANSI escape code test failures in test_extensions.py and test_presets.py 1524 tests pass, 0 failures. * fix: remove unused variable flagged by ruff (F841) * fix: address PR review — integration-type-aware deprecation messages and early generic validation - --ai-skills deprecation message now distinguishes SkillsIntegration ("skills are the default") from command-based integrations ("has no effect") - --ai-commands-dir validation for generic runs even when auto-promoted, giving clear CLI error instead of late ValueError from setup() - Resolves review comments from #2052 * fix: address PR review round 2 - Remove unused SKILL_DESCRIPTIONS dict from base.py (dead code after switching to template descriptions for ZIP parity) - Narrow YAML parse catch from Exception to yaml.YAMLError - Remove unused shutil import from test_integration_kimi.py - Remove unused _REGISTRAR_EXEMPT class attr from test_registry.py - Reword --ai-commands-dir deprecation to be actionable - Update generic validation error to mention both --ai and --integration * fix: address PR review round 3 - Clarify parsed_options forwarding is intentional (all options passed, integrations decide what to use) - Extract _strip_ansi() helper in test_extensions.py and test_presets.py - Remove unused pytest import (test_cli.py), unused locals (test_integration_base_skills.py) - Reword --ai-commands-dir deprecation to be actionable without referencing the not-yet-implemented --integration-options * fix: address PR review round 4 - Reorder kimi migration: run super().setup() first so hyphenated targets exist, then migrate dotted dirs (prevents user content loss) - Move _strip_ansi() to shared tests/conftest.py, import from there in test_extensions.py, test_presets.py, test_ai_skills.py - Remove now-unused re imports from all three test files * fix: address PR review round 5 - Use write_bytes() for LF-only newlines (no CRLF on Windows) - Add --integration-options CLI parameter — raw string passed through to the integration via opts['raw_options']; the integration owns parsing of its own options - GenericIntegration.setup() reads --commands-dir from raw_options when not in parsed_options (supports --integration-options="...") - Skip early --ai-commands-dir validation when --integration-options is provided (integration validates in its own setup()) - Remove parse_integration_options from core — integrations parse their own options * fix: address PR review round 6 - GenericIntegration is now stateless: removed self._commands_dir instance state, overrides setup() directly to compute destination from parsed_options/raw_options on the stack - commands_dest() raises by design (stateless singleton) - _quote() in SkillsIntegration now escapes backslashes and double quotes to produce valid YAML even with special characters * fix: address PR review round 7 - Support --commands-dir=value form in raw_options parsing (not just --commands-dir value with space separator) - Normalize CRLF to LF in write_file_and_record() before encoding - Persist ai_skills=True in init-options.json when using a SkillsIntegration, so extensions/presets emit SKILL.md overrides correctly even without explicit --ai-skills flag
This commit is contained in:
@@ -1907,6 +1907,7 @@ def init(
|
||||
preset: str = typer.Option(None, "--preset", help="Install a preset during initialization (by preset ID)"),
|
||||
branch_numbering: str = typer.Option(None, "--branch-numbering", help="Branch numbering strategy: 'sequential' (001, 002, ...) or 'timestamp' (YYYYMMDD-HHMMSS)"),
|
||||
integration: str = typer.Option(None, "--integration", help="Use the new integration system (e.g. --integration copilot). Mutually exclusive with --ai."),
|
||||
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.
|
||||
@@ -1997,6 +1998,26 @@ def init(
|
||||
f"--ai {ai_assistant}. The --ai flag will be deprecated in a future release.[/dim]"
|
||||
)
|
||||
|
||||
# Deprecation warnings for --ai-skills and --ai-commands-dir when using integration path
|
||||
if use_integration:
|
||||
if ai_skills:
|
||||
from .integrations.base import SkillsIntegration as _SkillsCheck
|
||||
if isinstance(resolved_integration, _SkillsCheck):
|
||||
console.print(
|
||||
"[dim]Note: --ai-skills is not needed with --integration; "
|
||||
"skills are the default for this integration.[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
"[dim]Note: --ai-skills has no effect with --integration "
|
||||
f"{resolved_integration.key}; this integration uses commands, not skills.[/dim]"
|
||||
)
|
||||
if ai_commands_dir and resolved_integration.key != "generic":
|
||||
console.print(
|
||||
"[dim]Note: --ai-commands-dir is deprecated; "
|
||||
'use [bold]--integration generic --integration-options="--commands-dir <dir>"[/bold] instead.[/dim]'
|
||||
)
|
||||
|
||||
if project_name == ".":
|
||||
here = True
|
||||
project_name = None # Clear project_name to use existing validation logic
|
||||
@@ -2062,8 +2083,18 @@ def init(
|
||||
"copilot"
|
||||
)
|
||||
|
||||
# Auto-promote interactively selected agents to the integration path
|
||||
# when a matching integration is registered (same behavior as --ai).
|
||||
if not use_integration:
|
||||
from .integrations import get_integration as _get_int
|
||||
_resolved = _get_int(selected_ai)
|
||||
if _resolved:
|
||||
use_integration = True
|
||||
resolved_integration = _resolved
|
||||
|
||||
# Agents that have moved from explicit commands/prompts to agent skills.
|
||||
if selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
|
||||
# Skip this check when using the integration path — skills are the default.
|
||||
if not use_integration and selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
|
||||
# If selected interactively (no --ai provided), automatically enable
|
||||
# ai_skills so the agent remains usable without requiring an extra flag.
|
||||
# Preserve fail-fast behavior only for explicit '--ai <agent>' without skills.
|
||||
@@ -2073,14 +2104,20 @@ def init(
|
||||
ai_skills = True
|
||||
console.print(f"\n[yellow]Note:[/yellow] {AGENT_SKILLS_MIGRATIONS[selected_ai]['interactive_note']}")
|
||||
|
||||
# Validate --ai-commands-dir usage
|
||||
if selected_ai == "generic":
|
||||
# Validate --ai-commands-dir usage.
|
||||
# Skip validation when --integration-options is provided — the integration
|
||||
# will validate its own options in setup().
|
||||
if selected_ai == "generic" and not integration_options:
|
||||
if not ai_commands_dir:
|
||||
console.print("[red]Error:[/red] --ai-commands-dir is required when using --ai generic")
|
||||
console.print("[dim]Example: specify init my-project --ai generic --ai-commands-dir .myagent/commands/[/dim]")
|
||||
console.print("[red]Error:[/red] --ai-commands-dir is required when using --ai generic or --integration generic")
|
||||
console.print("[dim]Example: specify init my-project --integration generic --integration-options=\"--commands-dir .myagent/commands/\"[/dim]")
|
||||
raise typer.Exit(1)
|
||||
elif ai_commands_dir:
|
||||
console.print(f"[red]Error:[/red] --ai-commands-dir can only be used with --ai generic (not '{selected_ai}')")
|
||||
elif ai_commands_dir and not use_integration:
|
||||
console.print(
|
||||
f"[red]Error:[/red] --ai-commands-dir can only be used with the "
|
||||
f"'generic' integration via --ai generic or --integration generic "
|
||||
f"(not '{selected_ai}')"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
current_dir = Path.cwd()
|
||||
@@ -2210,9 +2247,21 @@ def init(
|
||||
manifest = IntegrationManifest(
|
||||
resolved_integration.key, project_path, version=get_speckit_version()
|
||||
)
|
||||
|
||||
# Forward all legacy CLI flags to the integration as parsed_options.
|
||||
# Integrations receive every option and decide what to use;
|
||||
# irrelevant keys are simply ignored by the integration's setup().
|
||||
integration_parsed_options: dict[str, Any] = {}
|
||||
if ai_commands_dir:
|
||||
integration_parsed_options["commands_dir"] = ai_commands_dir
|
||||
if ai_skills:
|
||||
integration_parsed_options["skills"] = True
|
||||
|
||||
resolved_integration.setup(
|
||||
project_path, manifest,
|
||||
parsed_options=integration_parsed_options or None,
|
||||
script_type=selected_script,
|
||||
raw_options=integration_options,
|
||||
)
|
||||
manifest.save()
|
||||
|
||||
@@ -2268,7 +2317,7 @@ def init(
|
||||
shutil.rmtree(project_path)
|
||||
raise typer.Exit(1)
|
||||
# For generic agent, rename placeholder directory to user-specified path
|
||||
if selected_ai == "generic" and ai_commands_dir:
|
||||
if not use_integration and selected_ai == "generic" and ai_commands_dir:
|
||||
placeholder_dir = project_path / ".speckit" / "commands"
|
||||
target_dir = project_path / ai_commands_dir
|
||||
if placeholder_dir.is_dir():
|
||||
@@ -2284,10 +2333,11 @@ def init(
|
||||
ensure_constitution_from_template(project_path, tracker=tracker)
|
||||
|
||||
# Determine skills directory and migrate any legacy Kimi dotted skills.
|
||||
# (Legacy path only — integration path handles skills in setup().)
|
||||
migrated_legacy_kimi_skills = 0
|
||||
removed_legacy_kimi_skills = 0
|
||||
skills_dir: Optional[Path] = None
|
||||
if selected_ai in NATIVE_SKILLS_AGENTS:
|
||||
if not use_integration and selected_ai in NATIVE_SKILLS_AGENTS:
|
||||
skills_dir = _get_skills_dir(project_path, selected_ai)
|
||||
if selected_ai == "kimi" and skills_dir.is_dir():
|
||||
(
|
||||
@@ -2295,7 +2345,7 @@ def init(
|
||||
removed_legacy_kimi_skills,
|
||||
) = _migrate_legacy_kimi_dotted_skills(skills_dir)
|
||||
|
||||
if ai_skills:
|
||||
if not use_integration and ai_skills:
|
||||
if selected_ai in NATIVE_SKILLS_AGENTS:
|
||||
bundled_found = _has_bundled_skills(project_path, selected_ai)
|
||||
if bundled_found:
|
||||
@@ -2383,6 +2433,11 @@ def init(
|
||||
}
|
||||
if use_integration:
|
||||
init_opts["integration"] = resolved_integration.key
|
||||
# Ensure ai_skills is set for SkillsIntegration so downstream
|
||||
# tools (extensions, presets) emit SKILL.md overrides correctly.
|
||||
from .integrations.base import SkillsIntegration as _SkillsPersist
|
||||
if isinstance(resolved_integration, _SkillsPersist):
|
||||
init_opts["ai_skills"] = True
|
||||
save_init_options(project_path, init_opts)
|
||||
|
||||
# Install preset if specified
|
||||
@@ -2484,17 +2539,27 @@ def init(
|
||||
steps_lines.append("1. You're already in the project directory!")
|
||||
step_num = 2
|
||||
|
||||
if selected_ai == "codex" and ai_skills:
|
||||
# Determine skill display mode for the next-steps panel.
|
||||
# Skills integrations (codex, kimi, agy) should show skill invocation syntax
|
||||
# regardless of whether --ai-skills was explicitly passed.
|
||||
_is_skills_integration = False
|
||||
if use_integration:
|
||||
from .integrations.base import SkillsIntegration as _SkillsInt
|
||||
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
|
||||
|
||||
codex_skill_mode = selected_ai == "codex" and (ai_skills or _is_skills_integration)
|
||||
kimi_skill_mode = selected_ai == "kimi"
|
||||
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
|
||||
native_skill_mode = codex_skill_mode or kimi_skill_mode or agy_skill_mode
|
||||
|
||||
if codex_skill_mode and not ai_skills:
|
||||
# Integration path installed skills; show the helpful notice
|
||||
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
|
||||
|
||||
codex_skill_mode = selected_ai == "codex" and ai_skills
|
||||
kimi_skill_mode = selected_ai == "kimi"
|
||||
native_skill_mode = codex_skill_mode or kimi_skill_mode
|
||||
usage_label = "skills" if native_skill_mode else "slash commands"
|
||||
|
||||
def _display_cmd(name: str) -> str:
|
||||
if codex_skill_mode:
|
||||
if codex_skill_mode or agy_skill_mode:
|
||||
return f"$speckit-{name}"
|
||||
if kimi_skill_mode:
|
||||
return f"/skill:speckit-{name}"
|
||||
|
||||
Reference in New Issue
Block a user