Skip to content

[Feature]: specify preset update — in-place preset update with rollback and single-pass reconciliation #4427

Description

@digimangos

Summary

There is currently no specify preset update <id> command. The only way to move an installed preset to a new version is preset remove <id> followed by preset add <id>. That workaround has real correctness problems beyond performance, because both remove() and install_from_directory() independently trigger full-stack reconciliation (composed commands/skills, and — when constitution-sync is enabled — memory/constitution.md) whenever the touched preset provides a constitution template. A version bump therefore reconciles the constitution twice: once against the stack with the preset temporarily absent, once against the reinstalled stack — with a real window in between where on-disk state reflects neither the old nor the new version.

This was raised as a valid feature-parity gap in #4415, with a request to open a tracking issue including acceptance criteria for: single/all updates, catalog-only eligibility, preserving priority/enabled state, rollback on failure, and command/skill/constitution reconciliation.

Current behavior (traced in source)

  • PresetManager.install_from_directory (presets/__init__.py) refuses to install over an already-installed preset id unless force=True, which itself calls self.remove() first — i.e. there is no in-place update path, only teardown+rebuild.
  • PresetResolver.collect_all_layers / resolve_content recompute composed content from the entire installed-preset priority stack on every reconciliation call — never a diff against a single preset's previous version.
  • _reconcile_constitution / _materialize_constitution_template unconditionally overwrite .specify/memory/constitution.md (when constitution-sync is enabled and the file is still detected as machine-generated) with no check for whether the resolved content actually changed.
  • PresetRegistry stores priority and enabled per installed preset, but neither remove() nor install_from_directory() reads the previous entry back when reinstalling — so a manual remove+add workaround also risks losing an explicit --priority (if omitted, it silently falls back to the default of 10) and always resets enabled to True regardless of whether the preset was previously disabled.
  • Neither