Skip to content

fix(integrations): refresh manifest hashes after init --force and switch rollback - #5193

Merged
mnriem merged 1 commit into
github:mainfrom
kartsan03:fix/resync-manifest-init-force-switch-rollback
Oct 30, 2026
Merged

mnriem merged 1 commit into
github:mainfrom
kartsan03:fix/resync-manifest-init-force-switch-rollback

Conversation

@kartsan03

Copy link
Copy Markdown
Contributor

Description

Two more paths re-register installed extensions and presets after the integration manifest is written, but never refresh the hashes of files those presets rewrite: specify init --force into an existing project, and the rollback after a failed specify integration switch. A preset that overrides a core command then leaves a stale hash, integration status reports the file as modified, and integration upgrade refuses without --force. This is the gap Copilot named on #5145 ("init --force, and switch rollback still skip them"); #5145 fixed switch and use.

init --force saves a fresh manifest right before registration, the same situation as upgrade (#4696) and a switch to a new target. It now calls _resync_manifest_after_registration with every tracked file, so files registration didn't change keep their recovered markers. After init --force the project is in sync, including one where an earlier preset command had left a stale hash.

The rollback restores an integration whose manifest was written when it was installed, like use. It now takes _snapshot_manifest_before_registration() before re-registering and refreshes only files that were unmodified before and changed by registration. A file the user edited earlier keeps its old hash and is still reported as modified. Both steps are best-effort: a missing or corrupt manifest, or a failed save, prints a warning, and the switch error stays the primary message with exit code 1.

The uninstall fallback (integration uninstall of the default integration) has the same gap. I left it out because #5100 rewrites command_uninstall.py and moves that code into _operation_uninstall.py.

Repro with the real CLI and the preset from #5078 (overrides speckit.tasks). For the rollback, a plain file named .gemini makes the Gemini install fail:

# init --force
specify init --here --integration codex --script sh --ignore-agent-tools --non-interactive --offline
specify preset add --dev ../tasks-preset
specify init --here --force --integration codex --script sh --ignore-agent-tools --non-interactive --offline
specify integration upgrade codex --script sh

# failed switch
specify init --here --integration claude --script sh --ignore-agent-tools --non-interactive --offline
specify integration install codex --script sh
specify preset add --dev ../tasks-preset
printf 'not a directory\n' > .gemini
specify integration switch gemini --script sh   # exit 1: Not a directory: '.../.gemini/commands'; codex becomes the default
specify integration upgrade codex --script sh
Step main (2a23a84) This PR
integration status after init --force WARNING, 1 modified file OK, 0 modified
upgrade codex after init --force exit 1, "1 file(s) have been modified since installation" exit 0
integration status after the failed switch WARNING, 1 modified file OK, 0 modified
upgrade codex after the failed switch exit 1, same message exit 0

Refs #5078

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

New tests, through the CLI with a real preset:

  • test_command_init.py::test_init_force_syncs_manifest_hash_for_preset_override: after init --force the hash matches the preset-rendered skill, status shows 0 modified files and upgrade without --force exits 0. init --force still replaces a user-edited skill, as on main.
  • test_command_init.py::test_init_force_keeps_recovered_markers: a recovered file that registration doesn't touch stays recovered.
  • test_command_switch.py::test_failed_switch_syncs_fallback_manifest_without_blessing_user_edits[None|plan|tasks]: after the rollback the fallback's hash matches the file; a skill the user edited before the switch (plan, which the preset doesn't touch, and tasks, which it rewrites) keeps its old hash and upgrade still refuses.
  • test_command_switch.py::test_failed_switch_manifest_refresh_is_best_effort[missing|corrupt|save-error]: registration still runs, a missing manifest isn't created, a corrupt one is left as it is, and the original switch error and exit code survive.

With the two source files reverted to main, 5 of the 8 cases fail. The tasks edit case, the missing-manifest case and the recovered-marker test pass on main too, since main never re-hashes. With the unmodified_files argument removed from the rollback, both edit cases fail with "pre-existing edits must not be recorded as pristine"; removed from init, the recovered-marker test fails. With the fallback_manifest is not None check removed, the missing and corrupt cases fail.

Full suite on Linux, Python 3.13: 13450 passed, 441 skipped (uv sync --extra test, then pytest -n 2, 9 min). Ruff 0.15.0 (ruff check src tests) passes. I haven't run it on Windows.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: Codex CLI (model: gpt-6.1-sol, reasoning effort xhigh, autonomous, offline sandbox) reproduced the paths on main and wrote the fix and the tests. Claude Code (model: claude-opus-5-5, autonomous) reviewed the change, reran the repro with the real CLI on main and the branch, ran the full suite and lint, and drafted this description. The commit carries Assisted-by: trailers for both.

…tch rollback

`specify init --force` and the rollback of a failed `integration switch`
re-register installed extensions and presets after the integration manifest
is written, but never refresh the hashes of files those presets rewrite. A
preset that overrides a core command then shows up as a modified file in
`integration status`, and `integration upgrade` refuses without `--force`.
github#5145 fixed the same gap for `switch` and `use`.

`init --force` saves a fresh manifest right before registration, so it
resyncs every tracked file, like `upgrade` and a switch to a new target.
The rollback restores an integration whose manifest was written earlier, so
it snapshots the unmodified files first, like `use`, and earlier user edits
are still reported as modified. Both steps stay best-effort.

Refs github#5078

Assisted-by: Codex CLI (model: gpt-6.1-sol, autonomous)
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
@kartsan03
kartsan03 requested a review from mnriem as a code owner October 30, 2026 01:40
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Oct 30, 2026
@mnriem
mnriem requested an auto review from Copilot October 30, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain for the manifest-hash synchronization changes.

No open issues 🎉

What changed in this PR

Closes the remaining manifest-hash gaps after preset registration in init --force and failed integration-switch rollback, extending the fixes from #5145. This prevents legitimate preset overrides from appearing as user modifications and blocking later upgrades.

Changes:

  • Refreshes hashes after forced reinitialization while preserving recovered markers on unchanged files.
  • Refreshes rollback hashes selectively, preserving pre-existing modification detection and the original switch error.
  • Adds regression coverage for preset overrides, user edits, recovered markers, and manifest failures.

Before-and-after regression results are documented in the PR; tests were not rerun here.

File Description
tests/​specify_cli/​test_command_init.py Tests preset-hash synchronization and recovered-marker preservation.
tests/​specify_cli/​integrations/​test_command_switch.py Tests rollback synchronization, user-edit detection, and manifest-failure handling.
src/​specify_cli/​integrations/​command_switch.py Snapshots and selectively refreshes fallback manifest hashes during rollback.
src/​specify_cli/​command_init.py Refreshes manifest hashes after reinitialization registration.

Review effort: Balanced (auto)

@mnriem
mnriem merged commit e232048 into github:main Oct 30, 2026
16 checks passed
@mnriem

mnriem commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-must-have Verdict: high-value, important work for Spec Kit — do first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants