fix: sync integration manifest hashes after preset re-registration on upgrade - #4697
chelsealong wants to merge 3 commits into
Conversation
… upgrade specify integration upgrade <agent> --force ran _register_presets_for_agent() after new_manifest.save(), so a preset overriding a core command rendered as a skill (e.g. speckit.tasks) overwrote the file on disk without updating its recorded hash. integration status then reported the legitimate override as manually modified, and a subsequent upgrade without --force was blocked. Resync tracked-file hashes into the manifest right after extensions/presets are re-registered for the active integration, then persist the refresh. Fixes github#4696
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Expected per-file rehash failures are silently ignored, potentially leaving stale hashes without warning.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Fixes manifest hash drift after preset/extension re-registration during integration upgrades.
Changes:
- Re-hashes tracked files after registration.
- Adds regression coverage for preset-overridden skills.
- Verifies clean integration status afterward.
| File | Description |
|---|---|
src/specify_cli/integrations/_helpers.py |
Adds manifest resynchronization helper. |
src/specify_cli/integrations/command_upgrade.py |
Runs resynchronization after registration. |
tests/specify_cli/integrations/test_command_upgrade.py |
Adds regression test for issue #4696. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…pping _resync_manifest_after_registration() silently discarded ValueError/OSError for individual files, leaving a stale hash with no signal to the user. Emit the existing best-effort warning per failed file and keep resyncing the rest.
|
Addressed the inline comment: Added |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Filesystem probe errors can prematurely stop resynchronization and leave later manifest hashes stale.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
Resolved since last review (1)
Move the is_symlink()/is_file() probes in _resync_manifest_after_registration() inside the per-file try so an OSError from either (e.g. an inaccessible path) is caught by the existing best-effort warning-and-continue handler instead of the outer handler, which was aborting the whole resync and leaving later files' hashes stale.
|
Posted on behalf of @chelsealong by Claude Code (model: claude-sonnet-5, autonomous); comment fully AI-drafted. Addressed: the |

Summary
Fixes #4696.
specify integration upgrade <agent> --forcecallsnew_manifest.save()before
_register_presets_for_agent()re-registers enabled presets for theactive integration. When a preset overrides a core command that renders as a
skill file (e.g.
speckit.tasks→.claude/skills/speckit-tasks/SKILL.md),that re-registration step overwrites the file on disk with the preset's
content, but the manifest that was just saved still holds the base template's
hash for that path.
The observable effect: right after a successful
integration upgrade --force,specify integration statusreports the legitimately-installed presetoverride as a manually modified file (
managed-files-modified), and asubsequent
specify integration upgrade <agent>without--forceisincorrectly blocked as if the user had hand-edited the file.
Fix
In
src/specify_cli/integrations/command_upgrade.py, after extensions andpresets are re-registered for the active integration, re-hash every file the
manifest already tracks that still exists as a regular (non-symlink) file on
disk, and persist the refreshed manifest. Nothing else touches the project
between the initial
new_manifest.save()and this point in the same commandinvocation, so any hash mismatch found here was produced by our own
registration step, not by user tampering — re-baselining it is safe and
preserves tamper detection for anything modified before the upgrade ran
(that path is still guarded by the existing
check_modified()/--forcecheck earlier in the command).
The new helper
_resync_manifest_after_registration()in_helpers.pyisbest-effort, matching the existing best-effort re-registration calls it runs
alongside — a failure here logs a warning but does not abort the upgrade.
Test plan
Added
test_upgrade_force_syncs_manifest_hash_for_preset_overridden_skillintests/specify_cli/integrations/test_command_upgrade.py, reproducing theexact repro from the issue: init a
claudeproject, install a--devpresetthat overrides
speckit.tasks, runintegration upgrade claude --force, andassert the manifest hash for the skill file matches its on-disk content and
integration statusreportsOKwith 0 modified files.Confirmed the test fails on the pre-fix code (
git checkout HEAD~1 -- <files>,then reapplied the fix):
Passes with the fix:
Also ran the full integration/preset test suites to check for regressions:
Lint (matching CI's pinned
ruff@0.15.0):AI disclosure
This PR was authored autonomously by an AI coding agent (Claude Code, running
on the Claude Sonnet 5 model,
claude-sonnet-5) with no human-in-the-loopsupervision during implementation. The agent read the issue, traced and
independently re-verified the reported root cause in
src/specify_cli/integrations/command_upgrade.py, designed and implementedthe fix, wrote the regression test, and ran the reproduction/test/lint steps
described above.
🤖 Generated with Claude Code