fix(integrations): install kiro-cli prompts with hyphenated names · github/spec-kit@86afed7 · GitHub
Skip to content

Commit 86afed7

Browse files
committed
fix(integrations): install kiro-cli prompts with hyphenated names
Kiro CLI runs /name from .kiro/prompts/name.md only when the name has no dots, so the installed /speckit.plan is rejected as an unrecognized slash command. Install speckit-<command>.md and dispatch /speckit-<command>, following the Junie and Cline integrations, so workflow steps run the prompt instead of relying on the model to find the file. Upgrade stale-removes the old dotted prompts. Refs #4797 Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
1 parent 88c9c13 commit 86afed7

4 files changed

Lines changed: 276 additions & 11 deletions

File tree

‎docs/reference/integrations.md‎

Lines changed: 1 addition & 1 deletion

‎src/specify_cli/integrations/kiro_cli/__init__.py‎

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
from __future__ import annotations
44

5+
import re
56
from collections.abc import Mapping, Sequence
67
from pathlib import Path
78
from typing import Any
89

9-
from ..base import MarkdownIntegration
10+
from ..base import _HOOK_COMMAND_NOTE, MarkdownIntegration
11+
from ..manifest import IntegrationManifest
1012

1113

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

1921

22+
def format_kiro_command_name(cmd_name: str) -> str:
23+
"""Convert a command name to the hyphenated form Kiro CLI can invoke.
24+
25+
Kiro CLI runs ``/name`` from ``.kiro/prompts/name.md`` only when the name
26+
has no dots: ``/speckit.plan`` is rejected as an unrecognized slash
27+
command, while ``/speckit-plan`` runs the prompt (issue #4797).
28+
29+
The function is idempotent: already-formatted names are returned unchanged.
30+
31+
Examples:
32+
>>> format_kiro_command_name("plan")
33+
'speckit-plan'
34+
>>> format_kiro_command_name("speckit.plan")
35+
'speckit-plan'
36+
>>> format_kiro_command_name("speckit.git.commit")
37+
'speckit-git-commit'
38+
"""
39+
cmd_name = cmd_name.replace(".", "-")
40+
41+
if not cmd_name.startswith("speckit-"):
42+
cmd_name = f"speckit-{cmd_name}"
43+
44+
return cmd_name
45+
46+
2047
class KiroCliIntegration(MarkdownIntegration):
2148
key = "kiro-cli"
2249
# Kiro CLI keeps everything under a static, isolated agent root
@@ -39,7 +66,100 @@ class KiroCliIntegration(MarkdownIntegration):
3966
"format": "markdown",
4067
"args": _KIRO_ARG_FALLBACK,
4168
"extension": ".md",
69+
"format_name": format_kiro_command_name,
70+
"invoke_separator": "-",
4271
}
72+
invoke_separator = "-"
73+
74+
def command_filename(self, template_name: str) -> str:
75+
return format_kiro_command_name(template_name) + ".md"
76+
77+
def build_command_invocation(self, command_name: str, args: str = "") -> str:
78+
"""Kiro CLI installs hyphenated prompts (``/speckit-<name>``), so the
79+
dispatch invocation must match; the inherited dotted ``/speckit.<name>``
80+
is not a slash command Kiro recognizes.
81+
"""
82+
invocation = "/" + format_kiro_command_name(command_name)
83+
if args:
84+
invocation = f"{invocation} {args}"
85+
return invocation
86+
87+
def process_template(self, *args, **kwargs):
88+
"""Ensure shared templates render Kiro command references with hyphens."""
89+
kwargs.setdefault("invoke_separator", self.invoke_separator)
90+
return super().process_template(*args, **kwargs)
91+
92+
@staticmethod
93+
def _inject_hook_command_note(content: str) -> str:
94+
"""Insert a dot-to-hyphen note before each hook output instruction.
95+
96+
Hook command names come from ``extensions.yml`` in dot notation, so
97+
the model needs the mapping to the hyphenated prompt names. Skips
98+
instructions that already have the note immediately above them (the
99+
per-instruction check from the Cline fix in #4150).
100+
"""
101+
note = _HOOK_COMMAND_NOTE.rstrip("\n")
102+
103+
def repl(m: re.Match[str]) -> str:
104+
indent = m.group(1)
105+
instruction = m.group(2)
106+
previous_lines = content[:m.start()].splitlines()
107+
if previous_lines and previous_lines[-1] == indent + note:
108+
return m.group(0)
109+
# ``eol`` is empty when the instruction is the final line of a
110+
# file with no trailing newline.
111+
eol = m.group(3) or "\n"
112+
return indent + note + eol + indent + instruction + eol
113+
114+
return re.sub(
115+
r"(?m)^([ \t]*)(- For each executable hook, output the following[^\r\n]*)(\r\n|\n|$)",
116+
repl,
117+
content,
118+
)
119+
120+
@staticmethod
121+
def _rewrite_handoff_references(content: str) -> str:
122+
"""Replace dot-notation agent references in handoffs with hyphens."""
123+
return re.sub(
124+
r"(?m)^(\s*agent:\s*)(speckit\.[A-Za-z0-9-_]+(?:\.[A-Za-z0-9-_]+)*)",
125+
lambda m: f"{m.group(1)}{format_kiro_command_name(m.group(2))}",
126+
content,
127+
)
128+
129+
def post_process_command_content(self, content: str) -> str:
130+
"""Apply the hook note and handoff rewrite to Kiro prompt content.
131+
132+
``CommandRegistrar.register_commands()`` calls this hook too, so
133+
extension and preset prompts get the same transforms as core ones.
134+
"""
135+
return self._rewrite_handoff_references(self._inject_hook_command_note(content))
136+
137+
def setup(
138+
self,
139+
project_root: Path,
140+
manifest: IntegrationManifest,
141+
parsed_options: dict[str, Any] | None = None,
142+
**opts: Any,
143+
) -> list[Path]:
144+
"""Install Kiro prompts and apply post-processing transformations."""
145+
created = super().setup(project_root, manifest, parsed_options, **opts)
146+
147+
dest_dir = self.commands_dest(project_root).resolve()
148+
for path in created:
149+
try:
150+
path.resolve().relative_to(dest_dir)
151+
except ValueError:
152+
continue
153+
if path.suffix != ".md":
154+
continue
155+
156+
content = path.read_bytes().decode("utf-8")
157+
updated = self.post_process_command_content(content)
158+
if updated != content:
159+
path.write_bytes(updated.encode("utf-8"))
160+
self.record_file_in_manifest(path, project_root, manifest)
161+
162+
return created
43163

44164
def build_exec_args(
45165
self,
@@ -56,7 +176,7 @@ def build_exec_args(
56176
The inherited ``kiro-cli -p <prompt>`` exits 2 at argument parsing
57177
(``unexpected argument '-p'``). Kiro CLI runs one prompt headless
58178
through ``chat --no-interactive`` with the prompt as its positional
59-
input, and a ``/speckit.*`` input there runs the matching
179+
input, and a ``/speckit-*`` input there runs the matching
60180
``.kiro/prompts`` file. Headless mode cannot ask for tool approval, so
61181
without ``--trust-all-tools`` every file write is denied while the run
62182
still exits 0 (same role as Copilot's ``--yolo`` / Cursor's

‎tests/integrations/test_integration_kiro_cli.py‎

Lines changed: 124 additions & 8 deletions

0 commit comments

Comments
 (0)