Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 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
a80126f
fix(extensions): preserve shared migrated commands during removal
mnriem Oct 7, 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
Prev Previous commit
Next Next commit
fix(integrations): only treat real renames as a Kiro filename migration
_command_file_names_changed() now needs a removed command file and an
added one with the same name up to "."/"-" separators, so a release that
adds one command and drops another no longer unregisters extension
commands. When the rename does happen on the active integration, preset
commands are unregistered before re-registration too, so dotted preset
prompts such as speckit.fakeext.cmd.md don't survive the upgrade.

Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
  • Loading branch information
kartsan03 committed Sep 30, 2026
commit 569a1bdb7fec874d4890027cddf08cc4751145d2
12 changes: 8 additions & 4 deletions src/specify_cli/integrations/_command_upgrade_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ 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.
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)}
return bool(old - new) and bool(new - old)
removed = {PurePath(rel).name.replace(".", "-") for rel in old - new}
added = {PurePath(rel).name.replace(".", "-") for rel in new - old}
Comment thread
Copilot marked this conversation as resolved.
Outdated
return bool(removed & added)


def _legacy_command_root_upgrade_pending(integration, old_manifest) -> bool:
Expand Down
18 changes: 15 additions & 3 deletions src/specify_cli/integrations/command_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
_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 @@ -318,9 +318,10 @@ def integration_upgrade(
old_manifest,
new_manifest,
)
if legacy_command_root_changed or _command_file_names_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,
Expand All @@ -341,6 +342,17 @@ 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 them
# under their old file names before re-registering.
_unregister_presets_for_agent(
project_root,
key,
continuing=(
"The integration command file names changed, but old "
"preset command files may need manual cleanup."
),
)
_register_presets_for_agent(
project_root,
key,
Expand Down
73 changes: 72 additions & 1 deletion tests/specify_cli/integrations/test_command_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,43 @@ def test_upgrade_migrates_kilocode_legacy_dir(self, tmp_path):
def test_upgrade_replaces_dotted_kiro_prompts(self, tmp_path, monkeypatch):
"""Kiro installs used to write ``.kiro/prompts/speckit.<cmd>.md``,
which Kiro CLI cannot invoke (#4797). Upgrade replaces them, including
enabled extension prompts, with ``speckit-<cmd>.md``, but a
enabled extension and preset prompts, with ``speckit-<cmd>.md``, but a
user-modified one blocks it."""
import yaml

from specify_cli.agents import CommandRegistrar
from specify_cli.integrations.base import MarkdownIntegration
from specify_cli.integrations.kiro_cli import KiroCliIntegration

preset_src = tmp_path / "custom-preset"
(preset_src / "commands").mkdir(parents=True)
(preset_src / "commands" / "speckit.fakeext.cmd.md").write_text(
"---\ndescription: Custom command\n---\nCustom preset content\n",
encoding="utf-8",
)
(preset_src / "preset.yml").write_text(
yaml.dump({
"schema_version": "1.0",
"preset": {
"id": "custom-preset",
"name": "Custom Preset",
"version": "1.0.0",
"description": "Test preset with a custom command",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"templates": [
{
"type": "command",
"name": "speckit.fakeext.cmd",
"file": "commands/speckit.fakeext.cmd.md",
}
]
},
}),
encoding="utf-8",
)

CommandRegistrar._ensure_configs()
with monkeypatch.context() as m:
m.setattr(
Expand All @@ -316,8 +347,13 @@ def test_upgrade_replaces_dotted_kiro_prompts(self, tmp_path, monkeypatch):
project = _init_project(tmp_path, "kiro-cli")
added = _run_in_project(project, ["extension", "add", "git"])
assert added.exit_code == 0, added.output
added = _run_in_project(
project, ["preset", "add", "--dev", str(preset_src)]
)
assert added.exit_code == 0, added.output
prompts = project / ".kiro" / "prompts"
assert (prompts / "speckit.git.commit.md").is_file()
assert (prompts / "speckit.fakeext.cmd.md").is_file()
dotted_plan = prompts / "speckit.plan.md"
# Bytes, not text: write_text() would turn "\n" into "\r\n" on
# Windows, so the restored file would no longer match the manifest.
Expand All @@ -335,6 +371,41 @@ def test_upgrade_replaces_dotted_kiro_prompts(self, tmp_path, monkeypatch):
assert sorted(prompts.glob("speckit.*.md")) == []
assert (prompts / "speckit-plan.md").is_file()
assert (prompts / "speckit-git-commit.md").is_file()
assert "Custom preset content" in (
prompts / "speckit-fakeext-cmd.md"
).read_text(encoding="utf-8")

@pytest.mark.parametrize(
("old_files", "new_files", "expected"),
[
(["speckit.plan.md", "speckit.tasks.md"],
["speckit-plan.md", "speckit-tasks.md"], True),
(["speckit.plan.md", "speckit.old.md"],
["speckit.plan.md", "speckit.new.md"], False),
(["speckit.plan.md"], ["speckit.plan.md", "speckit.new.md"], False),
],
)
def test_command_file_names_changed_needs_a_rename(
self, old_files, new_files, expected
):
"""Commands added and dropped in the same release are not a rename,
so upgrade must not unregister extension commands for them."""
from types import SimpleNamespace

from specify_cli.integrations._command_upgrade_layout import (
_command_file_names_changed,
)

integration = SimpleNamespace(registrar_config={"dir": ".kiro/prompts"})

def manifest(names):
return SimpleNamespace(
files={f".kiro/prompts/{name}": "hash" for name in names}
)

assert _command_file_names_changed(
integration, manifest(old_files), manifest(new_files)
) is expected

def test_upgrade_migrates_qodercli_extension_commands_to_skills(self, tmp_path):
"""Qoder upgrade retires old extension commands after skills exist."""
Expand Down
Loading