Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 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
2f981de
Merge branch 'main' into fix/kiro-cli-dispatch
kartsan03 Oct 9, 2026
eab0910
fix(integrations): skip Kiro prompts that share a file instead of rew…
kartsan03 Oct 6, 2026
e7d15bf
fix(extensions): count unreadable owners at install, remove skipped Q…
kartsan03 Oct 6, 2026
ec595b6
fix(extensions): preserve shared migrated commands during removal
mnriem Oct 7, 2026
18aca19
fix(integrations): refuse the Kiro rename when prompt ownership is un…
kartsan03 Oct 7, 2026
5a22b1b
fix(extensions): check the ownership marker before removal deletes a …
kartsan03 Oct 7, 2026
7950bed
fix(extensions): keep unwritten registered names for Kiro CLI and Qod…
kartsan03 Oct 7, 2026
237f781
fix(extensions): let a disabled extension's manifest claim its files
kartsan03 Oct 8, 2026
02c6311
fix(extensions): check ownership on every Kiro CLI and Qoder cleanup …
kartsan03 Oct 8, 2026
8014038
fix(integrations): detect a pending Kiro rename from the prompts on disk
kartsan03 Oct 8, 2026
f77f6ad
fix(integrations): ignore dotted Kiro prompts left after the rename
kartsan03 Oct 8, 2026
344ef64
Merge branch 'main' into fix/kiro-cli-dispatch
kartsan03 Oct 9, 2026
64ebbc8
fix(integrations): journal Kiro CLI prompt rewrites like Cline and Junie
kartsan03 Oct 9, 2026
5b7a674
fix(extensions): print the unowned prompt path with forward slashes
kartsan03 Oct 9, 2026
5ba189e
test(integrations): build the dotted Kiro fixture from the integratio…
kartsan03 Oct 9, 2026
a844c73
fix(extensions): retire a dropped Kiro CLI or Qoder alias that shares…
kartsan03 Oct 9, 2026
e8bf882
fix(extensions): keep Kiro CLI and Qoder cleanup in the project's con…
kartsan03 Oct 9, 2026
e9b6a9b
fix(extensions): load installed integrations before checking every agent
kartsan03 Oct 9, 2026
7c446b3
fix(extensions): keep a prompt that is a symlink loop
kartsan03 Oct 9, 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
16 changes: 16 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,22 @@ 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). Enabled extension commands are
tracked in the extension registry rather than 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)}
return bool(old - new) and bool(new - old)
Comment thread
Copilot marked this conversation as resolved.
Resolved


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
9 changes: 6 additions & 3 deletions src/specify_cli/integrations/command_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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,
Expand Down Expand Up @@ -317,13 +318,15 @@ def integration_upgrade(
old_manifest,
new_manifest,
)
if legacy_command_root_changed:
if legacy_command_root_changed or _command_file_names_changed(
integration, old_manifest, new_manifest
):
_unregister_enabled_extension_commands_for_agent(
Comment thread
Copilot marked this conversation as resolved.
Resolved
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."
),
)

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.
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.
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
Loading
Loading