Skip to content

feat(cli): add project uninstall with specify destroy - #5100

Open
mnriem wants to merge 3 commits into
github:mainfrom
mnriem:mnriem-specify-destroy
Open

mnriem wants to merge 3 commits into
github:mainfrom
mnriem:mnriem-specify-destroy

Conversation

@mnriem

@mnriem mnriem commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Add specify destroy to uninstall Spec Kit from a project without manually
finding generated integration files.

  • Uninstall non-default integrations before the default through a shared
    operation also used by specify integration uninstall. Preserve native
    user settings while removing managed event entries and releasing the last
    dispatcher consumer.
  • Remove .specify/ and literal project-local specs/ by default.
    --preserve-constitution and --preserve-specs retain their respective SDD
    artifacts; --preserve-all combines both. Force never overrides preservation.
  • Preview before confirmation, keep JSON failures structured, respect existing
    external-adapter ownership/recovery rules, and retain original manifest
    evidence when strict teardown fails.
  • Handle declared home-scoped integration contributions separately from
    project files. Never follow a feature-directory override to enlarge deletion.

This deliberately changes no core prompt/command templates and adds no runtime
dependencies. The constitution/specification deletion scope is explicitly
described in CLI help and documentation.

Testing

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

The exact bare test command in the template was not used. Per the documented
worktree-environment guidance, dependencies and tests used this checkout's
own virtualenv instead:

  • uv sync --extra test — passed.
  • LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest -n 4 -q — passed:
    13,246 passed, 128 skipped, 62 warnings on macOS/Python 3.13.17.
  • ruff check --output-format concise src/specify_cli/_operation_destroy.py src/specify_cli/command_destroy.py src/specify_cli/mcp_destroy.py src/specify_cli/integrations/_operation_uninstall.py src/specify_cli/integrations/command_uninstall.py tests/specify_cli/test_operation_destroy.py tests/specify_cli/test_command_destroy.py tests/specify_cli/integrations/test_operation_uninstall.py
    — passed. Existing tracked files have no new Ruff diagnostic fingerprints
    compared with the committed baseline; unrelated repository-wide lint
    findings remain.
  • git diff --check — passed.

Sample-project validation used temporary projects and isolated homes, not the
development checkout:

  • 60 initialized-project combinations across Copilot, Claude, Gemini, generic
    command/skill layouts, and Hermes; five preservation choices; force on/off.
  • 30 additional actual CLI subprocess scenarios for init, preview, destroy,
    repeat, retained-byte verification, and external feature-directory isolation.
  • 20 actual workflow invocations using
    specify workflow add <smoke-package> --dev,
    specify workflow run destroy-smoke --input python=<interpreter> --json,
    and specify workflow status <run-id> --json — passed.
    Ten were resumed with
    specify workflow resume <run-id> --input approval=approve --json and
    completed; ten remained paused before destruction. All preservation choices
    and force modes were checked after workflow-generated constitution/specs,
    source files, and persisted run state existed.

The workflow smoke used real deterministic shell steps and an approval gate;
it did not mock execution or invoke the bundled AI-driven full SDD cycle.
The workflow generated artifacts, paused, resumed without replaying the
completed step, and verified completion before cleanup. Workflow state and
installed tooling were removed; selected SDD artifacts and unrelated source
were retained byte-for-byte.

Before/after evidence includes the initially missing destroy command, tracked
constitution loss, same-named skill ownership, missing recovery metadata
after teardown failures, global contribution discovery, and preservation flag
parsing/defaults. Regression tests cover the corrected positive and negative
paths. Collection increased from 13,141 at feature start to 13,374; no existing
tests were removed. Cross-platform CI has not been run locally.

AI Disclosure

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

AI disclosure: Authored on behalf of @mnriem with GitHub Copilot, powered by
GPT-6.1 Sol (gpt-6.1-sol), in autonomous/agent mode with runtime-default
reasoning settings and user-directed requirements and iterative feedback.
AI assistance covered feature specification/planning, implementation,
documentation, tests, review/reproduction, executable smoke checks, and this
PR description. The reported automated checks were run by the agent; no claim
of independent human line-by-line review or manual testing is made.

