From 7bcb98a7a1fb111559063032bc771a15ba5a5a56 Mon Sep 17 00:00:00 2001 From: Daniel Toms Date: Fri, 10 Apr 2026 20:57:14 -0700 Subject: [PATCH 1/4] fix: skip release workflow when head commit is a release Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/af-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/af-release.yml b/.github/workflows/af-release.yml index 256097be7..74a481633 100644 --- a/.github/workflows/af-release.yml +++ b/.github/workflows/af-release.yml @@ -12,6 +12,7 @@ permissions: jobs: release: runs-on: ubuntu-latest + if: "!startsWith(github.event.head_commit.message, 'chore: release')" steps: - name: Checkout uses: actions/checkout@v6 From f2992567447500138878c181a6b5592081063c70 Mon Sep 17 00:00:00 2001 From: Daniel Toms Date: Sat, 11 Apr 2026 07:40:59 -0700 Subject: [PATCH 2/4] docs: relax single-commit rule, add fork baseline to FORK.md Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 7 ++----- FORK.md | 13 +++++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3719e441c..28da860a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,16 +4,13 @@ Read @FORK.md for full context on this fork. ## Branch & Commit Strategy -- `af-main` is the fork branch. It must always be exactly **one commit** ahead of upstream. -- That commit has the subject line: `feat: AppFolio spec-kit fork — specify-af-cli with bundled extensions`. Use this to identify it. -- When merging work into `af-main` (directly or via squash merge), amend into this single fork commit rather than adding new commits. +- `af-main` is the fork branch. It accumulates commits from PRs and releases. - On feature branches, squash to one commit before merge. -- Before integrating an upstream release, ensure af-main is one commit ahead, then merge the upstream tag. Resolve conflicts per @FORK.md. +- Before integrating an upstream release, squash all af-main commits since the fork baseline (see @FORK.md) into one commit, then merge the upstream tag. Resolve conflicts per @FORK.md. - Use Conventional Commits on feature branches: `feat:`, `fix:`, `chore:`, etc. ## Git Rules (enforced by Claude unless user explicitly overrides) -- **Never create additional commits on `af-main`** — always amend the single fork commit. - **Always work on a feature branch.** Never commit directly to `af-main` without user confirmation. - **Never push merge commits.** If a pull or merge introduces a merge commit, rebase instead. - **Before force-pushing `af-main`:** confirm with the user first — this rewrites shared history. diff --git a/FORK.md b/FORK.md index e15f96c6c..860045877 100644 --- a/FORK.md +++ b/FORK.md @@ -25,6 +25,19 @@ When merging upstream releases, expect conflicts in these files: - `extensions/catalog.json` — keep AF entries and catalog URL - `.github/workflows/release.yml` — keep `af-v*` tag filter, AF install URL, extension ZIP step +## Fork Baseline + +The fork diverged from upstream at commit `43cb0fa` (`feat: add bundled lean preset with minimal workflow commands (#2161)`). +The first AF modification is `f44666a` (`feat: AppFolio spec-kit fork — specify-af-cli with bundled extensions`). + +When integrating an upstream release: +1. Squash release/chore commits on af-main (e.g. version bumps) — keep meaningful PR commits as separate logical groups for easier conflict resolution +2. Rebase onto the upstream tag +3. Resolve conflicts (see Conflict-Prone Files above) +4. Update the baseline commit below after completing the integration + +**Current baseline**: `43cb0fa` — `feat: add bundled lean preset with minimal workflow commands (#2161)` + ## How to Maintain ### Local Development Setup From 35fa1c77c99c4b0040d1ce30f9be78817a0ee53b Mon Sep 17 00:00:00 2001 From: Daniel Toms Date: Sat, 11 Apr 2026 08:44:19 -0700 Subject: [PATCH 3/4] fix: add AF extension files to inventory test expectations Co-Authored-By: Claude Sonnet 4.6 --- tests/integrations/conftest.py | 19 +++++++++++++++++++ .../test_integration_base_markdown.py | 6 ++++++ .../test_integration_base_skills.py | 6 ++++++ .../test_integration_base_toml.py | 6 ++++++ .../integrations/test_integration_copilot.py | 10 ++++++++++ .../integrations/test_integration_generic.py | 6 ++++-- 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py index 54f59e23a..e0222fd4d 100644 --- a/tests/integrations/conftest.py +++ b/tests/integrations/conftest.py @@ -2,6 +2,25 @@ from specify_cli.integrations.base import MarkdownIntegration +# AF bundled extension — command stems and installed file paths. +# Used by inventory tests to avoid duplicating these lists in every test file. +AF_EXTENSION_COMMANDS = [ + "af.after-analyze", "af.after-checklist", "af.after-clarify", + "af.after-constitution", "af.after-implement", "af.after-plan", + "af.after-specify", "af.after-tasks", "af.after-taskstoissues", + "af.before-analyze", "af.before-checklist", "af.before-clarify", + "af.before-constitution", "af.before-implement", "af.before-plan", + "af.before-specify", "af.before-tasks", "af.before-taskstoissues", + "af.placeholder", +] + +AF_EXTENSION_FILES = [ + ".specify/extensions.yml", + ".specify/extensions/.registry", + ".specify/extensions/af/README.md", + ".specify/extensions/af/extension.yml", +] + [f".specify/extensions/af/commands/speckit.{cmd}.md" for cmd in AF_EXTENSION_COMMANDS] + class StubIntegration(MarkdownIntegration): """Minimal concrete integration for testing.""" diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index e274b5224..49441dc70 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -11,6 +11,8 @@ from specify_cli.integrations.base import MarkdownIntegration from specify_cli.integrations.manifest import IntegrationManifest +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES + class MarkdownIntegrationTests: """Mixin — set class-level constants and inherit these tests. @@ -245,6 +247,10 @@ def _expected_files(self, script_variant: str) -> list[str]: files.append(f".specify/templates/{name}") files.append(".specify/memory/constitution.md") + # AF bundled extension + files += AF_EXTENSION_FILES + for cmd in AF_EXTENSION_COMMANDS: + files.append(f"{cmd_dir}/speckit.{cmd}.md") return sorted(files) def test_complete_file_inventory_sh(self, tmp_path): diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 007386611..5bf71b806 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -16,6 +16,8 @@ from specify_cli.integrations.base import SkillsIntegration from specify_cli.integrations.manifest import IntegrationManifest +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES + class SkillsIntegrationTests: """Mixin — set class-level constants and inherit these tests. @@ -347,6 +349,10 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", ] + # AF bundled extension + files += AF_EXTENSION_FILES + for cmd in AF_EXTENSION_COMMANDS: + files.append(f"{skills_prefix}/speckit-{cmd.replace('.', '-')}/SKILL.md") return sorted(files) def test_complete_file_inventory_sh(self, tmp_path): diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index fcded1834..4c62826ce 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -17,6 +17,8 @@ from specify_cli.integrations.base import TomlIntegration from specify_cli.integrations.manifest import IntegrationManifest +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES + class TomlIntegrationTests: """Mixin — set class-level constants and inherit these tests. @@ -445,6 +447,10 @@ def _expected_files(self, script_variant: str) -> list[str]: files.append(f".specify/templates/{name}") files.append(".specify/memory/constitution.md") + # AF bundled extension + files += AF_EXTENSION_FILES + for cmd in AF_EXTENSION_COMMANDS: + files.append(f"{cmd_dir}/speckit.{cmd}.toml") return sorted(files) def test_complete_file_inventory_sh(self, tmp_path): diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index 5db0155bd..890315ca4 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -6,6 +6,8 @@ from specify_cli.integrations import get_integration from specify_cli.integrations.manifest import IntegrationManifest +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES + class TestCopilotIntegration: def test_copilot_key_and_config(self): @@ -199,6 +201,10 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", ".specify/memory/constitution.md", + ] + AF_EXTENSION_FILES + [ + f".github/agents/speckit.{cmd}.agent.md" for cmd in AF_EXTENSION_COMMANDS + ] + [ + f".github/prompts/speckit.{cmd}.prompt.md" for cmd in AF_EXTENSION_COMMANDS ]) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" @@ -259,6 +265,10 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", ".specify/memory/constitution.md", + ] + AF_EXTENSION_FILES + [ + f".github/agents/speckit.{cmd}.agent.md" for cmd in AF_EXTENSION_COMMANDS + ] + [ + f".github/prompts/speckit.{cmd}.prompt.md" for cmd in AF_EXTENSION_COMMANDS ]) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index 2815456f2..2fa8dac3d 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -8,6 +8,8 @@ from specify_cli.integrations.base import MarkdownIntegration from specify_cli.integrations.manifest import IntegrationManifest +from .conftest import AF_EXTENSION_FILES + class TestGenericIntegration: """Tests for GenericIntegration — requires --commands-dir option.""" @@ -248,7 +250,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/templates/plan-template.md", ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", - ]) + ] + AF_EXTENSION_FILES) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}" @@ -304,7 +306,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/templates/plan-template.md", ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", - ]) + ] + AF_EXTENSION_FILES) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}" From 9d988f70600515e000658ea22cbe99b5606bbd27 Mon Sep 17 00:00:00 2001 From: Daniel Toms Date: Sat, 11 Apr 2026 08:44:24 -0700 Subject: [PATCH 4/4] docs: add testing section to CLAUDE.md Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 28da860a9..e13da001b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,25 @@ Read @FORK.md for full context on this fork. - Test: `specify-af version` then `specify-af init --here --ai claude` in a scratch directory - Binary is `specify-af`, package is `specify-af-cli` +## Testing + +Run tests before pushing. Also offer to run tests after completing a significant chunk of work, even if a push isn't imminent: + +```bash +uv run --extra test python -m pytest tests/ --tb=no -q +``` + +For focused checks (see `TESTING.md` for details): + +```bash +uv run --extra test python -m pytest tests/test_core_pack_scaffold.py -q # packaging/scaffolding +uv run --extra test python -m pytest tests/test_agent_config_consistency.py -q # agent config wiring +``` + +### Known failures + +- `tests/integrations/test_cli.py::TestForceExistingDirectory::test_without_force_errors_on_existing_dir` — upstream test that fails when terminal width is narrow (Rich panel wraps `"already exists"` across lines). Safe to ignore. + ## Key Files (AF-specific) - `src/specify_cli/af_init.py` — extension auto-install and upgrade logic