From 91bd3bb35ca11429b590634986d5206aaf4bb13e Mon Sep 17 00:00:00 2001 From: Daniel Toms Date: Tue, 12 May 2026 10:19:46 -0700 Subject: [PATCH] feat(af): clean up legacy AF skill dirs on init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AF fork's --force init overwrites current bundle files but leaves behind skill directories that were shipped in older versions and have since been removed or consolidated. Claude Code auto-discovers these via skill descriptions, which can hijack lifecycle dispatch. Concrete case: a stale speckit-af-check-version skill (replaced by the inline VERSION_FAIL branch in common.md) intercepts the VERSION_FAIL token from check-version.sh and renders its frozen-in-time "3-step" upgrade message with aggressive find-based cleanup, instead of letting common.md's current re-init prompt run. Mirror the existing .claude/commands/speckit.*.md cleanup with an explicit list of legacy AF skill directories to prune: - speckit-af-check-version — replaced by inline VERSION_FAIL branch - speckit-af-after-common — never had a SKILL.md; empty dir leftover Hardcoded list (not glob-derived) to avoid accidentally removing skills that the current bundle does ship — the install hierarchy names overlap enough that an "anything not in current bundle" heuristic is risky. Co-Authored-By: Claude Sonnet 4.6 --- FORK.md | 2 +- .../integrations/claude/__init__.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/FORK.md b/FORK.md index 228e00133c..f4c0811a2c 100644 --- a/FORK.md +++ b/FORK.md @@ -11,7 +11,7 @@ All files listed below are modified from upstream and will likely conflict on me | Package name | `specify-cli` → `specify-af-cli` | `pyproject.toml` | | Binary name | `specify` → `specify-af` | `pyproject.toml` | | Catalog URL | Points at `appfolio/spec-kit` `af-main` branch | `src/specify_cli/extensions.py`, `extensions/catalog.json` | -| Init flow | Auto-installs bundled AF extensions after scaffold. `--force` now overwrites init-managed files (scripts, templates, extensions, skills) while preserving user content (`memory/`, `feature.json`, `semantic-specs/`). Cleans up stale `.claude/commands/speckit.*.md` files from pre-fork installs | `src/specify_cli/__init__.py`, `src/specify_cli/integrations/claude/__init__.py` | +| Init flow | Auto-installs bundled AF extensions after scaffold. `--force` now overwrites init-managed files (scripts, templates, extensions, skills) while preserving user content (`memory/`, `feature.json`, `semantic-specs/`). Cleans up stale `.claude/commands/speckit.*.md` files from pre-fork installs and known-removed AF skill dirs under `.claude/skills/` (e.g. `speckit-af-check-version`, `speckit-af-after-common`) which Claude Code would otherwise auto-discover and use to hijack lifecycle dispatch | `src/specify_cli/__init__.py`, `src/specify_cli/integrations/claude/__init__.py` | | Removed command | `specify-af upgrade` removed — redundant with reinit flow | `src/specify_cli/__init__.py` | | Version resolution | Uses `packages_distributions()` instead of hardcoded package name | `src/specify_cli/__init__.py` | | Release workflow | release-please with `af-v*` tags on `af-main` | `.github/workflows/release.yml` | diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 90859ba8cc..f98008ca38 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import shutil from pathlib import Path from typing import Any @@ -166,6 +167,24 @@ def setup( for stale in commands_dir.glob("speckit.*.md"): stale.unlink() + # Clean up legacy AF skill directories that were shipped in earlier + # versions of the fork but have since been removed or consolidated. + # --force only overwrites current files; directories whose matching + # bundle content no longer exists survive unless explicitly pruned. + # Claude Code auto-discovers these via skill descriptions, which can + # hijack lifecycle dispatch (e.g. a stale speckit-af-check-version + # skill intercepts VERSION_FAIL handling that now lives in common.md). + legacy_af_skills = ( + "speckit-af-check-version", # replaced by inline VERSION_FAIL branch in common.md + "speckit-af-after-common", # never had a SKILL.md; empty dir leftover + ) + skills_root = project_root / ".claude" / "skills" + if skills_root.is_dir(): + for legacy in legacy_af_skills: + legacy_dir = skills_root / legacy + if legacy_dir.is_dir(): + shutil.rmtree(legacy_dir) + # Post-process generated skill files skills_dir = self.skills_dest(project_root).resolve()