Reuse integration teardown to remove generated commands, native event entries, and the last managed dispatcher before cleaning project artifacts. Add explicit constitution/specs preservation options, ownership-aware recovery, declared global-scope handling, and positive and negative lifecycle coverage.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI max review requested due to automatic review settings October 23, 2026 22:34

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.

🟡 Changes recommended

Confirmed deletion-scope, preservation, and recovery defects prevent safe teardown.

7 open issues
What changed in this PR

Adds specify destroy as the project-uninstall counterpart to specify init, reusing integration teardown before removing Spec Kit infrastructure.

Changes:

  • Adds preview, confirmation, structured JSON, and artifact-preservation options.
  • Extracts shared integration uninstall logic and adds contribution-path and native event-cleanup validation.
  • Documents deletion boundaries and expands lifecycle, preservation, recovery, and external-adapter coverage.

Validation used isolated standard-library checks; project tests and lint were not run.

AI review by GitHub Copilot for the requesting reviewer (model unavailable; automated mode, default settings), covering analysis and suggested fixes.

File Description
tests/​specify_cli/​test_operation_destroy.py Covers ownership, preservation, deletion scope, and failure recovery.
tests/​specify_cli/​test_command_init.py Updates expected root-command registration order.
tests/​specify_cli/​test_command_destroy.py Tests confirmation, preservation flags, and JSON contracts.
tests/​specify_cli/​integrations/​test_operation_uninstall.py Tests the shared uninstall operation.
tests/​specify_cli/​integrations/​test_installed_adapters.py Adds external-adapter teardown and preservation coverage.
src/​specify_cli/​mcp_destroy.py Records destroy as unavailable through MCP.
src/​specify_cli/​integrations/​command_uninstall.py Delegates CLI uninstall to the shared operation.
src/​specify_cli/​integrations/​_operation_uninstall.py Centralizes teardown, recovery, and package cleanup.
src/​specify_cli/​integrations/​_lifecycle.py Removes uninstall-specific orchestration moved to the shared operation.
src/​specify_cli/​events/​__init__.py Adds native event-cleanup verification.
src/​specify_cli/​command_destroy.py Implements command options, preview, confirmation, and output.
src/​specify_cli/​agents.py Enumerates recorded contribution output paths.
src/​specify_cli/​_operation_destroy.py Plans, validates, and executes project teardown.
src/​specify_cli/​__init__.py Registers the destroy command.
docs/​reference/​overview.md Introduces project uninstall in the command overview.
docs/​reference/​core.md Documents options, deletion boundaries, and recovery behavior.

Review effort: Max

Comment thread src/specify_cli/__init__.py Outdated
Comment thread src/specify_cli/agents.py Outdated
Comment thread src/specify_cli/integrations/_operation_uninstall.py Outdated
Comment thread src/specify_cli/_operation_destroy.py Outdated
Comment thread src/specify_cli/_operation_destroy.py Outdated
Comment thread src/specify_cli/_operation_destroy.py Outdated
Comment thread src/specify_cli/integrations/_operation_uninstall.py Outdated
@mnriem mnriem added the triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review label Oct 25, 2026
Preflight all contribution cleanup names and dispatcher ownership/content, preserve symlinked constitution claims lexically, reject incomplete legacy inventories before mutation, retain package registries through purge failures, and allow ordinary external uninstall without a manifest.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI max review requested due to automatic review settings October 26, 2026 13:15
@mnriem

mnriem commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Addressed the review in b7b7297. Cleanup now preflights dotted and formatted command paths, preserves symlinked constitution claims by lexical manifest key, rejects legacy-only inventories during preview with migration guidance, checks dispatcher ownership/content before any uninstall, retains extension/preset registries until package deletion completes, and permits ordinary external uninstall when its manifest is missing. Added regression coverage, including purge retry and regenerated LF/CRLF dispatchers. The exact CI command uvx ruff@0.15.0 check src tests passes; the full local suite passed with 13,261 passed and 128 skipped. The new-head CI run is green, including Ruff and all six pytest jobs across Ubuntu, macOS, and Windows on Python 3.13/3.14. Markdown lint, ShellCheck, CodeQL, and dependency checks also pass. Review threads remain open for reviewer confirmation.

