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
50 changes: 45 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,42 @@ Read @FORK.md for full context on this fork.

## Development

- Install locally (dev): `uv tool install . --force --reinstall`
- Install locally (dev):
```bash
uv tool install . --force --reinstall
PLUGIN_ROOT=$(find ~/.claude/plugins/cache -path "*/appfolio-speckit/*/skills" -type d 2>/dev/null | sort -V | tail -1 | sed 's|/skills$||')
[ -n "$PLUGIN_ROOT" ] && cp -R ~/src/ai-dev-tools/plugins/appfolio-speckit/* "$PLUGIN_ROOT/"
```
- Install from release: `uv tool install git+https://github.com/appfolio/spec-kit@af-stable --force`
- Test: `specify-af version` then `specify-af init --here --ai claude` in a scratch directory
- Binary is `specify-af`, package is `specify-af-cli`
- `af-stable` branch is the end-user install target — only updated when a release is tagged

## Testing

Run tests before pushing. Also offer to run tests after completing a significant chunk of work, even if a push isn't imminent:
**NEVER run `specify-af init` in this repo.** This is the source repo — installing into itself creates confusing duplicate copies of the extensions under `.specify/extensions/`. For manual init testing, always create a temporary directory (e.g., `/tmp/test-af`) or ask the user where to test.

Run tests before pushing, excluding tests that fail due to intentional fork changes:

```bash
uv run --extra test python -m pytest tests/ --tb=no -q
uv run --extra test python -m pytest tests/ --tb=no -q \
--deselect tests/extensions/git/test_git_extension.py::TestGitCommonBash::test_check_feature_branch_rejects_main \
--deselect tests/extensions/git/test_git_extension.py::TestGitCommonBash::test_check_feature_branch_rejects_malformed_timestamp \
--deselect tests/test_timestamp_branches.py::TestCheckFeatureBranch::test_rejects_main \
--deselect tests/test_timestamp_branches.py::TestCheckFeatureBranch::test_rejects_partial_timestamp \
--deselect tests/test_timestamp_branches.py::TestCheckFeatureBranch::test_rejects_timestamp_without_slug \
--deselect tests/test_timestamp_branches.py::TestCheckFeatureBranch::test_rejects_7digit_timestamp_without_slug \
--deselect tests/integrations/test_cli.py::TestGitExtensionAutoInstall::test_no_git_skips_extension \
--deselect tests/test_extension_skills.py::TestExtensionSkillRegistration::test_skill_md_has_parseable_yaml \
--deselect tests/test_presets.py::TestPresetSkills::test_skill_overridden_on_preset_install \
--deselect tests/test_presets.py::TestPresetSkills::test_skill_restored_on_preset_remove \
--deselect tests/integrations/test_integration_claude.py::TestClaudeIntegration::test_claude_preset_creates_new_skill_without_commands_dir \
--deselect tests/integrations/test_cli.py::TestInitIntegrationFlag::test_integration_copilot_creates_files \
--deselect tests/integrations/test_cli.py::TestInitIntegrationFlag::test_ai_copilot_auto_promotes \
--deselect tests/integrations/test_cli.py::TestInitIntegrationFlag::test_shared_infra_skips_existing_files \
--deselect tests/integrations/test_cli.py::TestForceExistingDirectory::test_without_force_errors_on_existing_dir \
--deselect tests/integrations/test_integration_copilot.py::TestCopilotIntegration::test_complete_file_inventory_sh \
--deselect tests/integrations/test_integration_copilot.py::TestCopilotIntegration::test_complete_file_inventory_ps
```

For focused checks (see `TESTING.md` for details):
Expand All @@ -38,13 +62,29 @@ uv run --extra test python -m pytest tests/test_core_pack_scaffold.py -q # pa
uv run --extra test python -m pytest tests/test_agent_config_consistency.py -q # agent config wiring
```

### Known failures
### Excluded tests (intentional fork changes)

- `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.
| Test | Reason |
|------|--------|
| `test_check_feature_branch_rejects_main/malformed_timestamp` (git, timestamp_branches) | Branch validation disabled — `feature.json` is source of truth |
| `test_no_git_skips_extension` | Git in `AF_EXTENSION_IDS` — always installed for latest hook files |
| `test_skill_md_has_parseable_yaml`, `test_skill_overridden/restored_on_preset` | `disable-model-invocation: false` — hooks must dispatch skills |
| `test_integration_copilot_creates_files`, `test_ai_copilot_auto_promotes`, copilot inventory | `_cleanup_copilot_files` removes copilot agent files — AF uses Claude only |
| `test_shared_infra_skips_existing_files` | `--force` now overwrites init-managed files |
| `test_without_force_errors_on_existing_dir` | Upstream — Rich panel wraps at narrow widths |

