mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
Stage 3: Standard markdown integrations — 19 agents migrated to plugin architecture (#2038)
* Stage 3: Standard markdown integrations — 19 agents migrated to plugin architecture
Migrate all standard markdown integrations to self-contained subpackages
under integrations/. Each subclasses MarkdownIntegration with config-only
overrides (~10 lines per __init__.py).
Integrations migrated (19):
claude, qwen, opencode, junie, kilocode, auggie, roo, codebuddy,
qodercli, amp, shai, bob, trae, pi, iflow, kiro-cli, windsurf,
vibe, cursor-agent
Changes:
- Create integrations/<key>/ subpackage with __init__.py and scripts/
(update-context.sh, update-context.ps1) for each integration
- Register all 19 in INTEGRATION_REGISTRY (20 total with copilot)
- MarkdownIntegration.setup() processes templates (replaces {SCRIPT},
{ARGS}, __AGENT__; strips frontmatter blocks; rewrites paths)
- Extract install_scripts() to IntegrationBase; refactor copilot to use it
- Generalize --ai auto-promote from copilot-only to registry-driven:
any integration registered in INTEGRATION_REGISTRY auto-promotes.
Unregistered agents (gemini, tabnine, codex, kimi, agy, generic)
continue through the legacy --ai path unchanged.
- Fix cursor/cursor-agent key mismatch in CommandRegistrar.AGENT_CONFIGS
- Add missing vibe entry to CommandRegistrar.AGENT_CONFIGS
- Update kiro alias test to reflect auto-promote behavior
Testing:
- Per-agent test files (test_integration_<agent>.py) with shared mixin
- 1316 tests passing, 0 failures
- Complete file inventory tests for both sh and ps variants
- Byte-for-byte validated against v0.4.3 release packages (684 files)
* Address PR review: fix repo root detection and no-op test
- Fix repo root fallback in all 20 update-context.sh scripts: walk up
from script location to find .specify/ instead of falling back to pwd
- Fix repo root fallback in all 20 update-context.ps1 scripts: walk up
from script location to find .specify/ instead of falling back to $PWD
- Add assertions to test_setup_writes_to_correct_directory: verify
expected_dir exists and all command files reside under it
* Fix REPO_ROOT priority: prefer .specify walk-up over git root
In monorepos the git toplevel may differ from the project root that
contains .specify/. The previous fix still preferred git rev-parse
over the walk-up result.
Bash scripts (20): prefer the discovered _root when it contains
.specify/; only accept git root if it also contains .specify/.
PowerShell scripts (20): validate git root contains .specify/ before
using it; fall back to walking up from script directory otherwise.
* Guard git call with try/catch in PowerShell scripts
With $ErrorActionPreference = 'Stop', an unguarded git rev-parse
throws a terminating CommandNotFoundException when git is not
installed, preventing the .specify walk-up fallback from running.
Wrap the git call in try/catch across all 20 update-context.ps1
scripts so the fallback works reliably without git.
* Rename hyphenated package dirs to valid Python identifiers
Rename kiro-cli → kiro_cli and cursor-agent → cursor_agent so the
packages can be imported with normal Python syntax instead of
importlib. The user-facing integration key (IntegrationBase.key)
stays hyphenated to match the actual CLI tool / binary name.
Also reorganize _register_builtins(): imports and registrations
are now grouped alphabetically with clear section comments.
* Reuse CommandRegistrar path rewriting in process_template()
Replace the duplicated regex-based path rewriting in
MarkdownIntegration.process_template() with a call to the shared
CommandRegistrar._rewrite_project_relative_paths() implementation.
This ensures extension-local paths are preserved and boundary rules
stay consistent across the codebase.
* Promote _rewrite_project_relative_paths to public API
Rename CommandRegistrar._rewrite_project_relative_paths() to
rewrite_project_relative_paths() (drop leading underscore) so
integrations can call it without reaching into a private method
across subsystem boundaries.
Addresses PR review feedback:
https://github.com/github/spec-kit/pull/2038#discussion_r3022105627
* Broaden TestRegistrarKeyAlignment to cover all integration keys
Parametrize across ALL_INTEGRATION_KEYS instead of only checking
cursor-agent and vibe. Keeps a separate negative test for the
stale 'cursor' shorthand.
Addresses PR review feedback:
https://github.com/github/spec-kit/pull/2038#discussion_r3022269032
This commit is contained in:
@@ -206,6 +206,53 @@ class IntegrationBase(ABC):
|
||||
manifest.record_existing(rel)
|
||||
return dest
|
||||
|
||||
def integration_scripts_dir(self) -> Path | None:
|
||||
"""Return path to this integration's bundled ``scripts/`` directory.
|
||||
|
||||
Looks for a ``scripts/`` sibling of the module that defines the
|
||||
concrete subclass (not ``IntegrationBase`` itself).
|
||||
Returns ``None`` if the directory doesn't exist.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
cls_file = inspect.getfile(type(self))
|
||||
scripts = Path(cls_file).resolve().parent / "scripts"
|
||||
return scripts if scripts.is_dir() else None
|
||||
|
||||
def install_scripts(
|
||||
self,
|
||||
project_root: Path,
|
||||
manifest: IntegrationManifest,
|
||||
) -> list[Path]:
|
||||
"""Copy integration-specific scripts into the project.
|
||||
|
||||
Copies files from this integration's ``scripts/`` directory to
|
||||
``.specify/integrations/<key>/scripts/`` in the project. Shell
|
||||
scripts are made executable. All copied files are recorded in
|
||||
*manifest*.
|
||||
|
||||
Returns the list of files created.
|
||||
"""
|
||||
scripts_src = self.integration_scripts_dir()
|
||||
if not scripts_src:
|
||||
return []
|
||||
|
||||
created: list[Path] = []
|
||||
scripts_dest = project_root / ".specify" / "integrations" / self.key / "scripts"
|
||||
scripts_dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for src_script in sorted(scripts_src.iterdir()):
|
||||
if not src_script.is_file():
|
||||
continue
|
||||
dst_script = scripts_dest / src_script.name
|
||||
shutil.copy2(src_script, dst_script)
|
||||
if dst_script.suffix == ".sh":
|
||||
dst_script.chmod(dst_script.stat().st_mode | 0o111)
|
||||
self.record_file_in_manifest(dst_script, project_root, manifest)
|
||||
created.append(dst_script)
|
||||
|
||||
return created
|
||||
|
||||
@staticmethod
|
||||
def process_template(
|
||||
content: str,
|
||||
@@ -299,13 +346,11 @@ class IntegrationBase(ABC):
|
||||
# 6. Replace __AGENT__
|
||||
content = content.replace("__AGENT__", agent_name)
|
||||
|
||||
# 7. Rewrite paths (matches release script's rewrite_paths())
|
||||
content = re.sub(r"(/?)memory/", r".specify/memory/", content)
|
||||
content = re.sub(r"(/?)scripts/", r".specify/scripts/", content)
|
||||
content = re.sub(r"(/?)templates/", r".specify/templates/", content)
|
||||
# Fix double-prefix (same as release script's .specify.specify/ fix)
|
||||
content = content.replace(".specify.specify/", ".specify/")
|
||||
content = content.replace(".specify/.specify/", ".specify/")
|
||||
# 7. Rewrite paths — delegate to the shared implementation in
|
||||
# CommandRegistrar so extension-local paths are preserved and
|
||||
# boundary rules stay consistent across the codebase.
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
content = CommandRegistrar.rewrite_project_relative_paths(content)
|
||||
|
||||
return content
|
||||
|
||||
@@ -405,11 +450,51 @@ class MarkdownIntegration(IntegrationBase):
|
||||
Subclasses only need to set ``key``, ``config``, ``registrar_config``
|
||||
(and optionally ``context_file``). Everything else is inherited.
|
||||
|
||||
The default ``setup()`` from ``IntegrationBase`` copies templates
|
||||
into the agent's commands directory — which is correct for the
|
||||
standard Markdown case.
|
||||
``setup()`` processes command templates (replacing ``{SCRIPT}``,
|
||||
``{ARGS}``, ``__AGENT__``, rewriting paths) and installs
|
||||
integration-specific scripts (``update-context.sh`` / ``.ps1``).
|
||||
"""
|
||||
|
||||
# MarkdownIntegration inherits IntegrationBase.setup() as-is.
|
||||
# Future stages may add markdown-specific path rewriting here.
|
||||
pass
|
||||
def setup(
|
||||
self,
|
||||
project_root: Path,
|
||||
manifest: IntegrationManifest,
|
||||
parsed_options: dict[str, Any] | None = None,
|
||||
**opts: Any,
|
||||
) -> list[Path]:
|
||||
templates = self.list_command_templates()
|
||||
if not templates:
|
||||
return []
|
||||
|
||||
project_root_resolved = project_root.resolve()
|
||||
if manifest.project_root != project_root_resolved:
|
||||
raise ValueError(
|
||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||
f"project_root ({project_root_resolved})"
|
||||
)
|
||||
|
||||
dest = self.commands_dest(project_root).resolve()
|
||||
try:
|
||||
dest.relative_to(project_root_resolved)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"Integration destination {dest} escapes "
|
||||
f"project root {project_root_resolved}"
|
||||
) from exc
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
script_type = opts.get("script_type", "sh")
|
||||
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS") if self.registrar_config else "$ARGUMENTS"
|
||||
created: list[Path] = []
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||
dst_name = self.command_filename(src_file.stem)
|
||||
dst_file = self.write_file_and_record(
|
||||
processed, dest / dst_name, project_root, manifest
|
||||
)
|
||||
created.append(dst_file)
|
||||
|
||||
created.extend(self.install_scripts(project_root, manifest))
|
||||
return created
|
||||
|
||||
Reference in New Issue
Block a user