On behalf of @mnriem: GitHub Copilot, GPT-6.1 Sol (gpt-6.1-sol), autonomous agent mode with runtime-default reasoning settings; AI assistance covered analysis, implementation, regression tests, validation, and this comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Comment thread src/specify_cli/_operation_destroy.py Outdated
Comment thread src/specify_cli/integrations/_operation_uninstall.py
Comment thread src/specify_cli/_operation_destroy.py
Comment thread src/specify_cli/_operation_destroy.py
Comment thread src/specify_cli/_operation_destroy.py Outdated
Comment on lines +456 to +462
for name in candidate_leaves:
path = root / name
digests = expected.get(name, set())
if digests and (
path.is_symlink() or hashlib.sha256(files.read_bytes(name)).hexdigest() not in digests
):
requiring_force.add(name)
Comment thread src/specify_cli/_operation_destroy.py Outdated
Comment thread src/specify_cli/command_destroy.py
Handle case-insensitive artifact preservation without following links, force validated generic contribution cleanup before dropping integration state, resolve stacked preset skill provenance, preflight native settings and the final dispatcher claimant, synchronize hashes after managed preset initialization, finish specs-directory deletion before infrastructure, and expose human recovery details.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI max review requested due to automatic review settings October 26, 2026 14:42
@mnriem

mnriem commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Addressed the preservation and cleanup findings in d5bf78d. Constitution retention now uses filesystem-aware entry identity without following leaf links, including alternate case spellings and tracked symlinks. Validated generic contribution cleanup honors force and verifies completion before dropping integration state. Stacked preset skill claims share the lifecycle's validated override provenance; preset initialization updates hashes only for files changed by managed rendering. Planning parses built-in native settings and checks the dispatcher against its last claimant before any teardown. Specs files and directories are removed before infrastructure, and human-readable errors include remaining paths and completed work. Regression cases reproduced all eight new behavioral findings before fixes; additional tests cover retry and preservation boundaries. The full local suite passed with 13,279 passed and 128 skipped; CI-pinned Ruff and the real workflow smoke (20 runs, ten resumes) also pass. New-head CI is green: Ruff and all six Ubuntu/macOS/Windows Python 3.13/3.14 test jobs passed, as did Markdown lint, ShellCheck, CodeQL, and dependency checks. Review threads are left open for confirmation.

On behalf of @mnriem: GitHub Copilot, GPT-6.1 Sol (gpt-6.1-sol), autonomous agent mode with runtime-default reasoning settings. AI assistance covered analysis, implementation, regression tests, validation, and this review summary.

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.

raise FileHelperError(f"Refusing symlink during destroy: {relative}")
leaves.add(relative)
elif stat.S_ISREG(mode):
files.read_bytes(relative)
if kind == "presets":
from .presets import PresetManager

override_sources = PresetManager(root).skill_ownership_sources(identifier)
Comment on lines +298 to +300
candidates = registrar.registered_artifact_paths(
agent, root, commands=[], skills=[skill], registrar_config=config,
)
Comment on lines +337 to +338
if source not in sources:
raise ValueError(f"Cannot establish skill ownership at {candidate}")
Comment on lines +563 to +564
warnings.extend(result.warnings)
uninstalled.append(key)
Comment on lines +941 to +951
changed = False
for name, previous in before_preset.items():
path = project_path / name
if (
path.is_file() and not path.is_symlink()
and hashlib.sha256(path.read_bytes()).hexdigest() != previous
):
manifest.record_existing(name)
changed = True
if changed:
manifest.save()
Comment on lines +92 to +93
except FileNotFoundError:
continue
Comment on lines +204 to +207
warnings.extend(unregister_integration_artifacts(
root, key, require_complete=request.require_complete, force=request.force,
))
if request.require_complete and skipped:
Comment on lines +238 to +240
_remove_integration_json(root)
if default == key:
_clear_init_options_for_integration(root, key)
Comment on lines +312 to +313
with _retain_recovery_manifest(request, key):
result = _uninstall(request, warnings)

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.

2 participants