## Key Files (AF-specific)

- `src/specify_cli/af_init.py` — extension auto-install and upgrade logic
- `extensions/af/` — bundled AF extension (lifecycle hooks)
- `extensions/catalog.json` — extension registry (includes AF entries)
- `pyproject.toml` — force-include for bundled extensions

## Active Technologies
- Python 3.11+ (CLI), Markdown (hook commands — agentic, not programmatic) + spec-kit upstream (0.6.x), Claude Code (executes markdown commands) (dtoms-afspeckit-refactor)
- YAML (`extensions.yml`, `extension.yml`), JSON (`feature.json`, `.registry`) (dtoms-afspeckit-refactor)
- Markdown (plugin commands — agentic, read by Claude Code) + `specify-af` CLI (checked via `/speckit.af.check-version`), `feature.json` (spec-kit core contract) (dtoms-afspeckit-refactor)
- Files on disk (`specs/<slug>/prd.md`, `specs/<slug>/ux.md`, `semantic-specs.yml`, `specs/prd-{slug}.md` for staging) (dtoms-afspeckit-refactor)

## Recent Changes
- dtoms-afspeckit-refactor: Added Python 3.11+ (CLI), Markdown (hook commands — agentic, not programmatic) + spec-kit upstream (0.6.x), Claude Code (executes markdown commands)
57 changes: 33 additions & 24 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,36 @@ This is AppFolio's fork of [github/spec-kit](https://github.com/github/spec-kit)

## What's Changed from Upstream

| Area | Change |
|------|--------|
| Package name | `specify-cli` → `specify-af-cli` |
| Binary name | `specify` → `specify-af` |
| Catalog URL | Points at `appfolio/spec-kit` `af-main` branch |
| Init flow | Auto-installs bundled AF extensions after scaffold |
| New command | `specify-af upgrade` — syncs AF extensions to bundled versions |
| Version resolution | Uses `packages_distributions()` instead of hardcoded package name |
| Release workflow | release-please with `af-v*` tags on `af-main` |
| Install branch | `af-stable` — only updated on release, safe for end-user installs |
All files listed below are modified from upstream and will likely conflict on merge. Always keep the AF-side changes.

