Skip to content

fix: sync integration manifest hashes after preset re-registration on upgrade - #4697

Open
chelsealong wants to merge 3 commits into
github:mainfrom
chelsealong:fix/4696-preset-skill-manifest-sync
Open

chelsealong wants to merge 3 commits into
github:mainfrom
chelsealong:fix/4696-preset-skill-manifest-sync

Conversation

@chelsealong

Copy link
Copy Markdown
Contributor

Summary

Fixes #4696.

specify integration upgrade <agent> --force calls new_manifest.save()
before _register_presets_for_agent() re-registers enabled presets for the
active 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 status reports the legitimately-installed preset
override as a manually modified file (managed-files-modified), and a
subsequent specify integration upgrade <agent> without --force is
incorrectly blocked as if the user had hand-edited the file.

Fix

In src/specify_cli/integrations/command_upgrade.py, after extensions and
presets 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 command
invocation, 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() / --force
check earlier in the command).

The new helper _resync_manifest_after_registration() in _helpers.py is
best-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_skill in
tests/specify_cli/integrations/test_command_upgrade.py, reproducing the
exact repro from the issue: init a claude project, install a --dev preset
that overrides speckit.tasks, run integration upgrade claude --force, and
assert the manifest hash for the skill file matches its on-disk content and
integration status reports OK with 0 modified files.

Confirmed the test fails on the pre-fix code (git checkout HEAD~1 -- <files>,
then reapplied the fix):

FAILED ...test_upgrade_force_syncs_manifest_hash_for_preset_overridden_skill
AssertionError: manifest hash for the preset-overridden skill must match the
file `_register_presets_for_agent` just wrote
assert 'd148aeda1011260c4465c5f105fa1e045a0b0224ccb3e6e0c1ec32c732cae1ae' == '03c6814be770c7ed4e24f54ee383543d7cf984a310aace8ea3de2f1367f7a633'

Passes with the fix:

tests/specify_cli/integrations/test_command_upgrade.py::TestIntegrationUpgradeDetailed::test_upgrade_force_syncs_manifest_hash_for_preset_overridden_skill PASSED

Also ran the full integration/preset test suites to check for regressions:

$ python -m pytest tests/specify_cli/integrations/ tests/test_presets.py
====================== 947 passed, 15 warnings in 40.39s =======================

Lint (matching CI's pinned ruff@0.15.0):

$ uvx ruff@0.15.0 check src tests
All checks passed!

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-loop
supervision 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 implemented
the fix, wrote the regression test, and ran the reproduction/test/lint steps
described above.

🤖 Generated with Claude Code

… 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
@chelsealong
chelsealong requested a review from mnriem as a code owner September 23, 2026 11:06
@mnriem mnriem added the triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review label Sep 23, 2026
@mnriem
mnriem requested a balanced review from Copilot September 23, 2026 17:24

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.

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 Medium severity

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.

Comment thread src/specify_cli/integrations/_helpers.py Outdated
…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.
@chelsealong

Copy link
Copy Markdown
Contributor Author

Addressed the inline comment: _resync_manifest_after_registration() was silently swallowing per-file ValueError/OSError during rehash, so a failure left a stale hash with no warning. It now emits the existing best-effort _print_cli_warning for the specific file that failed and continues resyncing the rest.

Added test_resync_manifest_warns_on_per_file_failure_and_keeps_going, confirmed it fails on the pre-fix code and passes with the fix. Full tests/specify_cli/integrations/ + tests/test_presets.py suite: 948 passed. ruff@0.15.0 check src tests: all checks passed.

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.

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 Medium severity

Open (1)
Resolved since last review (1)

Comment thread src/specify_cli/integrations/_helpers.py
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.
@chelsealong

Copy link
Copy Markdown
Contributor Author

Posted on behalf of @chelsealong by Claude Code (model: claude-sonnet-5, autonomous); comment fully AI-drafted.

Addressed: the is_symlink()/is_file() probes in _resync_manifest_after_registration() now run inside the per-file try (previously they ran before it), so an OSError from either is caught by the existing per-file warn-and-continue handler instead of the outer handler that aborted the whole loop. Added test_resync_manifest_probe_error_does_not_abort_remaining_files, confirmed it fails on the pre-fix code and passes with the fix. Full tests/specify_cli/integrations/ + tests/test_presets.py: 949 passed. ruff@0.15.0 check src tests: all checks passed.

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.

Copilot review overview

🟢 Approval recommended

The fix directly addresses the reported stale-hash path with focused regression and error-handling tests.

Review effort: Balanced
Findings: None

Resolved since last review (1)

This branch has not been deployed

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

Labels

triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Preset-overridden skill hashes are not synchronized in integration manifest during integration upgrade

3 participants