From 31c8d3dc80d6a520eaaa30a5e9083dffc8c58893 Mon Sep 17 00:00:00 2001 From: eldar702 Date: Thu, 7 May 2026 20:48:22 +0300 Subject: [PATCH 1/2] fix(kiro-cli): replace literal $ARGUMENTS with prose fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kiro CLI file-based prompts do not natively substitute any argument placeholder (kirodotdev/Kiro#4141, kiro.dev/docs/cli manage-prompts), so the literal "$ARGUMENTS" set in KiroCliIntegration.registrar_config["args"] reached the model verbatim and broke the prompt — every parameterized SpecKit command under Kiro CLI was unusable. Replace the placeholder with a prose fallback that instructs the model to take its argument from the user's next message, mirroring the convention used by other integrations whose target CLI lacks native argument injection. Add two regression tests in TestKiroCliIntegration: - test_rendered_prompts_do_not_contain_raw_arguments - test_rendered_prompts_contain_kiro_arg_placeholder and override the inherited test_registrar_config so it does not require args == "$ARGUMENTS". Fixes #1926 --- .../integrations/kiro_cli/__init__.py | 10 +++- .../integrations/test_integration_kiro_cli.py | 51 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/integrations/kiro_cli/__init__.py b/src/specify_cli/integrations/kiro_cli/__init__.py index b316cb4bd2..4571b54f90 100644 --- a/src/specify_cli/integrations/kiro_cli/__init__.py +++ b/src/specify_cli/integrations/kiro_cli/__init__.py @@ -3,6 +3,14 @@ from ..base import MarkdownIntegration +# Kiro CLI file-based prompts do NOT support any argument-substitution syntax, +# so a raw "$ARGUMENTS" token would reach the model verbatim and break the +# prompt (issue #1926, kirodotdev/Kiro#4141). Use a prose fallback so the +# rendered prompt instructs the model to take its argument from the user's +# next message. +_KIRO_ARG_FALLBACK = "(the user will provide the argument in this conversation)" + + class KiroCliIntegration(MarkdownIntegration): key = "kiro-cli" config = { @@ -15,7 +23,7 @@ class KiroCliIntegration(MarkdownIntegration): registrar_config = { "dir": ".kiro/prompts", "format": "markdown", - "args": "$ARGUMENTS", + "args": _KIRO_ARG_FALLBACK, "extension": ".md", } context_file = "AGENTS.md" diff --git a/tests/integrations/test_integration_kiro_cli.py b/tests/integrations/test_integration_kiro_cli.py index e3b260bf05..bfcd7b894c 100644 --- a/tests/integrations/test_integration_kiro_cli.py +++ b/tests/integrations/test_integration_kiro_cli.py @@ -2,6 +2,9 @@ import os +from specify_cli.integrations import get_integration +from specify_cli.integrations.manifest import IntegrationManifest + from .test_integration_base_markdown import MarkdownIntegrationTests @@ -12,6 +15,54 @@ class TestKiroCliIntegration(MarkdownIntegrationTests): REGISTRAR_DIR = ".kiro/prompts" CONTEXT_FILE = "AGENTS.md" + def test_registrar_config(self): + """Override base assertion: kiro-cli uses a prose fallback for args + because Kiro CLI file-based prompts do not natively substitute + ``$ARGUMENTS`` (see issue #1926 / kirodotdev/Kiro#4141).""" + i = get_integration(self.KEY) + assert i.registrar_config["dir"] == self.REGISTRAR_DIR + assert i.registrar_config["format"] == "markdown" + assert i.registrar_config["args"] != "$ARGUMENTS" + assert i.registrar_config["args"] + assert i.registrar_config["extension"] == ".md" + + def test_rendered_prompts_do_not_contain_raw_arguments(self, tmp_path): + """Rendered Kiro prompt files must NOT contain the raw ``$ARGUMENTS`` + token — Kiro CLI does not substitute it, so the literal would reach + the model and break the prompt (issue #1926).""" + integration = get_integration(self.KEY) + manifest = IntegrationManifest(self.KEY, tmp_path) + integration.setup(tmp_path, manifest, script_type="sh") + + prompts_dir = tmp_path / self.REGISTRAR_DIR + rendered = list(prompts_dir.glob("*.md")) + assert rendered, "expected at least one rendered prompt file" + + offenders = [ + p.name for p in rendered if "$ARGUMENTS" in p.read_text(encoding="utf-8") + ] + assert offenders == [], ( + f"these rendered prompts still contain the raw $ARGUMENTS token: {offenders}" + ) + + def test_rendered_prompts_contain_kiro_arg_placeholder(self, tmp_path): + """The chosen kiro-cli args fallback string must end up in at least + one rendered prompt (proves substitution actually fired, not just + that $ARGUMENTS was removed).""" + integration = get_integration(self.KEY) + manifest = IntegrationManifest(self.KEY, tmp_path) + integration.setup(tmp_path, manifest, script_type="sh") + + expected = integration.registrar_config["args"] + prompts_dir = tmp_path / self.REGISTRAR_DIR + contents = "\n".join( + p.read_text(encoding="utf-8") for p in prompts_dir.glob("*.md") + ) + assert expected in contents, ( + f"none of the rendered prompts contain the configured args fallback " + f"({expected!r})" + ) + class TestKiroAlias: """--ai kiro alias normalizes to kiro-cli and auto-promotes.""" From 835de7d2a56a9f2886349dd30c8a21911a99c3ca Mon Sep 17 00:00:00 2001 From: eldar702 Date: Sun, 10 May 2026 15:38:11 +0300 Subject: [PATCH 2/2] test(kiro-cli): tighten args regression guard + document quirk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on PR #2482. Two changes that bracket the original bug fix from both sides — code AND documentation: 1. Test layer (Copilot finding at lines 27, 56) The previous test_registrar_config asserted only that args != "$ARGUMENTS" and that args is truthy. That would silently pass if a future change swapped $ARGUMENTS for $INPUT, {{userMessage}}, , or any other unsubstituted placeholder syntax — defeating the regression guard for issue #1926. Replace with a dual-layer guard: - test_registrar_config_args_is_exact_prose_fallback pins args to the imported _KIRO_ARG_FALLBACK constant. Wording drift now requires a deliberate paired commit (production constant + test). - test_registrar_config_args_does_not_look_like_a_placeholder_token is an independent regression guard built on a 7-pattern regex set covering Bash ($X, ${X}, ${X:-default}), Mustache/Handlebars/Jinja ({{X}}, {{{X}}}), Liquid/Jinja control ({% %}), Python str.format / .NET ({0}, {var}), angle-bracket (), and Windows (%X%). Patterns are anchored to the full string so legitimate prose mentioning a placeholder ("the {{magic}} of placeholders") is not flagged. Also fix the line-56 tautology by importing _KIRO_ARG_FALLBACK directly into test_rendered_prompts_contain_kiro_arg_placeholder, instead of reading the constant back from registrar_config["args"]. The test now verifies the FALLBACK STRING reaches the rendered output, independent of the integration's own config staying correct. 2. Docs layer (mnriem CHANGES_REQUESTED) The Kiro CLI row in docs/reference/integrations.md only documented its alias. Update the notes column to lead with the limitation — Kiro CLI does not substitute $ARGUMENTS in file-based prompts, so Spec Kit ships a prose fallback at render time — with inline links to upstream Kiro "Manage prompts" docs and issue #1926. Style follows the Pi row ("limitation first, alias preserved at end"). Refs #1926 --- docs/reference/integrations.md | 2 +- .../integrations/test_integration_kiro_cli.py | 69 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 42332b1fe7..e3773d96ba 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -23,7 +23,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` | | | [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; supports `--migrate-legacy` for dotted→hyphenated directory migration | -| [Kiro CLI](https://kiro.dev/docs/cli/) | `kiro-cli` | Alias: `--integration kiro` | +| [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` | | [Lingma](https://lingma.aliyun.com/) | `lingma` | Skills-based integration; skills are installed automatically | | [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | `vibe` | | | [opencode](https://opencode.ai/) | `opencode` | | diff --git a/tests/integrations/test_integration_kiro_cli.py b/tests/integrations/test_integration_kiro_cli.py index bfcd7b894c..9b0b23da08 100644 --- a/tests/integrations/test_integration_kiro_cli.py +++ b/tests/integrations/test_integration_kiro_cli.py @@ -1,13 +1,41 @@ """Tests for KiroCliIntegration.""" import os +import re from specify_cli.integrations import get_integration +from specify_cli.integrations.kiro_cli import _KIRO_ARG_FALLBACK from specify_cli.integrations.manifest import IntegrationManifest from .test_integration_base_markdown import MarkdownIntegrationTests +# Regex shapes that indicate a value is a placeholder token, not prose. +# Covers Bash ($VAR, ${VAR}, ${VAR:-default}), Mustache/Handlebars/Jinja +# ({{var}}, {{{var}}}), Liquid/Jinja control ({% ... %}), Python str.format / +# .NET ({var}, {0}), angle-bracket (), and Windows-style (%VAR%). +# Anchored to the FULL STRING so legitimate prose mentioning a placeholder +# (e.g. "the {{magic}} of placeholders") is not flagged. The Liquid pattern +# is anchored to the START so multi-tag templates fire while mid-sentence +# {%-quotation does not. +_PLACEHOLDER_TOKEN_PATTERNS = ( + re.compile(r"^\$\w+$"), # $ARGUMENTS, $args + re.compile(r"^\$\{\w+(?:[:\-+?][^}]*)?\}$"), # ${ARGS}, ${ARGS:-default} + re.compile(r"^\{\{\{?\s*\w+(\s*[|.][^}]*)?\s*\}?\}\}$"), # {{var}} {{{var}}} {{x|y}} + re.compile(r"^\{%"), # {% if x %}{{ x }}{% endif %} + re.compile(r"^<\w+>$"), # + re.compile(r"^%\w+%$"), # %USERNAME% + re.compile(r"^\{(?:\d+|[a-zA-Z_]\w*)(?:[.\[][^}]*)?(?:![rsa])?(?::[^}]*)?\}$"), # {0}, {var}, {0:>5} +) + + +def _looks_like_placeholder_token(value: str) -> bool: + """Return True if *value* matches a known placeholder-token shape.""" + if not value: + return False + return any(p.search(value) for p in _PLACEHOLDER_TOKEN_PATTERNS) + + class TestKiroCliIntegration(MarkdownIntegrationTests): KEY = "kiro-cli" FOLDER = ".kiro/" @@ -18,14 +46,41 @@ class TestKiroCliIntegration(MarkdownIntegrationTests): def test_registrar_config(self): """Override base assertion: kiro-cli uses a prose fallback for args because Kiro CLI file-based prompts do not natively substitute - ``$ARGUMENTS`` (see issue #1926 / kirodotdev/Kiro#4141).""" + ``$ARGUMENTS`` (see issue #1926 / kirodotdev/Kiro#4141). The + regression-guard load is carried by the two layer tests below + (exact-fallback + placeholder-shape rejection).""" i = get_integration(self.KEY) assert i.registrar_config["dir"] == self.REGISTRAR_DIR assert i.registrar_config["format"] == "markdown" - assert i.registrar_config["args"] != "$ARGUMENTS" - assert i.registrar_config["args"] assert i.registrar_config["extension"] == ".md" + def test_registrar_config_args_is_exact_prose_fallback(self): + """Layer 1 — pin the exact fallback so wording drift requires a + deliberate paired commit (production constant + test update).""" + i = get_integration(self.KEY) + assert i.registrar_config["args"] == _KIRO_ARG_FALLBACK, ( + f"args drifted from the pinned fallback constant. " + f"Got: {i.registrar_config['args']!r}; expected: {_KIRO_ARG_FALLBACK!r}. " + f"If the wording change is intentional, update _KIRO_ARG_FALLBACK and " + f"this test together." + ) + + def test_registrar_config_args_does_not_look_like_a_placeholder_token(self): + """Layer 2 — independent regression guard: even if someone bypasses + layer-1 by changing both constant and test, the value still must not + look like ANY placeholder token shape ($X, ${X}, {{X}}, , %X%, {0}, + {% %}). Catches the class of regression Copilot called out: a swap + from $ARGUMENTS to $INPUT or {{userMessage}} would fail this test + even if it accidentally passed layer 1.""" + i = get_integration(self.KEY) + args = i.registrar_config["args"] + assert not _looks_like_placeholder_token(args), ( + f"registrar_config['args'] = {args!r} matches a known placeholder-" + f"token shape — Kiro CLI does not substitute placeholders so this " + f"would reach the model verbatim and break the prompt (issue #1926). " + f"Use a prose fallback instead." + ) + def test_rendered_prompts_do_not_contain_raw_arguments(self, tmp_path): """Rendered Kiro prompt files must NOT contain the raw ``$ARGUMENTS`` token — Kiro CLI does not substitute it, so the literal would reach @@ -48,12 +103,16 @@ def test_rendered_prompts_do_not_contain_raw_arguments(self, tmp_path): def test_rendered_prompts_contain_kiro_arg_placeholder(self, tmp_path): """The chosen kiro-cli args fallback string must end up in at least one rendered prompt (proves substitution actually fired, not just - that $ARGUMENTS was removed).""" + that $ARGUMENTS was removed). Imports the fallback constant directly + instead of reading the field back so the test stays independent of + the integration's own config — even if the registrar_config['args'] + regresses, this test still verifies the FALLBACK STRING is in the + rendered output.""" integration = get_integration(self.KEY) manifest = IntegrationManifest(self.KEY, tmp_path) integration.setup(tmp_path, manifest, script_type="sh") - expected = integration.registrar_config["args"] + expected = _KIRO_ARG_FALLBACK prompts_dir = tmp_path / self.REGISTRAR_DIR contents = "\n".join( p.read_text(encoding="utf-8") for p in prompts_dir.glob("*.md")