| Area | Change | File(s) |
|------|--------|---------|
| 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` |
| 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` |
| Install branch | `af-stable` — only updated on release, safe for end-user installs | — |
| Model invocation | `disable-model-invocation: false` — allows lifecycle hooks to invoke skills programmatically. Revert when upstream merges [#2098](https://github.com/github/spec-kit/issues/2098) / [#2099](https://github.com/github/spec-kit/pull/2099) | `src/specify_cli/agents.py` |
| Branch validation | `check_feature_branch()` disabled — `feature.json` is the source of truth, `GIT_BRANCH_NAME` allows arbitrary branch names. AF before-specify hook handles main/master blocking. Revert when upstream resolves [#1680](https://github.com/github/spec-kit/issues/1680) / [#1901](https://github.com/github/spec-kit/issues/1901) | `scripts/bash/common.sh`, `extensions/git/scripts/bash/git-common.sh` |
| Constitution loading | All AF `before-*` hooks read `.specify/memory/constitution.md` into context if not already loaded | `extensions/af/commands/speckit.af.before-*.md` |
| Hook presentation | Templates execute hooks sequentially, filter out internal AF hooks, present optional hooks as numbered list | `templates/commands/*.md` |
| Specify empty args | `/speckit-specify` defers empty-argument error until after hooks run — before-hooks may provide context (e.g., PRD detection) | `templates/commands/specify.md` |
| Agent context update | Plan template skips `update-agent-context.sh` when AF extension is installed — AF manages agent context separately | `templates/commands/plan.md` |
| AF package name in errors | Error message references `specify-af-cli` | `src/specify_cli/presets.py` |
| Git commit hooks removed | All optional `speckit.git.commit` hooks removed — only `before_specify` (validate + feature) remains. AF manages commit discipline via plugin (`/speckit.af.commit.per.task`) rather than per-phase auto-commit prompts | `extensions/git/extension.yml` |
| AF hook dispatch | Domain logic (semantic search, PRD detection, artifact relocation, next-step suggestions) moved from inline hook steps to manifest-driven plugin dispatch via `hooks.yml` + `speckit-hooks/{phase}.md` | `extensions/af/commands/speckit.af.*.md` |

## Hook Architecture

The AF extension uses a manifest-driven dispatch between the bundled spec-kit extension and the separate Claude Code plugin (`appfolio-speckit`):

- **Before hooks** (`speckit.af.before-*.md`) run framework guards (version check, constitution, spec-picker) then dispatch to plugin-side hook files via `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml`. If the current phase is listed in `phases:`, the hook reads and executes `speckit-hooks/{phase}.md` from the plugin.
- **After hooks** (`speckit.af.after-*.md`) dispatch to plugin-side hook files the same way, falling back to `speckit-hooks/after-common.md` if no phase-specific file is listed.
- The AI's conversation context is the transport layer between before and after hooks — values like `APPFOLIO_SPECKIT_PLUGIN_ROOT` and `feature_directory` are resolved by before hooks and available to after hooks within the same session.
- If the plugin is not installed, `hooks.yml` is missing, or the phase is not listed, hooks proceed silently — no errors, no warnings.

## Installing

Expand All @@ -29,27 +49,16 @@ To upgrade to the latest release:
uv tool install git+https://github.com/appfolio/spec-kit@af-stable --force --reinstall
```

## Conflict-Prone Files

When merging upstream releases, expect conflicts in these files:

- `pyproject.toml` — keep AF name, console script, and force-include entries
- `src/specify_cli/__init__.py` — keep version fix (`_get_distribution_name`), init hook (`install_af_extensions`), upgrade command, panel title
- `src/specify_cli/extensions.py` — keep AF catalog URL
- `src/specify_cli/presets.py` — keep AF package name in error message
- `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)`).
When integrating an upstream release:
1. Squash work commits into logical groups and drop release commits (their info lives in git tags and FORK.md changelog)
2. Rebase onto the upstream tag
3. Resolve conflicts (see Conflict-Prone Files above)
3. Resolve conflicts (see What's Changed from Upstream above — look for files marked conflict-prone)
4. Update the baseline commit below after completing the integration

**Current baseline**: `43cb0fa` — `feat: add bundled lean preset with minimal workflow commands (#2161)`
**Current baseline**: `43cb0fa` — `feat: add bundled lean preset with minimal workflow commands (#2161)` (upstream `0.6.1`)

## How to Maintain

Expand Down Expand Up @@ -87,7 +96,7 @@ git push -u origin feat/my-feature
git fetch upstream --tags
git checkout af-main
git merge v<upstream-version>
# Resolve conflicts (see Conflict-Prone Files above)
# Resolve conflicts (see What's Changed from Upstream — files marked conflict-prone)
# Test: uv tool install . --force --reinstall && specify-af init --here
```

Expand Down
41 changes: 25 additions & 16 deletions extensions/af/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
# AppFolio Extension for Spec Kit

AppFolio lifecycle hooks and commands for Spec Kit. This extension provides hook points at every stage of the spec-kit workflow, allowing AppFolio-specific behavior to be injected before and after each operation.

## Hook Points

| Lifecycle Phase | Before | After |
|----------------|--------|-------|
| specify | `before_specify` | `after_specify` |
| plan | `before_plan` | `after_plan` |
| tasks | `before_tasks` | `after_tasks` |
| implement | `before_implement` | `after_implement` |
| analyze | `before_analyze` | `after_analyze` |
| checklist | `before_checklist` | `after_checklist` |
| clarify | `before_clarify` | `after_clarify` |
| constitution | `before_constitution` | `after_constitution` |
| taskstoissues | `before_taskstoissues` | `after_taskstoissues` |
AppFolio lifecycle hooks for Spec Kit. This extension provides framework guards (branch validation, spec-picker, constitution) and manifest-driven plugin dispatch at each workflow phase.

## Hook Behaviors

| Hook | Behavior |
|------|----------|
| `before_specify` | Branch detection, plugin dispatch (PRD detection), git feature hook |
| `after_specify` | Confirms `feature.json`, logs active spec path, plugin dispatch (artifact relocation + next steps) |
| `before_plan`, `before_tasks`, `before_implement` | Spec-picker guard + plugin dispatch (semantic search, etc.) |
| `before_analyze`, `before_checklist`, `before_clarify` | Spec-picker guard + plugin dispatch |
| `before_constitution`, `before_taskstoissues` | Pre-flight + plugin dispatch |
| All `after_*` | Plugin dispatch (phase-specific or `after-common` fallback) |

### Framework Guards

- **Spec-Picker Guard**: Runs before plan, tasks, implement, analyze, checklist, and clarify. Validates `.specify/feature.json` points to a valid spec directory. If missing or stale, scans `specs/*/spec.md` and prompts for selection.
- **Branch Validation**: Blocks on main/master before specify.
- **Constitution**: Loads `.specify/memory/constitution.md` into context if present.

### Plugin Dispatch

Each hook checks for a `hooks.yml` manifest at the plugin root (`$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml`). If the current phase is listed in `phases:`, the hook reads and executes the corresponding markdown file from `speckit-hooks/{phase}.md`. For after-hooks, if no phase-specific file is listed, it falls back to `after-common`. If the plugin is not installed or the phase is not listed, the hook proceeds silently.

This architecture allows the plugin to add domain-specific logic (semantic search, security modules, etc.) without modifying the spec-kit extension.

## Installation

This extension is bundled with `af-specify-cli` and installed automatically during `af-specify init`.
This extension is bundled with `specify-af-cli` and installed automatically during `specify-af init`. It installs before the git extension so AF hooks fire first at each lifecycle phase.
14 changes: 12 additions & 2 deletions extensions/af/commands/speckit.af.after-analyze.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: "AppFolio after-analyze lifecycle hook"
description: "Plugin dispatch (after-analyze or after-common fallback)"
---

# AppFolio: after-analyze
Expand All @@ -14,4 +14,14 @@ Do not proceed further.

## Hook Logic

<!-- TODO: Add instructions for what should happen at this lifecycle point -->
### Step 1: Plugin Dispatch

If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the before-hook earlier in this conversation:

1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists.
2. Read it and check if `after-analyze` is listed in `phases:`.
3. If listed, read and execute the instructions in:
`$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-analyze.md`
4. If the phase is not listed, check if `after-common` is listed instead.
If so, read and execute: `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-common.md`
5. If hooks.yml is missing, neither phase is listed, or the file doesn't exist, proceed silently.
14 changes: 12 additions & 2 deletions extensions/af/commands/speckit.af.after-checklist.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: "AppFolio after-checklist lifecycle hook"
description: "Plugin dispatch (after-checklist or after-common fallback)"
---

# AppFolio: after-checklist
Expand All @@ -14,4 +14,14 @@ Do not proceed further.

## Hook Logic

<!-- TODO: Add instructions for what should happen at this lifecycle point -->
### Step 1: Plugin Dispatch

If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the before-hook earlier in this conversation:

1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists.
2. Read it and check if `after-checklist` is listed in `phases:`.
3. If listed, read and execute the instructions in:
`$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-checklist.md`
4. If the phase is not listed, check if `after-common` is listed instead.
If so, read and execute: `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-common.md`
5. If hooks.yml is missing, neither phase is listed, or the file doesn't exist, proceed silently.
14 changes: 12 additions & 2 deletions extensions/af/commands/speckit.af.after-clarify.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: "AppFolio after-clarify lifecycle hook"
description: "Plugin dispatch (after-clarify or after-common fallback)"
---

# AppFolio: after-clarify
Expand All @@ -14,4 +14,14 @@ Do not proceed further.

## Hook Logic

<!-- TODO: Add instructions for what should happen at this lifecycle point -->
### Step 1: Plugin Dispatch

If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the before-hook earlier in this conversation:

1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists.
2. Read it and check if `after-clarify` is listed in `phases:`.
3. If listed, read and execute the instructions in:
`$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-clarify.md`
4. If the phase is not listed, check if `after-common` is listed instead.
If so, read and execute: `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-common.md`
5. If hooks.yml is missing, neither phase is listed, or the file doesn't exist, proceed silently.
14 changes: 12 additions & 2 deletions extensions/af/commands/speckit.af.after-constitution.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: "AppFolio after-constitution lifecycle hook"
description: "Plugin dispatch (after-constitution or after-common fallback)"
---

# AppFolio: after-constitution
Expand All @@ -14,4 +14,14 @@ Do not proceed further.

## Hook Logic

<!-- TODO: Add instructions for what should happen at this lifecycle point -->
### Step 1: Plugin Dispatch

If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the before-hook earlier in this conversation:

1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists.
2. Read it and check if `after-constitution` is listed in `phases:`.
3. If listed, read and execute the instructions in:
`$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-constitution.md`
4. If the phase is not listed, check if `after-common` is listed instead.
If so, read and execute: `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-common.md`
5. If hooks.yml is missing, neither phase is listed, or the file doesn't exist, proceed silently.
Loading
Loading