You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(integrations): dispatch kiro-cli headless and install hyphenated prompts - #4798
Fixes#4797: Kiro CLI workflow dispatch and prompt names.
Dispatch. Workflow steps with integration: kiro-cli ran kiro-cli -p "<prompt>", inherited from MarkdownIntegration.build_exec_args(). Kiro CLI rejects that with error: unexpected argument '-p' found, exit 2.
KiroCliIntegration.build_exec_args() now builds kiro-cli chat --no-interactive --trust-all-tools [--model M] [--output-format stream-json] <prompt>, following kiro-cli chat --help on Kiro CLI 2.26.0:
Without --trust-all-tools, headless mode denies every write and still exits 0. It plays the role of Copilot's --yolo and Cursor's --force.
Kiro has no json output format, so output_json maps to stream-json.
SPECKIT_INTEGRATION_KIRO_CLI_EXTRA_ARGS goes before the prompt.
Prompt names. Kiro CLI rejects /speckit.constitution and runs /speckit-constitution. So, like Junie (#4073) and Cline:
Prompts install as .kiro/prompts/speckit-<command>.md.
Dispatch and shared templates use /speckit-<command>, and extension and preset prompts get the same names.
specify integration upgrade kiro-cli replaces the core speckit.*.md prompts through the manifest. A modified one stops the upgrade until --force.
Each registration pass (use, switch, upgrade of the active integration) writes an extension's hyphenated prompts. It then removes each dotted prompt whose replacement exists, the step Qoder's skills migration uses (_retire_legacy_flat_extension_commands, [bug-fix] Fix qodercli-skills-migration: migrate QodercliIntegration to SkillsIntegration #4205). A command the manifest still declares but that isn't written keeps its old prompt and stays tracked, so extension remove still deletes it.
Upgrade refuses the rename, before changing files, when:
presets have commands registered for Kiro;
an extension has registered a prompt where a core prompt goes, as with an alias such as speckit-plan, which Spec Kit 1.0.7 and earlier accepted;
the extension registry, or an entry's registered_commands, can't be read, so that can't be checked (also with --force).
A file nothing installed at a new core prompt name, such as a prompt written by hand, is replaced only with --force, and a symlink there never is.
Name collisions.speckit.foo.bar-baz and speckit.foo-bar.baz both become speckit-foo-bar-baz.
Install and update reject a command or alias that writes the same file as a core command or another installed extension once dots become hyphens. A command's own alias may share its file. An installed extension counts with its manifest's names and the names the registry still tracks for it, which covers a manifest that can't be read or no longer declares a name. Install and update refuse while the registry can't be read, or while an installed extension has neither a readable manifest nor readable registered commands.
Once one of the pair is removed, the other migrates.
Extension removal, and the extension cleanup in integration switch and uninstall, delete a file at a core command's name, an old flat file (Qoder keeps them in .qoder/commands), or a file another extension also writes, only when its first <!-- Extension: <id> --> marker (or SKILL.md metadata.source) names the extension. Core commands the integration manifest tracks are always kept.
Detection.specify check and specify init accepted a bare kiro, which launches Kiro IDE by default. Kiro IDE 1.2.4 exits 0 on the headless argv without running anything. Detection now looks for kiro-cli only, like dispatch, the workflow preflights and the devcontainer.
Docs. The Kiro row in docs/reference/integrations.md covers names, dispatch, detection, the upgrade refusals and older collisions. docs/reference/extensions.md covers the install refusal and older collisions on Kiro CLI and Qoder CLI. The row drops "Alias: --integration kiro", because specify init --integration kiro is rejected.
Headless Kiro still drops text after /speckit-<name>. The existing prose fallback covers that (#1926).
A 1.1.0 project with git ends with 15 speckit-*.md prompts in each of these cases:
a plain upgrade;
with Kiro secondary, the upgrade and then integration use kiro-cli;
with a preset, preset remove, the upgrade and preset add, which leaves the override in speckit-plan.md.
A 1.1.0 project with speckit.foo.bar-baz (plus speckit.foo.other) and speckit.foo-bar.baz. At 3bcee7e the upgrade lost foo's body and deleted both dotted prompts. Here both extensions are skipped, the dotted prompts are byte-identical, and foo migrates after extension remove foo-bar.
A 1.0.7 project with alias speckit-plan, dev-installed so the prompt is a symlink. At 3bcee7e the upgrade overwrote the core prompt and extension remove deleted it. Here the upgrade is refused, and after extension remove old it writes a regular core speckit-plan.md. With alias plan instead, extension remove old keeps the core prompt.
A 0.16.5 Qoder project with the same pair. On main the upgrade wrote one body into the shared skill and deleted both old commands. Here both extensions are skipped, extension remove foo-bar deletes its old command, and the next upgrade migrates foo.
Testing
Tested locally with uv run specify --help
Ran existing tests with uv sync && uv run pytest
Tested with a sample project (if applicable)
tests/integrations/test_integration_kiro_cli.py: argv, names, dispatch, hook note, handoffs, and the IDE launcher.
collisions: a pair left unregistered, then migrated; a disabled or unreadable owner; enable; alias plan; a Qoder pair, then removing one; a command's own alias; force reinstall.
ownership: an unreadable or malformed extension registry, a prompt nothing installed at a new core name, names the registry still tracks, core prompts without manifest tracking on removal and switch, Qoder old commands, and a marker quoted in a prompt's body.
tests/test_extensions.py: install rejection, including an owner whose manifest can't be read, a name only the registry still tracks, and unreadable registry state.
On main's source, 27 of these tests fail and the Kiro module fails to import.
Each guard has a test that fails without it:
registration skip off: 6 collision tests fail;
no upgrade refusal, or removal judging core files by name: the speckit-plan test fails;
no core files kept on removal: the plan test fails;
no fallback for unreadable manifests: the unreadable-owner install test fails;
no removal of old flat files: the Qoder test fails.
Full suite: 9559 passed, 265 skipped (Linux, Python 3.13). ruff check src tests (0.15.0) and markdownlint: clean.
AI Disclosure
I did not use AI assistance for this contribution
I did use AI assistance (fill in the disclosure below)
AI disclosure: Claude Code (Claude Opus 5.5, autonomous agent mode) was used to investigate Kiro CLI and Kiro IDE, run the reproductions, and write the code changes, the tests and this description. Codex CLI (model: gpt-6-astra, reasoning effort xhigh, autonomous, sandboxed and offline) wrote base-vs-PR regression tests for the ownership checks and made the fixes in 24630bc, which Claude Code reviewed and verified. Commits carry Assisted-by: trailers. a80126f was pushed by @mnriem.
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 github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Pushed 86afed7, which adds the second half of #4797: Kiro prompts are now hyphenated. The assessment on #4797 says the argv change alone is not a complete fix, and I agree. With only 88c9c13, a workflow step sends /speckit.constitution, and Kiro answers that it "is not a built-in Kiro CLI slash command". The step then passes only because the model searches for and reads .kiro/prompts/speckit.constitution.md on its own.
format_kiro_command_name and the command_filename, build_command_invocation and invoke_separator = "-" overrides, so shared templates and scripts render /speckit-plan.
format_name in registrar_config, so extension and preset prompts get the same names. The bundled git extension installs speckit-git-commit.md.
This also resolves the Copilot finding. The build_exec_args docstring now says a /speckit-* input runs the prompt file, which is true once the files are hyphenated.
New tests:
the formatter, filenames and invocation;
a CommandStep dispatch test asserting the exact Kiro argv with /speckit-constitution;
the hook note and handoffs;
hyphenated overrides of the base Markdown inventory tests;
test_upgrade_replaces_dotted_kiro_prompts.
12 of these fail with the previous kiro_cli/__init__.py.
docs/reference/integrations.md: the Kiro row now says prompts are hyphenated and why.
Answers to the assessment's open questions
Output format. Workflow command steps call dispatch_command() with the default stream=True, and prompt steps pass output_json=False. So Kiro gets no --output-format and prints text, which the runner streams without parsing. If a caller does ask for JSON, output_json=True maps to --output-format stream-json (JSON Lines), since Kiro has no plain json format.
--trust-all-tools. It's always added. Headless Kiro can't ask for approval, so without it every write is denied while the run still exits 0. A workflow step would then report success with nothing written. Copilot's --yolo and Cursor's --force do the same job in their integrations.
Migration.specify integration upgrade kiro-cli stale-removes the dotted prompts through the existing manifest contract.
I ran it on a project initialized with the code before this PR. Output: "Removed 10 stale file(s) from previous install", and all 10 prompts are now speckit-*.md.
If a dotted prompt was modified, the upgrade stops and lists it, and the file stays until --force is used. test_upgrade_replaces_dotted_kiro_prompts covers both cases.
Kiro dispatch exited 2 before this PR, so nothing that worked before depended on the dotted names being dispatched.
Versions. I tested only Kiro CLI 2.26.0, the current stable download, and added no version gate.
Real run (Kiro CLI 2.26.0, specify workflow run with a speckit.constitution step and a shell step)
With this commit, Kiro expands the prompt itself. The model's first tool calls are the prompt's own steps: the extensions.yml hook check and resolve-template.sh. It never opens .kiro/prompts, and the run ends Status: completed.
The full suite passes: 8771 passed, 251 skipped. ruff check src tests is clean.
One correction: 88c9c13 is missing the Assisted-by: trailer. The same agent made it, and 86afed7 carries the trailer.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent wrote the code, tests and this comment, and ran the Kiro CLI and upgrade checks above.
I updated the title and description for Copilot's scope finding. They now cover the prompt rename, the integration upgrade migration of the old speckit.*.md files, and the docs row, and they say Fixes #4797 because both halves are in this PR. The code is unchanged since 86afed7.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent rewrote the PR title and description and wrote this comment.
test_upgrade_replaces_dotted_kiro_prompts restored the edited prompt
with write_text(), which writes CRLF on Windows. Integration files are
written as LF bytes, so the restored file no longer matched its
manifest hash and the second upgrade was still blocked as modified
(pytest on windows-latest). Read and write the prompt as bytes.
Refs github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Fixed the Windows pytest failure in cb898ce. The problem was in the test I added, not the Kiro change.
test_upgrade_replaces_dotted_kiro_prompts edits .kiro/prompts/speckit.plan.md to check that upgrade stops on a modified prompt, then restores it. It restored the file with write_text(), which writes CRLF on Windows. write_file_and_record() writes LF bytes, so the restored file no longer matched its manifest hash, and the second upgrade was still blocked. The test now reads and writes the prompt as bytes.
The macOS 3.13 job was cancelled by fail-fast after the Windows failure; it didn't fail itself. The upgrade and Kiro test modules pass locally (75).
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent diagnosed the CI failure from the job logs, fixed the test and wrote this comment.
Existing extension and preset prompts are not migrated. On upgrade, this formatter makes re-registration write new hyphenated files, but command_upgrade.py:314-345 only unregisters old registrations when the command directory changes; Kiro's directory remains .kiro/prompts. Those artifacts are tracked outside the integration manifest, so their old speckit.*.md copies remain beside the replacements. Add same-directory naming-migration cleanup before re-registration and cover an upgrade with an installed extension and preset.
Document that headless Kiro workflow dispatch always passes --trust-all-tools, which auto-approves tool use. This is a security-relevant runtime default introduced by this PR; unlike the MiniMax row below, the current Kiro row only describes prompt naming and argument substitution, so users cannot discover the permission behavior from the integration reference.
Upgrade only unregistered enabled extension commands when the command
directory changed. Kiro keeps .kiro/prompts but renamed its files, and
extension prompts are tracked in the extension registry rather than the
manifest, so upgrading a project with the git extension left the five
speckit.git.*.md prompts beside the new speckit-git-*.md ones. Treat a
same-directory rename of the core command files like a directory change,
so the existing cleanup removes them before re-registration.
Also document that headless Kiro dispatch passes --trust-all-tools.
Refs github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Extension prompts on upgrade. Confirmed and fixed. I initialized a project with the code before this PR and added the bundled git extension, then ran specify integration upgrade kiro-cli on the branch. The 10 core prompts were replaced, but the five speckit.git.*.md prompts stayed beside the new speckit-git-*.md ones. That happened because upgrade only calls _unregister_enabled_extension_commands_for_agent() when the command directory changes. _command_file_names_changed() now also triggers it when the core command files were renamed inside the same directory. unregister_commands() already removes both the formatted name and the raw registered name, so the existing cleanup deletes the dotted files before re-registration. The real upgrade now leaves 15 speckit-*.md prompts and no dotted ones. test_upgrade_replaces_dotted_kiro_prompts now installs the git extension under the old naming and fails without the fix.
Preset prompts. I didn't reproduce a leftover here. With the bundled lean preset installed under the old naming, upgrade --force (the preset overrides make a plain upgrade stop as "modified", same as on main) left the lean content in the hyphenated core prompts and no dotted copies.
--trust-all-tools. The Kiro row in docs/reference/integrations.md now says headless dispatch runs kiro-cli chat --no-interactive --trust-all-tools, which auto-approves every tool call, and why.
The full suite passes: 8771 passed, 251 skipped. ruff check src tests and markdownlint are clean.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent reproduced the upgrade cases, made the change and wrote this comment.
_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)
Both findings from the latest Copilot pass are addressed in 569a1bd.
Rename detection._command_file_names_changed() no longer treats "some files added, some removed" as a rename. It only fires when a removed command file and an added one have the same name once . and - are treated as the same separator, as with speckit.plan.md → speckit-plan.md. A release that adds speckit.new.md and drops speckit.old.md no longer unregisters extension commands. test_command_file_names_changed_needs_a_rename covers the rename case, the add-and-drop case, and an add-only case.
Preset commands. When that rename happens on the active integration, upgrade now calls _unregister_presets_for_agent() before the existing _register_presets_for_agent(), the same pairing switch uses. test_upgrade_replaces_dotted_kiro_prompts now installs a preset with a custom speckit.fakeext.cmd command under the old naming. After the upgrade, it checks that no speckit.*.md prompt is left and that speckit-fakeext-cmd.md has the preset content. Without the change, that test fails with speckit.fakeext.cmd.md still in .kiro/prompts.
tests/specify_cli/integrations, tests/integrations and tests/specify_cli/presets pass (3668 passed, 6 skipped), and ruff is clean on the changed files.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent reproduced both findings, made the change and wrote this comment.
Hyphenation is not injective, so speckit.foo.bar-baz and speckit.foo-bar.baz both become speckit-foo-bar-baz, and an alias can land on the same file.
Install now rejects that pair, including an alias of one command that hyphenates onto another command or onto a core prompt. A command's own alias may still reuse its hyphenated name, because both render the same body. speckit.foo.plan stays legal: it is not the core plan command.
A pair that is already installed is left in place. Registration warns, does not write either prompt, and does not retire either dotted file. The extension's other commands still migrate, and the core rename still runs. I did not fail the whole upgrade. The review asked to refuse the migration or otherwise preserve ownership, and preserve keeps the core rename independent of one colliding extension. extension enable does refuse, before it flips the flag.
Removal writes the remaining owner's source back into the shared file when that source can be read, then deletes only the removed command's own file. If the source cannot be read, the shared file stays. Dropping a command the manifest no longer declares does the same rewrite, including when the other owner is disabled and this pass does not register it. A file only the dropped command owned is still deleted.
Preset registration skips a colliding command, and the reconcile step no longer writes it afterwards. An exact override of a core command, or of an extension command's own name, still writes. Skills follow the same preserve-and-rewrite rule.
use and switch still do not run integration.setup, so a dotted core prompt such as speckit.plan.md is renamed by integration upgrade, not by selecting Kiro.
Full suite: 9063 passed, 250 skipped (Linux, Python 3.13). ruff check src tests (0.15.0) is clean.
@mnriem Please re-run Copilot on 62732e8. The fork workflows will need approval before CI runs.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent wrote the code, the tests and this comment.
main's github#4823 pins the zip entry time in the catalog test archive helper,
so this branch's copy of that fix (ed42004) is dropped in favour of
main's version of tests/specify_cli/workflows/test_catalog_versions.py.
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
…riting
62732e8 kept extension commands that hyphenate to one prompt
(speckit.foo.bar-baz and speckit.foo-bar.baz) by rewriting the shared
file from whichever owner remained, across removal, enable, presets and
skills. That rewriting is replaced by rules that leave such files alone.
Install and update still reject a command or alias that writes the same
file as a core command or another installed extension once dots become
hyphens. For a pair installed before that check, or an alias such as
plan from Spec Kit 1.0.7 and earlier, registration for Kiro CLI and
Qoder skips the extension through the per-extension error path from
github#2950. It writes and retires nothing, the registry stays as it was, and
the warning names the other writer. Once one extension of a pair is
removed, the other migrates on the next pass.
An alias such as speckit-plan has its prompt where the renamed core
speckit.plan goes, so integration upgrade refuses the rename while one
is registered, as it does for preset overrides. Extension removal keeps
a core command's file only when the integration manifest tracks it:
before the rename it deletes that alias's own prompt, so a dev-mode
symlink is not left dangling.
Presets and extension enable are back to main's code.
Refs github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Pushed 8570f0a. It replaces the collision handling from 62732e8 instead of patching it.
62732e8 kept colliding commands working by rewriting the shared prompt from whichever owner remained, in removal, enable, presets and skills. Each path needed its own ownership rules, and the skill restoration missed the other agents' directories (r4185677037). The new rule never writes a shared file:
Install and update still reject names that write the same file once dots become hyphens.
A pair installed before that check is not registered for Kiro CLI or Qoder. Nothing is written, retired or untracked, and the warning names the other writer. The other extension migrates once one of them is removed.
integration upgrade refuses the rename while an older alias such as speckit-plan already has its prompt where the core prompt goes, as it does for preset overrides.
Extension removal keeps a core command's file only when the integration manifest tracks it.
presets/_manager_commands.py and extensions/command_enable.py are back to main. The source diff against main went from +1341/−46 in 9 files to +611/−32 in 7.
I also merged main. #4823 already pins the zip entry time, so this branch's _archive() change is gone.
Projects created with Spec Kit 1.1.0 and 1.0.7 were upgraded on this branch (details in the description):
Colliding pair (1.1.0): at 3bcee7e the upgrade lost one extension's body. Here both stay byte-identical.
speckit-plan alias (1.0.7): at 3bcee7e the upgrade overwrote the core prompt and extension remove then deleted it. Here the upgrade is refused first, and after the extension is removed it writes the core prompt.
Full suite: 9484 passed, 265 skipped. ruff and markdownlint: clean.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent made the change, ran the upgrade comparisons above and wrote this comment.
…oder commands
The install check built its name map from readable manifests only, so an
installed extension whose extension.yml can't be read did not count, and a
colliding install could overwrite its files. Fall back to the command names
the registry tracks for it.
Registration leaves a skipped extension's old flat commands in place. For
Qoder they sit in .qoder/commands, outside the registrar's directory, so
extension removal left them behind. Removal now deletes the tracked old
flat files too (never a core command's own), reusing the registration-time
retirement step without its replacement check.
Refs github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Pushed fdb8d75 with fixes for both findings from the last review:
An installed extension whose manifest can't be read now counts at install and update, with the command names the registry tracks for it (r4194890103).
Extension removal deletes the extension's tracked old flat commands. A Qoder extension that registration skipped no longer leaves .qoder/commands/*.md behind, and a core command's own old file is never removed (r4194890191).
Real run with a 0.16.5 Qoder project holding speckit.foo.bar-baz and speckit.foo-bar.baz:
On main, the upgrade merged them into one skill and deleted both old commands.
Here, both are skipped, extension remove foo-bar deletes its old command, and the next upgrade migrates foo.
Full suite: 9485 passed, 265 skipped. ruff: clean.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). The agent made the change, ran the Qoder comparison above and wrote this comment.
Ready for review as is. The last Copilot pass on fdb8d75 raised no new findings and resolved the two from the previous round. I replied on the remaining collision thread (r4184204489) with the fix and resolved it, so no Copilot threads are open.
For the closer look Copilot asked for, the cross-cutting parts are:
Install and update: _validate_install_conflicts rejects a command or alias that hyphenates to a core command's file or another extension's file. Extensions whose manifest can't be read count by their registered names.
Upgrade (command_upgrade.py): the Kiro rename is refused while presets have Kiro commands, or while an old alias such as speckit-plan owns a core prompt's new file name.
Removal (ExtensionManager.remove): core files are kept only when the integration manifest tracks them, and the extension's old flat files are deleted.
Real runs on projects from 1.1.0, 1.0.7 and 0.16.5 (Qoder) are in the description, each compared with the previous behaviour. Full suite: 9485 passed, 265 skipped.
Verify ownership before deleting colliding Kiro prompts or Qoder skills. Protect formatted and raw aliases, preserve ambiguous files with warnings, and continue retiring the removed extension's legacy commands. Add 36 regression cases covering both owners and invalid ownership markers.
Validation: full pytest run completed with 9766 passed, 19 skipped, and one pre-existing preset CLI help-text assertion failure reproduced on untouched fdb8d75. CI-pinned Ruff passed across src and tests.
Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Pushed a80126f to fix/kiro-cli-dispatch. Removal now verifies ownership of shared migrated Kiro prompts and Qoder skills, protects formatted and raw aliases, and preserves ambiguous or unreadable shared files with a warning. The removed extension’s own legacy commands are still retired. Added 36 regression cases covering both removal orders, enabled/disabled/unreadable manifests, and missing or invalid ownership markers.
Validation:
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests -q: 9,766 passed, 19 skipped, 1 failed. The failure is tests/integration/test_preset_update_workflow.py::test_preset_update_cli_contract, a preset CLI help-text assertion. It also fails on an untouched export of fdb8d75 with the same dependencies; no unrelated preset changes were made.
uvx ruff@0.15.0 check src tests: passed.
git diff --check: passed before commit.
Authored and posted on behalf of @mnriem by GitHub Copilot, model GPT-6.1 Sol, default reasoning settings, autonomous implementation and validation under maintainer direction. AI involvement: code, regression tests, documentation, commit, and this summary.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The migration changes shared artifact ownership and removal behavior across multiple integrations, warranting final human review despite extensive coverage.
The collision fix covers the original case, but two ownership gaps remain:
Unreadable ownership metadata: a corrupt extension registry or malformed registered_commands entry lets upgrade overwrite an existing extension alias at speckit-plan.md, without --force. Please fail closed before writing when ownership cannot be verified.
Stale registered names: collision detection ignores previously registered names when an enabled extension’s manifest changes. Installing another command with the same hyphenated output then removing the original extension can delete the new owner’s prompt. Please include outstanding registrations and verify ownership before deletion.
Three additional regression cases pass against the base source and fail on this PR.
…known
Upgrade checked the extension registry for an alias such as speckit-plan
at a renamed core prompt's path, but skipped a registry it couldn't parse
and entries whose registered_commands wasn't a list of names. The rename
then wrote the core prompt over the alias's file, even without --force.
Read the registry the way the preset guard does and refuse before
changing files when it or an entry is malformed. A file at a new core
name that nothing tracks, such as a prompt written by hand, is replaced
only with --force, and a symlink there never is.
Install and the shared-file checks used an enabled extension's manifest
names only. A name the registry still tracks after the manifest stopped
declaring it keeps its prompt on disk, and removing that extension
deletes it. So another extension could install a command writing the
same file, and removing the first one deleted the new owner's prompt.
Registered names now count at install, and in the claims that
registration and removal's ownership check read.
Refs github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
…file
Removal and integration cleanup still deleted files they couldn't show
were the extension's:
- A core prompt at an old alias's new name (plan -> speckit-plan.md) was
kept only while the integration manifest tracked it. With the manifest
missing or no longer listing it, extension remove deleted it, and
integration switch deleted it right after uninstall had kept it as
modified, because unregister_agent_artifacts passed no preserved names.
- Removal deleted every registered name's old Qoder command in
.qoder/commands, including a file another owner or the user wrote.
- The shared-file check accepted any line equal to the extension's marker,
so a prompt whose body quoted that line counted as the extension's.
Ownership is now read from the first marker, where registration writes
it (or SKILL.md metadata.source), in one helper. A core file the manifest
doesn't track, and an old flat command on removal, are deleted only when
that marker names the extension; integration cleanup passes the same
preserved set as removal. Test fixtures that planted these files now
carry the marker registration writes.
Install and update also refuse while the registry can't be read, or while
an installed extension has neither a readable manifest nor readable
registered commands, since its names can't be checked.
Refs github#4797
Assisted-by: Codex CLI (model: gpt-6-astra, autonomous)
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
…er only
3bcee7e kept a registered name whose command source is missing, so the
dotted Kiro prompt it still has on disk stays tracked. That applied to
every agent, but only Kiro CLI and Qoder check who owns a shared file on
removal. In Claude, Codex, Cline, Junie or Forge, an older pair such as
speckit.foo.bar-baz and speckit.foo-bar.baz shares one file; when foo's
source went missing, foo kept its name, foo-bar rewrote the file, and
removing foo deleted foo-bar's prompt. Other agents now drop an unwritten
name as before.
Refs github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Both gaps are fixed in cce66b5, and 24630bc, c68b384 and 41ed86c fix more cases of the same kind. I found those by running the same kind of check as yours: tests that pass on the base source and fail on a80126f.
1. Unreadable ownership metadata (cce66b5). Before the rename writes any file, upgrade now reads .specify/extensions/.registry the way the preset guard reads the preset registry. It refuses, even with --force, when the registry can't be parsed, when an entry isn't a mapping, or when registered_commands or its kiro-cli value isn't a list of names. The message says to fix or restore the registry. Prompt files and the manifest stay byte-identical. A file at a new core prompt name that nothing tracks, such as a speckit-plan.md someone wrote because Kiro ignored the dotted names, is replaced only with --force. A symlink there isn't replaced at all.
2. Stale registered names (cce66b5). An installed extension now counts at install and update with both its manifest's names and the names the registry still tracks for it. Registration's shared-file check and removal's ownership check count them too. In your scenario, speckit.foo-bar.baz is refused at install because it writes the speckit-foo-bar-baz.md that foo still tracks. If such a file was written before this check, removing foo reads its marker and keeps it.
Core prompts by name. With alias plan (1.0.7 and earlier) and a Kiro or Qoder integration manifest that's missing or no longer lists the file, extension remove deleted the core speckit-plan.md. integration switch claude deleted a user-modified core prompt right after the uninstall step had kept it as modified, because unregister_agent_artifacts() passed no preserved names. A core file the manifest doesn't track is now deleted only when its marker names the extension. Integration cleanup passes the same preserved set as remove().
Qoder old commands. Removal deleted .qoder/commands/<name>.md for every registered name, including a file the user or another extension wrote. It now deletes only files carrying the extension's marker.
Marker position. The ownership check accepted any line equal to the marker, so a prompt whose body quoted <!-- Extension: foo --> counted as foo's. It now reads the first marker, where registration writes it.
Install with unreadable state. Install and update refuse while the registry can't be read, or while an installed extension has neither a readable manifest nor readable registered commands. Before, the registry loaded as empty and the new extension could write over an installed one's prompt. This refusal applies to every integration.
Missing sources outside Kiro and Qoder (c68b384).3bcee7e keeps a registered name whose command source is missing, for every agent. But only Kiro CLI and Qoder check who owns a shared file on removal. In Claude, Codex, Cline, Junie and Forge, an older pair such as speckit.foo.bar-baz and speckit.foo-bar.baz already shares one file on main. Once foo's source went missing, the next registration rewrote the file as foo-bar's, and extension remove foo deleted it. Now only Kiro CLI and Qoder keep such a name; other agents drop it, as main does.
Disabled owner with an unreadable entry (41ed86c). A disabled extension claimed only the names its registry entry lists for the agent. With that entry unreadable, it claimed nothing, so integration use, switch or upgrade registered another extension's command over its prompt. It now claims its manifest's names in that case, and registration skips the pair.
Extension prompts have carried <!-- Extension: <id> --> since the extension system shipped. So files from real older installs are still removed. Some fixtures planted these files without the marker, and I added the marker where registration writes it, without changing their assertions: test_upgrade_refuses_while_an_extension_prompt_has_a_core_prompt_name, test_qoder_upgrade_leaves_extensions_that_share_a_skill_unregistered, and the legacy files in your test_removing_colliding_extension_preserves_other_owners_migrated_file. All three pass with the marker on a80126f too.
Tests. 12 new tests, 38 cases. On a80126f's source, 32 fail; the other 6 are controls (an owned Qoder command is still removed, a readable manifest still lets an unrelated install through, and uninstall keeps a modified core prompt). All pass here. The base-vs-PR checks I used for the two gaps:
corrupt registry, or registered_commands as [], a string, or a list of non-strings, then upgrade kiro-cli: the speckit-plan alias prompt keeps its body;
foo installed with speckit.foo.bar-baz, its manifest renamed to speckit.foo.other, extension add of speckit.foo-bar.baz, then extension remove foo: foo-bar's prompt survives (here the add is refused).
Each passes on the base source, fails on a80126f and passes on 41ed86c.
Full suite: 9559 passed, 265 skipped. ruff check src tests (0.15.0) and markdownlint: clean. The description is updated.
Drafted on behalf of @kartsan03 by Claude Code (model: claude-opus-5-5, autonomous). Codex CLI (model: gpt-6-astra, reasoning effort xhigh, autonomous, sandboxed and offline) wrote the base-vs-PR regression tests that found the cases in 24630bc, and made those fixes. Claude Code made cce66b5, c68b384 and 41ed86c, reviewed Codex's diff, reran every test against a80126f and this branch, ran the full suite and wrote this comment.
A disabled extension claimed only the names its registry entry lists for
the agent, because its manifest's commands aren't registered. When that
entry can't be read (registered_commands or its per-agent value isn't a
list of names), it claimed nothing, so integration use, switch or upgrade
registered another extension's command over its prompt, e.g.
speckit.foo.bar-baz over a disabled speckit.foo-bar.baz. It now claims
its manifest's names in that case, and registration skips the pair.
Refs github#4797
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
triage-can-waitVerdict: valid and in-scope but deprioritized; held behind the evidence gate
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #4797: Kiro CLI workflow dispatch and prompt names.
Dispatch. Workflow steps with
integration: kiro-clirankiro-cli -p "<prompt>", inherited fromMarkdownIntegration.build_exec_args(). Kiro CLI rejects that witherror: unexpected argument '-p' found, exit 2.KiroCliIntegration.build_exec_args()now buildskiro-cli chat --no-interactive --trust-all-tools [--model M] [--output-format stream-json] <prompt>, followingkiro-cli chat --helpon Kiro CLI 2.26.0:--trust-all-tools, headless mode denies every write and still exits 0. It plays the role of Copilot's--yoloand Cursor's--force.jsonoutput format, sooutput_jsonmaps tostream-json.SPECKIT_INTEGRATION_KIRO_CLI_EXTRA_ARGSgoes before the prompt.Prompt names. Kiro CLI rejects
/speckit.constitutionand runs/speckit-constitution. So, like Junie (#4073) and Cline:.kiro/prompts/speckit-<command>.md./speckit-<command>, and extension and preset prompts get the same names.Migration.
specify integration upgrade kiro-clireplaces the corespeckit.*.mdprompts through the manifest. A modified one stops the upgrade until--force.use,switch, upgrade of the active integration) writes an extension's hyphenated prompts. It then removes each dotted prompt whose replacement exists, the step Qoder's skills migration uses (_retire_legacy_flat_extension_commands, [bug-fix] Fix qodercli-skills-migration: migrate QodercliIntegration to SkillsIntegration #4205). A command the manifest still declares but that isn't written keeps its old prompt and stays tracked, soextension removestill deletes it.speckit-plan, which Spec Kit 1.0.7 and earlier accepted;registered_commands, can't be read, so that can't be checked (also with--force).--force, and a symlink there never is.Name collisions.
speckit.foo.bar-bazandspeckit.foo-bar.bazboth becomespeckit-foo-bar-baz.Install and update reject a command or alias that writes the same file as a core command or another installed extension once dots become hyphens. A command's own alias may share its file. An installed extension counts with its manifest's names and the names the registry still tracks for it, which covers a manifest that can't be read or no longer declares a name. Install and update refuse while the registry can't be read, or while an installed extension has neither a readable manifest nor readable registered commands.
Projects can predate that check, with a pair like the one above or an alias such as
planfrom 1.0.7 and earlier. Registration for Kiro CLI and Qoder then skips the extension through the per-extension error path from register_enabled_extensions_for_agent has no per-extension error isolation: one failing extension silently drops the rest #2950:Once one of the pair is removed, the other migrates.
Extension removal, and the extension cleanup in
integration switchanduninstall, delete a file at a core command's name, an old flat file (Qoder keeps them in.qoder/commands), or a file another extension also writes, only when its first<!-- Extension: <id> -->marker (or SKILL.mdmetadata.source) names the extension. Core commands the integration manifest tracks are always kept.Detection.
specify checkandspecify initaccepted a barekiro, which launches Kiro IDE by default. Kiro IDE 1.2.4 exits 0 on the headless argv without running anything. Detection now looks forkiro-clionly, like dispatch, the workflow preflights and the devcontainer.Docs. The Kiro row in
docs/reference/integrations.mdcovers names, dispatch, detection, the upgrade refusals and older collisions.docs/reference/extensions.mdcovers the install refusal and older collisions on Kiro CLI and Qoder CLI. The row drops "Alias:--integration kiro", becausespecify init --integration kirois rejected.Headless Kiro still drops text after
/speckit-<name>. The existing prose fallback covers that (#1926).Real runs.
mainfails as above. This branch expands/speckit-constitution, writes.specify/memory/constitution.mdand ends withStatus: completed.speckit-*.mdprompts in each of these cases:integration use kiro-cli;preset remove, the upgrade andpreset add, which leaves the override inspeckit-plan.md.speckit.foo.bar-baz(plusspeckit.foo.other) andspeckit.foo-bar.baz. At 3bcee7e the upgrade lostfoo's body and deleted both dotted prompts. Here both extensions are skipped, the dotted prompts are byte-identical, andfoomigrates afterextension remove foo-bar.speckit-plan, dev-installed so the prompt is a symlink. At 3bcee7e the upgrade overwrote the core prompt andextension removedeleted it. Here the upgrade is refused, and afterextension remove oldit writes a regular corespeckit-plan.md. With aliasplaninstead,extension remove oldkeeps the core prompt.mainthe upgrade wrote one body into the shared skill and deleted both old commands. Here both extensions are skipped,extension remove foo-bardeletes its old command, and the next upgrade migratesfoo.Testing
Tested locally with
uv run specify --helpRan existing tests with
uv sync && uv run pytestTested with a sample project (if applicable)
tests/integrations/test_integration_kiro_cli.py: argv, names, dispatch, hook note, handoffs, and the IDE launcher.tests/specify_cli/integrations/test_command_upgrade.py:use,switchandenableafter the rename;plan; a Qoder pair, then removing one; a command's own alias; force reinstall.switch, Qoder old commands, and a marker quoted in a prompt's body.tests/test_extensions.py: install rejection, including an owner whose manifest can't be read, a name only the registry still tracks, and unreadable registry state.On
main's source, 27 of these tests fail and the Kiro module fails to import.Each guard has a test that fails without it:
speckit-plantest fails;plantest fails;The ownership tests added in cce66b5, 24630bc, c68b384 and 41ed86c (38 cases): 32 fail on a80126f's source, and 6 are controls.
Full suite: 9559 passed, 265 skipped (Linux, Python 3.13).
ruff check src tests(0.15.0) and markdownlint: clean.AI Disclosure
AI disclosure: Claude Code (Claude Opus 5.5, autonomous agent mode) was used to investigate Kiro CLI and Kiro IDE, run the reproductions, and write the code changes, the tests and this description. Codex CLI (model: gpt-6-astra, reasoning effort xhigh, autonomous, sandboxed and offline) wrote base-vs-PR regression tests for the ownership checks and made the fixes in 24630bc, which Claude Code reviewed and verified. Commits carry
Assisted-by:trailers. a80126f was pushed by @mnriem.