Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/af-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 21 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,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
Expand Down
13 changes: 13 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tests/integrations/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
6 changes: 6 additions & 0 deletions tests/integrations/test_integration_base_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 6 additions & 0 deletions tests/integrations/test_integration_base_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 6 additions & 0 deletions tests/integrations/test_integration_base_toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
10 changes: 10 additions & 0 deletions tests/integrations/test_integration_copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions tests/integrations/test_integration_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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))}"
Expand Down Expand Up @@ -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))}"
Expand Down
Loading