Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
88c9c13
fix(integrations): dispatch kiro-cli through chat --no-interactive
kartsan03 Sep 30, 2026
86afed7
fix(integrations): install kiro-cli prompts with hyphenated names
kartsan03 Sep 30, 2026
cb898ce
test(integrations): restore the kiro prompt as bytes in the upgrade test
kartsan03 Sep 30, 2026
270f740
fix(integrations): remove dotted kiro extension prompts on upgrade
kartsan03 Sep 30, 2026
569a1bd
fix(integrations): only treat real renames as a Kiro filename migration
kartsan03 Sep 30, 2026
268a908
fix(integrations): keep disabled preset prompts and SKILL.md paths ou…
kartsan03 Sep 30, 2026
cc91560
fix(integrations): retire dotted Kiro prompts only after their replac…
kartsan03 Sep 30, 2026
f53b21f
fix(integrations): refuse the Kiro prompt rename while presets are in…
kartsan03 Sep 30, 2026
ec15335
fix(integrations): stop treating the Kiro IDE launcher as Kiro CLI
kartsan03 Oct 1, 2026
ed42004
test(workflows): pin the zip entry time in catalog release archives
kartsan03 Oct 1, 2026
a5334ea
Merge branch 'main' into fix/kiro-cli-dispatch
kartsan03 Oct 2, 2026
234bbd2
fix(integrations): retire dotted Kiro extension prompts on every regi…
kartsan03 Oct 2, 2026
3bcee7e
fix(integrations): keep Kiro prompts tracked when a command source is…
kartsan03 Oct 3, 2026
62732e8
fix(integrations): keep Kiro prompts that hyphenate to one file
kartsan03 Oct 5, 2026
7034726
Merge branch 'main' into fix/kiro-cli-dispatch
kartsan03 Oct 6, 2026
8570f0a
fix(integrations): skip Kiro prompts that share a file instead of rew…
kartsan03 Oct 6, 2026
fdb8d75
fix(extensions): count unreadable owners at install, remove skipped Q…
kartsan03 Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [Junie](https://junie.jetbrains.com/) | `junie` | |
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | `kilocode` | Installs commands into `.kilo/commands`; legacy `.kilocode/workflows` installs remain supported as a registration fallback |
| [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; installs into `.kimi-code/skills/`. `--migrate-legacy` moves old `.kimi/skills/` installs to the new paths |
| [Kiro CLI](https://kiro.dev/docs/cli/) | `kiro-cli` | Kiro CLI does not substitute `$ARGUMENTS` in file-based prompts, so Spec Kit ships a prose fallback at render time (see [Manage prompts](https://kiro.dev/docs/cli/chat/manage-prompts/) and issue [#1926](https://github.com/github/spec-kit/issues/1926)). Alias: `--integration kiro` |
| [Kiro CLI](https://kiro.dev/docs/cli/) | `kiro-cli` | Installs prompts as `.kiro/prompts/speckit-<command>.md` and invokes them as `/speckit-<command>`, because Kiro CLI does not run slash commands whose names contain dots. Headless workflow dispatch runs `kiro-cli chat --no-interactive --trust-all-tools`, which auto-approves every tool call: without it, Kiro denies all file writes in non-interactive mode and still exits 0. Kiro CLI does not substitute `$ARGUMENTS` in file-based prompts, so Spec Kit ships a prose fallback at render time (see [Manage prompts](https://kiro.dev/docs/cli/chat/manage-prompts/) and issue [#1926](https://github.com/github/spec-kit/issues/1926)). Alias: `--integration kiro` |
| [Lingma](https://lingma.aliyun.com/) | `lingma` | Skills-based integration; skills are installed automatically |
| [MiniMax Code](https://github.com/MiniMax-AI/minimax-code) | `mcode` | Skills-based integration; installs skills into `.minimax/skills` and invokes them as `/speckit-<command>`. Headless workflow dispatch uses `--permission full` so tool actions are not blocked by headless permission gating; set `SPECKIT_INTEGRATION_MCODE_EXTRA_ARGS="--permission smart"` to use MiniMax Code's stricter policy. |
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | `vibe` | |
Expand Down
21 changes: 21 additions & 0 deletions src/specify_cli/integrations/_command_upgrade_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ def _legacy_command_root_changed(
return old_had_legacy and new_has_canonical


def _command_file_names_changed(integration, old_manifest, new_manifest) -> bool:
"""Return True when core command files were renamed inside the command dir.

Kiro CLI moved from ``speckit.<cmd>.md`` to ``speckit-<cmd>.md`` in the
same ``.kiro/prompts`` directory (#4797). Only a removed file that matches
an added one up to ``.``/``-`` separators counts, so a release that just
adds and drops commands is not a rename. Enabled extension and preset
commands are tracked outside the manifest, so re-registering them alone
would leave their old names beside the new ones.
"""
commands_dir = (integration.registrar_config or {}).get("dir")
if not isinstance(commands_dir, str) or not commands_dir.strip():
return False
old = {rel for rel in old_manifest.files if _manifest_path_under(rel, commands_dir)}
new = {rel for rel in new_manifest.files if _manifest_path_under(rel, commands_dir)}
# Compare whole paths: skill layouts name every file SKILL.md.
removed = {rel.replace(".", "-") for rel in old - new}
added = {rel.replace(".", "-") for rel in new - old}
return bool(removed & added)


def _legacy_command_root_upgrade_pending(integration, old_manifest) -> bool:
"""Return True when the old manifest tracks command files under legacy_dir."""
config = integration.registrar_config or {}
Expand Down
3 changes: 2 additions & 1 deletion src/specify_cli/integrations/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ def _unregister_presets_for_agent(
agent_key: str,
*,
continuing: str,
enabled_only: bool = False,
) -> None:
"""Best-effort removal of ``agent_key``'s preset command/skill artifacts.

Expand All @@ -553,7 +554,7 @@ def _unregister_presets_for_agent(
from ..presets import PresetManager

preset_mgr = PresetManager(project_root)
preset_mgr.unregister_agent_artifacts(agent_key)
preset_mgr.unregister_agent_artifacts(agent_key, enabled_only=enabled_only)
except Exception as preset_err:
from .. import _print_cli_warning

Expand Down
25 changes: 21 additions & 4 deletions src/specify_cli/integrations/command_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
from ..integration_state import default_integration_key as _default_integration_key, installed_integration_keys as _installed_integration_keys
from ._command_upgrade_layout import (
_PresetRegistryUnreadableError,
_command_file_names_changed,
_installed_command_presets_affecting_agent,
_installed_presets_affecting_agent,
_legacy_command_root_changed,
_legacy_command_root_upgrade_pending,
_manifest_tracks_skill_layout,
)
from ._commands import integration_app
from ._helpers import _MANIFEST_READ_ERRORS, _SharedTemplateRefreshError, _cli_error_detail, _cli_phase_label, _get_speckit_version, _read_integration_json, _refresh_init_options_speckit_version, _register_extensions_for_agent, _register_presets_for_agent, _resolve_integration_options, _resolve_integration_script_type, _resync_manifest_after_registration, _unregister_enabled_extension_commands_for_agent, _update_init_options_for_integration, _write_integration_json
from ._helpers import _MANIFEST_READ_ERRORS, _SharedTemplateRefreshError, _cli_error_detail, _cli_phase_label, _get_speckit_version, _read_integration_json, _refresh_init_options_speckit_version, _register_extensions_for_agent, _register_presets_for_agent, _resolve_integration_options, _resolve_integration_script_type, _resync_manifest_after_registration, _unregister_enabled_extension_commands_for_agent, _unregister_presets_for_agent, _update_init_options_for_integration, _write_integration_json


@integration_app.command("upgrade")
Expand Down Expand Up @@ -317,13 +318,16 @@ def integration_upgrade(
old_manifest,
new_manifest,
)
if legacy_command_root_changed:
command_file_names_changed = _command_file_names_changed(
integration, old_manifest, new_manifest
)
if legacy_command_root_changed or command_file_names_changed:
_unregister_enabled_extension_commands_for_agent(
project_root,
key,
continuing=(
"The integration command directory changed, but legacy enabled "
"extension artifacts may need manual cleanup."
"The integration command directory or file names changed, but "
"legacy enabled extension artifacts may need manual cleanup."
),
)
Comment thread
Copilot marked this conversation as resolved.
Outdated

Expand All @@ -338,6 +342,19 @@ def integration_upgrade(
force=True,
continuing="The integration was upgraded, but installed extensions may need re-registration.",
)
if command_file_names_changed:
# Preset commands aren't in the manifest either, so drop enabled
# ones under their old file names before re-registering. Disabled
# presets keep their files until removal.
_unregister_presets_for_agent(
project_root,
key,
continuing=(
"The integration command file names changed, but old "
"preset command files may need manual cleanup."
),
enabled_only=True,
)
_register_presets_for_agent(
project_root,
key,
Expand Down
160 changes: 159 additions & 1 deletion src/specify_cli/integrations/kiro_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
"""Kiro CLI integration."""

from ..base import MarkdownIntegration
from __future__ import annotations

import re
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

from ..base import _HOOK_COMMAND_NOTE, MarkdownIntegration
from ..manifest import IntegrationManifest


# Kiro CLI file-based prompts do NOT support any argument-substitution syntax,
Expand All @@ -11,6 +19,31 @@
_KIRO_ARG_FALLBACK = "(the user will provide the argument in this conversation)"


def format_kiro_command_name(cmd_name: str) -> str:
"""Convert a command name to the hyphenated form Kiro CLI can invoke.

Kiro CLI runs ``/name`` from ``.kiro/prompts/name.md`` only when the name
has no dots: ``/speckit.plan`` is rejected as an unrecognized slash
command, while ``/speckit-plan`` runs the prompt (issue #4797).

The function is idempotent: already-formatted names are returned unchanged.

Examples:
>>> format_kiro_command_name("plan")
'speckit-plan'
>>> format_kiro_command_name("speckit.plan")
'speckit-plan'
>>> format_kiro_command_name("speckit.git.commit")
'speckit-git-commit'
"""
cmd_name = cmd_name.replace(".", "-")

if not cmd_name.startswith("speckit-"):
cmd_name = f"speckit-{cmd_name}"
Comment thread
kartsan03 marked this conversation as resolved.

return cmd_name


class KiroCliIntegration(MarkdownIntegration):
key = "kiro-cli"
# Kiro CLI keeps everything under a static, isolated agent root
Expand All @@ -33,4 +66,129 @@ class KiroCliIntegration(MarkdownIntegration):
"format": "markdown",
"args": _KIRO_ARG_FALLBACK,
"extension": ".md",
"format_name": format_kiro_command_name,
"invoke_separator": "-",
Comment thread
Copilot marked this conversation as resolved.
}
invoke_separator = "-"

def command_filename(self, template_name: str) -> str:
return format_kiro_command_name(template_name) + ".md"

def build_command_invocation(self, command_name: str, args: str = "") -> str:
"""Kiro CLI installs hyphenated prompts (``/speckit-<name>``), so the
dispatch invocation must match; the inherited dotted ``/speckit.<name>``
is not a slash command Kiro recognizes.
"""
invocation = "/" + format_kiro_command_name(command_name)
if args:
invocation = f"{invocation} {args}"
return invocation

def process_template(self, *args, **kwargs):
"""Ensure shared templates render Kiro command references with hyphens."""
kwargs.setdefault("invoke_separator", self.invoke_separator)
return super().process_template(*args, **kwargs)

@staticmethod
def _inject_hook_command_note(content: str) -> str:
"""Insert a dot-to-hyphen note before each hook output instruction.

Hook command names come from ``extensions.yml`` in dot notation, so
the model needs the mapping to the hyphenated prompt names. Skips
instructions that already have the note immediately above them (the
per-instruction check from the Cline fix in #4150).
"""
note = _HOOK_COMMAND_NOTE.rstrip("\n")

def repl(m: re.Match[str]) -> str:
indent = m.group(1)
instruction = m.group(2)
previous_lines = content[:m.start()].splitlines()
if previous_lines and previous_lines[-1] == indent + note:
return m.group(0)
# ``eol`` is empty when the instruction is the final line of a
# file with no trailing newline.
eol = m.group(3) or "\n"
return indent + note + eol + indent + instruction + eol

return re.sub(
r"(?m)^([ \t]*)(- For each executable hook, output the following[^\r\n]*)(\r\n|\n|$)",
repl,
content,
)

@staticmethod
def _rewrite_handoff_references(content: str) -> str:
"""Replace dot-notation agent references in handoffs with hyphens."""
return re.sub(
r"(?m)^(\s*agent:\s*)(speckit\.[A-Za-z0-9-_]+(?:\.[A-Za-z0-9-_]+)*)",
lambda m: f"{m.group(1)}{format_kiro_command_name(m.group(2))}",
content,
)

def post_process_command_content(self, content: str) -> str:
"""Apply the hook note and handoff rewrite to Kiro prompt content.

``CommandRegistrar.register_commands()`` calls this hook too, so
extension and preset prompts get the same transforms as core ones.
"""
return self._rewrite_handoff_references(self._inject_hook_command_note(content))

def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install Kiro prompts and apply post-processing transformations."""
created = super().setup(project_root, manifest, parsed_options, **opts)

dest_dir = self.commands_dest(project_root).resolve()
for path in created:
try:
path.resolve().relative_to(dest_dir)
except ValueError:
continue
if path.suffix != ".md":
continue

content = path.read_bytes().decode("utf-8")
updated = self.post_process_command_content(content)
if updated != content:
path.write_bytes(updated.encode("utf-8"))
self.record_file_in_manifest(path, project_root, manifest)

return created

def build_exec_args(
self,
prompt: str,
*,
model: str | None = None,
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build CLI arguments for headless ``kiro-cli chat`` execution.

The inherited ``kiro-cli -p <prompt>`` exits 2 at argument parsing
(``unexpected argument '-p'``). Kiro CLI runs one prompt headless
through ``chat --no-interactive`` with the prompt as its positional
input, and a ``/speckit-*`` input there runs the matching
``.kiro/prompts`` file. Headless mode cannot ask for tool approval, so
without ``--trust-all-tools`` every file write is denied while the run
still exits 0 (same role as Copilot's ``--yolo`` / Cursor's
``--force``). Kiro has no ``json`` output format; its structured output
is ``--output-format stream-json`` (JSON Lines).
"""
self.validate_runtime_config(integration_args, integration_options)
args = [self._resolve_executable(), "chat", "--no-interactive", "--trust-all-tools"]
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
if output_json:
args.extend(["--output-format", "stream-json"])
args.append(prompt)
return args
11 changes: 10 additions & 1 deletion src/specify_cli/presets/_manager_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,9 @@ def register_enabled_presets_for_agent(self, agent_name: str) -> None:
pack_id, {"registered_skills": merged_skills}
)

def unregister_agent_artifacts(self, agent_name: str) -> None:
def unregister_agent_artifacts(
self, agent_name: str, *, enabled_only: bool = False
) -> None:
"""Remove ``agent_name``'s tracked preset command/skill artifacts.

Mirrors ``ExtensionManager.unregister_agent_artifacts()`` (#2948):
Expand All @@ -689,6 +691,10 @@ def unregister_agent_artifacts(self, agent_name: str) -> None:
tracking, and preset packs themselves are left untouched, and no
priority-stack reconciliation runs — this is agent-scoped cleanup
only, not preset removal.

Set ``enabled_only=True`` when a caller is about to re-register
enabled presets and must keep disabled presets' artifacts, which
stay in place until the preset is removed.
"""
if not agent_name:
return
Expand All @@ -705,6 +711,9 @@ def unregister_agent_artifacts(self, agent_name: str) -> None:
return

for pack_id, metadata in list(self.registry.list().items()):
if enabled_only and not metadata.get("enabled", True):
continue

updates: Dict[str, Any] = {}

raw_skills = metadata.get("registered_skills", [])
Expand Down
Loading
Loading