diff --git a/CLAUDE.md b/CLAUDE.md index 0a5f1cbbe5..5b05a6ee4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,12 @@ 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` @@ -25,10 +30,29 @@ Read @FORK.md for full context on this fork. ## 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): @@ -38,9 +62,16 @@ 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) @@ -48,3 +79,12 @@ uv run --extra test python -m pytest tests/test_agent_config_consistency.py -q - `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//prd.md`, `specs//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) diff --git a/FORK.md b/FORK.md index 862b28f75e..3f8b11cf0f 100644 --- a/FORK.md +++ b/FORK.md @@ -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 @@ -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 @@ -87,7 +96,7 @@ git push -u origin feat/my-feature git fetch upstream --tags git checkout af-main git merge v -# 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 ``` diff --git a/extensions/af/README.md b/extensions/af/README.md index 0b49bfd9f0..c787a7e9df 100644 --- a/extensions/af/README.md +++ b/extensions/af/README.md @@ -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. diff --git a/extensions/af/commands/speckit.af.after-analyze.md b/extensions/af/commands/speckit.af.after-analyze.md index 255277cae6..7f4df5264c 100644 --- a/extensions/af/commands/speckit.af.after-analyze.md +++ b/extensions/af/commands/speckit.af.after-analyze.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-analyze lifecycle hook" +description: "Plugin dispatch (after-analyze or after-common fallback)" --- # AppFolio: after-analyze @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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. diff --git a/extensions/af/commands/speckit.af.after-checklist.md b/extensions/af/commands/speckit.af.after-checklist.md index 7ea6dd4621..1d2f5462c6 100644 --- a/extensions/af/commands/speckit.af.after-checklist.md +++ b/extensions/af/commands/speckit.af.after-checklist.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-checklist lifecycle hook" +description: "Plugin dispatch (after-checklist or after-common fallback)" --- # AppFolio: after-checklist @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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. diff --git a/extensions/af/commands/speckit.af.after-clarify.md b/extensions/af/commands/speckit.af.after-clarify.md index ab2ae5e8d6..25796b1113 100644 --- a/extensions/af/commands/speckit.af.after-clarify.md +++ b/extensions/af/commands/speckit.af.after-clarify.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-clarify lifecycle hook" +description: "Plugin dispatch (after-clarify or after-common fallback)" --- # AppFolio: after-clarify @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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. diff --git a/extensions/af/commands/speckit.af.after-constitution.md b/extensions/af/commands/speckit.af.after-constitution.md index 98913285e2..1847642b68 100644 --- a/extensions/af/commands/speckit.af.after-constitution.md +++ b/extensions/af/commands/speckit.af.after-constitution.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-constitution lifecycle hook" +description: "Plugin dispatch (after-constitution or after-common fallback)" --- # AppFolio: after-constitution @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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. diff --git a/extensions/af/commands/speckit.af.after-implement.md b/extensions/af/commands/speckit.af.after-implement.md index aa3bcd0f8b..456e21a506 100644 --- a/extensions/af/commands/speckit.af.after-implement.md +++ b/extensions/af/commands/speckit.af.after-implement.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-implement lifecycle hook" +description: "Plugin dispatch (after-implement or after-common fallback)" --- # AppFolio: after-implement @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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-implement` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-implement.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. diff --git a/extensions/af/commands/speckit.af.after-plan.md b/extensions/af/commands/speckit.af.after-plan.md index 610b820796..b711738d45 100644 --- a/extensions/af/commands/speckit.af.after-plan.md +++ b/extensions/af/commands/speckit.af.after-plan.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-plan lifecycle hook" +description: "Plugin dispatch (after-plan or after-common fallback)" --- # AppFolio: after-plan @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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-plan` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-plan.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. diff --git a/extensions/af/commands/speckit.af.after-specify.md b/extensions/af/commands/speckit.af.after-specify.md index 75ad16a7a2..5798aca1f5 100644 --- a/extensions/af/commands/speckit.af.after-specify.md +++ b/extensions/af/commands/speckit.af.after-specify.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-specify lifecycle hook" +description: "Confirms feature.json + plugin dispatch" --- # AppFolio: after-specify @@ -14,4 +14,40 @@ Do not proceed further. ## Hook Logic - +### Step 1: Confirm or repair feature.json + +1. Check if `.specify/feature.json` exists and contains a valid `feature_directory` pointing to a directory with `spec.md`. +2. If valid, proceed to Step 2. +3. If missing or invalid, **repair it**: find the spec directory that was just created: + +```bash +ls -td specs/*/spec.md 2>/dev/null | head -1 +``` + +Extract the parent directory from the result (e.g., `specs/001-hello-world-script/spec.md` → `specs/001-hello-world-script`). Write `.specify/feature.json`: + +```json +{ + "feature_directory": "specs/001-hello-world-script" +} +``` + +Confirm to the user: + +> **Repaired**: `feature.json` was missing — set to `{feature_directory}`. + +### Step 2: Log active spec path + +Confirm to the engineer: + +> **Active feature**: `{feature_directory}` — spec and artifacts will be stored here. + +### Step 3: 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-specify` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-specify.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. diff --git a/extensions/af/commands/speckit.af.after-tasks.md b/extensions/af/commands/speckit.af.after-tasks.md index 46ac93396b..1295bfbdde 100644 --- a/extensions/af/commands/speckit.af.after-tasks.md +++ b/extensions/af/commands/speckit.af.after-tasks.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-tasks lifecycle hook" +description: "Plugin dispatch (after-tasks or after-common fallback)" --- # AppFolio: after-tasks @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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-tasks` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-tasks.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. diff --git a/extensions/af/commands/speckit.af.after-taskstoissues.md b/extensions/af/commands/speckit.af.after-taskstoissues.md index f6294e392b..8e780636cc 100644 --- a/extensions/af/commands/speckit.af.after-taskstoissues.md +++ b/extensions/af/commands/speckit.af.after-taskstoissues.md @@ -1,5 +1,5 @@ --- -description: "AppFolio after-taskstoissues lifecycle hook" +description: "Plugin dispatch (after-taskstoissues or after-common fallback)" --- # AppFolio: after-taskstoissues @@ -14,4 +14,14 @@ Do not proceed further. ## Hook Logic - +### 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-taskstoissues` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/after-taskstoissues.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. diff --git a/extensions/af/commands/speckit.af.before-analyze.md b/extensions/af/commands/speckit.af.before-analyze.md index e4cfd6b7db..03aa81dd58 100644 --- a/extensions/af/commands/speckit.af.before-analyze.md +++ b/extensions/af/commands/speckit.af.before-analyze.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-analyze lifecycle hook" +description: "Framework guard + plugin dispatch" --- # AppFolio: before-analyze @@ -14,4 +14,36 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Spec-Picker Guard + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.spec-picker.md` to validate `feature.json` and resolve the active spec directory. Wait for it to complete before proceeding. + +### Step 5: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-analyze` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-analyze.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +Proceed to the core command. diff --git a/extensions/af/commands/speckit.af.before-checklist.md b/extensions/af/commands/speckit.af.before-checklist.md index 31a10651a8..196523e715 100644 --- a/extensions/af/commands/speckit.af.before-checklist.md +++ b/extensions/af/commands/speckit.af.before-checklist.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-checklist lifecycle hook" +description: "Framework guard + plugin dispatch" --- # AppFolio: before-checklist @@ -14,4 +14,36 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Spec-Picker Guard + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.spec-picker.md` to validate `feature.json` and resolve the active spec directory. Wait for it to complete before proceeding. + +### Step 5: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-checklist` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-checklist.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +Proceed to the core command. diff --git a/extensions/af/commands/speckit.af.before-clarify.md b/extensions/af/commands/speckit.af.before-clarify.md index c9ea7251f3..22dac6c8a3 100644 --- a/extensions/af/commands/speckit.af.before-clarify.md +++ b/extensions/af/commands/speckit.af.before-clarify.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-clarify lifecycle hook" +description: "Framework guard + plugin dispatch" --- # AppFolio: before-clarify @@ -14,4 +14,36 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Spec-Picker Guard + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.spec-picker.md` to validate `feature.json` and resolve the active spec directory. Wait for it to complete before proceeding. + +### Step 5: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-clarify` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-clarify.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +Proceed to the core command. diff --git a/extensions/af/commands/speckit.af.before-constitution.md b/extensions/af/commands/speckit.af.before-constitution.md index f8a8b7f663..7c5cd8aae1 100644 --- a/extensions/af/commands/speckit.af.before-constitution.md +++ b/extensions/af/commands/speckit.af.before-constitution.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-constitution lifecycle hook" +description: "Framework pre-flight + plugin dispatch" --- # AppFolio: before-constitution @@ -14,4 +14,32 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-constitution` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-constitution.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +Proceed to the core command. diff --git a/extensions/af/commands/speckit.af.before-implement.md b/extensions/af/commands/speckit.af.before-implement.md index 673aa9ce7c..9c7ab75282 100644 --- a/extensions/af/commands/speckit.af.before-implement.md +++ b/extensions/af/commands/speckit.af.before-implement.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-implement lifecycle hook" +description: "Framework guard + plugin dispatch" --- # AppFolio: before-implement @@ -14,4 +14,38 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Spec-Picker Guard + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.spec-picker.md` to validate `feature.json` and resolve the active spec directory. Wait for it to complete before proceeding. + +### Step 5: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-implement` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-implement.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +### Step 6: Proceed + +After completing the guard and plugin dispatch (or skipping them), proceed to the core `/speckit.implement` command. diff --git a/extensions/af/commands/speckit.af.before-plan.md b/extensions/af/commands/speckit.af.before-plan.md index 261ad6dc40..f9507fa200 100644 --- a/extensions/af/commands/speckit.af.before-plan.md +++ b/extensions/af/commands/speckit.af.before-plan.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-plan lifecycle hook" +description: "Framework guard + plugin dispatch" --- # AppFolio: before-plan @@ -14,4 +14,38 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Spec-Picker Guard + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.spec-picker.md` to validate `feature.json` and resolve the active spec directory. Wait for it to complete before proceeding. + +### Step 5: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-plan` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-plan.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +### Step 6: Proceed + +After completing the guard and plugin dispatch (or skipping them), proceed to the core `/speckit.plan` command. diff --git a/extensions/af/commands/speckit.af.before-specify.md b/extensions/af/commands/speckit.af.before-specify.md index 4825557ac4..12aac6addc 100644 --- a/extensions/af/commands/speckit.af.before-specify.md +++ b/extensions/af/commands/speckit.af.before-specify.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-specify lifecycle hook" +description: "Branch detection, plugin dispatch, git feature hook" --- # AppFolio: before-specify @@ -14,4 +14,70 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-specify` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-specify.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +### Step 5: Detect current branch + +Run the following command to get the current git branch name: + +```bash +git rev-parse --abbrev-ref HEAD +``` + +If the command fails (not a git repo or git not available), warn and proceed: + +> **Warning**: Could not detect git branch. Proceeding without branch context. + +### Step 6: Validate branch + +If the current branch is `main` or `master`, **STOP** and display this error: + +> **Error**: You are on the `{branch}` branch. Please switch to a feature branch before running `/speckit.specify`. +> +> Example: `git checkout -b feat/my-feature` + +Do **not** proceed to the core specify command. The workflow ends here. + +### Step 7: Execute the git feature hook + +If the branch is valid (not main/master), run the git extension's `create-new-feature.sh` script directly. This replaces the separate `speckit.git.feature` hook — do **not** dispatch that hook as a skill when it appears next in the hook list; it has already been handled here. + +Run this command, substituting the actual values: + +```bash +GIT_BRANCH_NAME="" .specify/extensions/git/scripts/bash/create-new-feature.sh --json --allow-existing-branch --short-name "" "" +``` + +- `` — the branch detected in Step 5 +- `` — a 2-4 word slug derived from the feature description (e.g., `hello-world-script`) +- `` — the user's feature description (from `$ARGUMENTS` or PRD title from Step 4) + +If the script succeeds, note the `BRANCH_NAME` and `FEATURE_NUM` from its JSON output. These are available to the core specify command. + +If the script fails, display the error and stop — do not proceed to the core specify command. diff --git a/extensions/af/commands/speckit.af.before-tasks.md b/extensions/af/commands/speckit.af.before-tasks.md index b32f73d965..7fd1ddf5ed 100644 --- a/extensions/af/commands/speckit.af.before-tasks.md +++ b/extensions/af/commands/speckit.af.before-tasks.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-tasks lifecycle hook" +description: "Framework guard + plugin dispatch" --- # AppFolio: before-tasks @@ -14,4 +14,38 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Spec-Picker Guard + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.spec-picker.md` to validate `feature.json` and resolve the active spec directory. Wait for it to complete before proceeding. + +### Step 5: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-tasks` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-tasks.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +### Step 6: Proceed + +After completing the guard and plugin dispatch (or skipping them), proceed to the core `/speckit.tasks` command. diff --git a/extensions/af/commands/speckit.af.before-taskstoissues.md b/extensions/af/commands/speckit.af.before-taskstoissues.md index 5278fc23a2..4d2212bbb0 100644 --- a/extensions/af/commands/speckit.af.before-taskstoissues.md +++ b/extensions/af/commands/speckit.af.before-taskstoissues.md @@ -1,5 +1,5 @@ --- -description: "AppFolio before-taskstoissues lifecycle hook" +description: "Framework pre-flight + plugin dispatch" --- # AppFolio: before-taskstoissues @@ -14,4 +14,32 @@ Do not proceed further. ## Hook Logic - +### Step 1: Extensions Context + +If you have not already read `.specify/extensions.yml` in this conversation, read it now: + +```bash +cat .specify/extensions.yml +``` + +This ensures you are aware of all registered hooks and their execution order. Do not skip hooks that are registered in this file. + +### Step 2: Common Pre-Flight + +Read and execute the instructions in `.specify/extensions/af/commands/speckit.af.common.md` to verify CLI compatibility and resolve the plugin root. If it fails, stop — do not proceed. + +### Step 3: Constitution + +If `.specify/memory/constitution.md` exists and you have not already read it in this conversation, read it now. This ensures project principles are in context for all downstream decisions. + +### Step 4: Plugin Dispatch + +If `APPFOLIO_SPECKIT_PLUGIN_ROOT` was resolved in the common pre-flight: + +1. Check if `$APPFOLIO_SPECKIT_PLUGIN_ROOT/hooks.yml` exists. +2. Read it and check if `before-taskstoissues` is listed in `phases:`. +3. If listed, read and execute the instructions in: + `$APPFOLIO_SPECKIT_PLUGIN_ROOT/speckit-hooks/before-taskstoissues.md` +4. If hooks.yml is missing, the phase is not listed, or the file doesn't exist, proceed silently. + +Proceed to the core command. diff --git a/extensions/af/commands/speckit.af.common.md b/extensions/af/commands/speckit.af.common.md new file mode 100644 index 0000000000..bc8e3ac20f --- /dev/null +++ b/extensions/af/commands/speckit.af.common.md @@ -0,0 +1,76 @@ +--- +description: "Common pre-flight checks dispatched by all AF lifecycle hooks — version check and plugin root resolution" +--- + +# AppFolio: Common Pre-Flight + +This is a utility command dispatched by lifecycle hooks. It runs version compatibility checks and resolves the plugin installation path. + +If you are executing this because a user typed this command directly (not because a hook dispatched it), STOP. Reply with: + +> This is an internal utility command. It runs automatically during the spec-kit workflow. + +Do not proceed further. + +## Session Short-Circuit + +If you have already completed these checks earlier in this conversation, skip them all and return immediately. + +## Step 1: Version Check + +Run this script: + +```bash +.specify/extensions/af/scripts/bash/check-version.sh +``` + +### If `VERSION_OK` + +Continue to Step 2. + +### If `VERSION_FAIL` + +**STOP.** Display this message exactly as written below (substituting the version values), then offer to help. Do not proceed to the core command. + +``` +specify-af version mismatch + + Installed: {INSTALLED value, or "not installed" if empty} + Required: {MINIMUM} or later + +To fix this, run these 3 steps: + + 1. Upgrade the CLI + uv tool install git+https://github.com/appfolio/spec-kit@af-stable --force + + 2. Re-initialize this repo + specify-af init --here --ai claude --script sh --force + + The --force flag overwrites init-managed files (scripts, templates, + extensions, skills) while preserving user content (memory/, + feature.json, semantic-specs/, etc.). + + 3. Restart Claude Code + The new version won't take effect until you restart. +``` + +Then ask: **"Would you like me to run steps 1 and 2 for you?"** + +If the user agrees: +1. Run the `uv tool install` command from Step 1 +2. Run `specify-af init --here --ai claude --script sh --force` +3. After both succeed, say: "Upgrade complete. Please restart Claude Code to pick up the new version." + +Do not proceed to the core command — a restart is required regardless. + +## Step 2: Plugin Root + +Resolve the appfolio-speckit plugin installation path: + +```bash +find ~/.claude/plugins/cache -path "*/appfolio-speckit/*/skills" -type d 2>/dev/null | sort -V | tail -1 | sed 's|/skills$||' +``` + +Note the output as `APPFOLIO_SPECKIT_PLUGIN_ROOT`. If the command returns nothing, the plugin is not installed — proceed without it. + +Return to the calling hook. diff --git a/extensions/af/commands/speckit.af.placeholder.md b/extensions/af/commands/speckit.af.placeholder.md deleted file mode 100644 index f57e024cb6..0000000000 --- a/extensions/af/commands/speckit.af.placeholder.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: "Placeholder AppFolio command" ---- - -# AppFolio: Placeholder - - diff --git a/extensions/af/commands/speckit.af.spec-picker.md b/extensions/af/commands/speckit.af.spec-picker.md new file mode 100644 index 0000000000..a2b00e2c3c --- /dev/null +++ b/extensions/af/commands/speckit.af.spec-picker.md @@ -0,0 +1,62 @@ +--- +description: "Resolves active spec directory from feature.json — full guard mode for lifecycle hooks, resolve-only mode for skills" +--- + +# AppFolio: Spec-Picker + +This is a utility command dispatched by lifecycle hooks and plugin skills. It resolves the active spec directory from `feature.json`. + +If you are executing this because a user typed this command directly (not because a hook or skill dispatched it), STOP. Reply with: + +> This is an internal utility command. It runs automatically during the spec-kit workflow. + +Do not proceed further. + +## Modes + +- **Default (full guard)**: Validates `feature.json`, scans and prompts if missing, writes `feature.json`. Used by lifecycle hooks (before-plan, before-tasks, before-implement). +- **`--resolve-only`**: Reads `feature.json` if it exists, returns the feature directory path or "NONE". No scanning, no prompting, no blocking. Used by plugin skills (pm, uxd) that need to know the active feature but don't require one. + +## Full Guard Logic (default) + +1. Check if `.specify/feature.json` exists. + +2. If it exists, read it and extract the `feature_directory` value. + +3. Validate that the directory exists (e.g., `specs//` exists and contains `spec.md`). + +4. If `feature.json` exists but points to a missing directory: + - Delete the stale `.specify/feature.json` + - Fall through to the scan-and-pick flow below. + +5. If `.specify/feature.json` does not exist (or was just deleted): + - Locate the `specs/` directory that is a **peer of the `.specify/` directory** (i.e., in the same parent directory as `.specify/`, not inside it). If not found, skip silently and return. + - Scan for all `specs/*/spec.md` files in that directory. + - If no specs found, skip silently and return. + - If exactly one spec found, auto-select it and write `feature.json`: + ```json + { + "feature_directory": "specs/" + } + ``` + - If multiple specs found, run this command to sort by modification time (most recent first): + ```bash + if stat -f "%m" / >/dev/null 2>&1; then stat -f "%m %Sm %N" -t "%m-%d %H:%M" specs/*/spec.md; else stat -c "%Y %n" specs/*/spec.md; fi 2>/dev/null | sort -rn | awk '{print $2, $3, $4}' + ``` + - Run `ToolSearch` with query `select:AskUserQuestion` to load the tool schema. + - Then use `AskUserQuestion` to present up to 4 specs as options (most recently modified first). Each option label should be the spec slug (e.g. `003-user-auth`) and the description should include the last modified date. If there are more than 4 specs, include the 4 most recent and add a note that older specs were omitted. + - Write the selected path to `.specify/feature.json`. + +6. Once `feature.json` is valid, note the active spec path for context and return to the calling hook. + +## Resolve-Only Logic (`--resolve-only`) + +1. Check if `.specify/feature.json` exists. + +2. If it exists, read it and extract the `feature_directory` value. + +3. Validate that the directory exists. + +4. If valid, return the `feature_directory` path to the calling skill. + +5. If `feature.json` is missing, unreadable, or points to a non-existent directory, return "NONE". Do not scan, prompt, or block. diff --git a/extensions/af/extension.yml b/extensions/af/extension.yml index 2fbde594d4..f339612059 100644 --- a/extensions/af/extension.yml +++ b/extensions/af/extension.yml @@ -3,8 +3,8 @@ schema_version: "1.0" extension: id: af name: "AppFolio" - version: "1.0.0" - description: "AppFolio lifecycle hooks and commands for Spec Kit" + version: "1.1.0" + description: "AppFolio lifecycle hooks for Spec Kit — framework guards (branch, spec-picker, constitution) + manifest-driven plugin dispatch" author: appfolio repository: https://github.com/appfolio/spec-kit license: proprietary @@ -14,155 +14,160 @@ requires: provides: commands: + # Utility commands (dispatched by hooks, not user-invoked) + - name: speckit.af.common + file: commands/speckit.af.common.md + description: "Common pre-flight checks — version compatibility and plugin root resolution (session-cached)" + - name: speckit.af.spec-picker + file: commands/speckit.af.spec-picker.md + description: "Resolves active spec directory — full guard for hooks, --resolve-only for skills" + # Lifecycle hooks - name: speckit.af.before-specify file: commands/speckit.af.before-specify.md - description: "Hook: runs before specification generation" + description: "Branch detection, plugin dispatch, git feature hook" - name: speckit.af.after-specify file: commands/speckit.af.after-specify.md - description: "Hook: runs after specification generation" + description: "Confirms feature.json + plugin dispatch" - name: speckit.af.before-plan file: commands/speckit.af.before-plan.md - description: "Hook: runs before implementation planning" + description: "Framework guard + plugin dispatch" - name: speckit.af.after-plan file: commands/speckit.af.after-plan.md - description: "Hook: runs after implementation planning" + description: "Plugin dispatch (after-plan or after-common fallback)" - name: speckit.af.before-tasks file: commands/speckit.af.before-tasks.md - description: "Hook: runs before task generation" + description: "Framework guard + plugin dispatch" - name: speckit.af.after-tasks file: commands/speckit.af.after-tasks.md - description: "Hook: runs after task generation" + description: "Plugin dispatch (after-tasks or after-common fallback)" - name: speckit.af.before-implement file: commands/speckit.af.before-implement.md - description: "Hook: runs before implementation" + description: "Framework guard + plugin dispatch" - name: speckit.af.after-implement file: commands/speckit.af.after-implement.md - description: "Hook: runs after implementation" + description: "Plugin dispatch (after-implement or after-common fallback)" - name: speckit.af.before-analyze file: commands/speckit.af.before-analyze.md - description: "Hook: runs before cross-artifact analysis" + description: "Framework guard + plugin dispatch" - name: speckit.af.after-analyze file: commands/speckit.af.after-analyze.md - description: "Hook: runs after cross-artifact analysis" + description: "Plugin dispatch (after-analyze or after-common fallback)" - name: speckit.af.before-checklist file: commands/speckit.af.before-checklist.md - description: "Hook: runs before checklist generation" + description: "Framework guard + plugin dispatch" - name: speckit.af.after-checklist file: commands/speckit.af.after-checklist.md - description: "Hook: runs after checklist generation" + description: "Plugin dispatch (after-checklist or after-common fallback)" - name: speckit.af.before-clarify file: commands/speckit.af.before-clarify.md - description: "Hook: runs before spec clarification" + description: "Framework guard + plugin dispatch" - name: speckit.af.after-clarify file: commands/speckit.af.after-clarify.md - description: "Hook: runs after spec clarification" + description: "Plugin dispatch (after-clarify or after-common fallback)" - name: speckit.af.before-constitution file: commands/speckit.af.before-constitution.md - description: "Hook: runs before constitution update" + description: "Framework pre-flight + plugin dispatch" - name: speckit.af.after-constitution file: commands/speckit.af.after-constitution.md - description: "Hook: runs after constitution update" + description: "Plugin dispatch (after-constitution or after-common fallback)" - name: speckit.af.before-taskstoissues file: commands/speckit.af.before-taskstoissues.md - description: "Hook: runs before tasks-to-issues conversion" + description: "Framework pre-flight + plugin dispatch" - name: speckit.af.after-taskstoissues file: commands/speckit.af.after-taskstoissues.md - description: "Hook: runs after tasks-to-issues conversion" - - name: speckit.af.placeholder - file: commands/speckit.af.placeholder.md - description: "Placeholder AppFolio command" + description: "Plugin dispatch (after-taskstoissues or after-common fallback)" hooks: before_specify: command: speckit.af.before-specify - optional: true + optional: false prompt: "Run AppFolio pre-specify hook?" - description: "AppFolio hook before specification generation" + description: "Branch detection, plugin dispatch, git feature hook" after_specify: command: speckit.af.after-specify - optional: true + optional: false prompt: "Run AppFolio post-specify hook?" - description: "AppFolio hook after specification generation" + description: "Confirms feature.json + plugin dispatch" before_plan: command: speckit.af.before-plan - optional: true + optional: false prompt: "Run AppFolio pre-plan hook?" - description: "AppFolio hook before implementation planning" + description: "Framework guard + plugin dispatch" after_plan: command: speckit.af.after-plan - optional: true + optional: false prompt: "Run AppFolio post-plan hook?" - description: "AppFolio hook after implementation planning" + description: "Plugin dispatch (after-plan or after-common fallback)" before_tasks: command: speckit.af.before-tasks - optional: true + optional: false prompt: "Run AppFolio pre-tasks hook?" - description: "AppFolio hook before task generation" + description: "Framework guard + plugin dispatch" after_tasks: command: speckit.af.after-tasks - optional: true + optional: false prompt: "Run AppFolio post-tasks hook?" - description: "AppFolio hook after task generation" + description: "Plugin dispatch (after-tasks or after-common fallback)" before_implement: command: speckit.af.before-implement - optional: true + optional: false prompt: "Run AppFolio pre-implement hook?" - description: "AppFolio hook before implementation" + description: "Framework guard + plugin dispatch" after_implement: command: speckit.af.after-implement - optional: true + optional: false prompt: "Run AppFolio post-implement hook?" - description: "AppFolio hook after implementation" + description: "Plugin dispatch (after-implement or after-common fallback)" before_analyze: command: speckit.af.before-analyze - optional: true + optional: false prompt: "Run AppFolio pre-analyze hook?" - description: "AppFolio hook before cross-artifact analysis" + description: "Framework guard + plugin dispatch" after_analyze: command: speckit.af.after-analyze - optional: true + optional: false prompt: "Run AppFolio post-analyze hook?" - description: "AppFolio hook after cross-artifact analysis" + description: "Plugin dispatch (after-analyze or after-common fallback)" before_checklist: command: speckit.af.before-checklist - optional: true + optional: false prompt: "Run AppFolio pre-checklist hook?" - description: "AppFolio hook before checklist generation" + description: "Framework guard + plugin dispatch" after_checklist: command: speckit.af.after-checklist - optional: true + optional: false prompt: "Run AppFolio post-checklist hook?" - description: "AppFolio hook after checklist generation" + description: "Plugin dispatch (after-checklist or after-common fallback)" before_clarify: command: speckit.af.before-clarify - optional: true + optional: false prompt: "Run AppFolio pre-clarify hook?" - description: "AppFolio hook before spec clarification" + description: "Framework guard + plugin dispatch" after_clarify: command: speckit.af.after-clarify - optional: true + optional: false prompt: "Run AppFolio post-clarify hook?" - description: "AppFolio hook after spec clarification" + description: "Plugin dispatch (after-clarify or after-common fallback)" before_constitution: command: speckit.af.before-constitution - optional: true + optional: false prompt: "Run AppFolio pre-constitution hook?" - description: "AppFolio hook before constitution update" + description: "Framework pre-flight + plugin dispatch" after_constitution: command: speckit.af.after-constitution - optional: true + optional: false prompt: "Run AppFolio post-constitution hook?" - description: "AppFolio hook after constitution update" + description: "Plugin dispatch (after-constitution or after-common fallback)" before_taskstoissues: command: speckit.af.before-taskstoissues - optional: true + optional: false prompt: "Run AppFolio pre-taskstoissues hook?" - description: "AppFolio hook before tasks-to-issues conversion" + description: "Framework pre-flight + plugin dispatch" after_taskstoissues: command: speckit.af.after-taskstoissues - optional: true + optional: false prompt: "Run AppFolio post-taskstoissues hook?" - description: "AppFolio hook after tasks-to-issues conversion" + description: "Plugin dispatch (after-taskstoissues or after-common fallback)" tags: - "appfolio" diff --git a/extensions/af/scripts/bash/check-version.sh b/extensions/af/scripts/bash/check-version.sh new file mode 100755 index 0000000000..c020cac8d0 --- /dev/null +++ b/extensions/af/scripts/bash/check-version.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# AF extension: check-version.sh +# Verifies that the installed specify-af CLI meets the minimum version. +# Output: "VERSION_OK: " or "VERSION_FAIL: " + +set -e + +MINIMUM="0.6.1.4" + +INSTALLED=$(specify-af version 2>/dev/null | grep "CLI Version" | sed 's/.*CLI Version[[:space:]]*//' | sed 's/[[:space:]]*│.*//' | tr -d ' ') + +if [ -z "$INSTALLED" ] || \ + [ "$(printf '%s\n' "$MINIMUM" "$INSTALLED" | sort -V | head -1)" != "$MINIMUM" ]; then + echo "VERSION_FAIL: ${INSTALLED:-NOT_INSTALLED}" +else + echo "VERSION_OK: ${INSTALLED}" +fi diff --git a/extensions/catalog.json b/extensions/catalog.json index 2ec63f26af..dbf5c09101 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -21,8 +21,8 @@ "af": { "name": "AppFolio Lifecycle Hooks", "id": "af", - "version": "1.0.0", - "description": "AppFolio lifecycle hooks and commands for Spec Kit", + "version": "1.1.0", + "description": "AppFolio lifecycle hooks — branch-aware spec creation, spec-picker guard, semantic search dispatch with graceful degradation", "author": "appfolio", "repository": "https://github.com/appfolio/spec-kit", "download_url": "https://github.com/appfolio/spec-kit/releases/latest/download/af.zip", diff --git a/extensions/git/extension.yml b/extensions/git/extension.yml index 13c1977ea1..f77846f251 100644 --- a/extensions/git/extension.yml +++ b/extensions/git/extension.yml @@ -48,87 +48,6 @@ hooks: command: speckit.git.feature optional: false description: "Create feature branch before specification" - before_clarify: - command: speckit.git.commit - optional: true - prompt: "Commit outstanding changes before clarification?" - description: "Auto-commit before spec clarification" - before_plan: - command: speckit.git.commit - optional: true - prompt: "Commit outstanding changes before planning?" - description: "Auto-commit before implementation planning" - before_tasks: - command: speckit.git.commit - optional: true - prompt: "Commit outstanding changes before task generation?" - description: "Auto-commit before task generation" - before_implement: - command: speckit.git.commit - optional: true - prompt: "Commit outstanding changes before implementation?" - description: "Auto-commit before implementation" - before_checklist: - command: speckit.git.commit - optional: true - prompt: "Commit outstanding changes before checklist?" - description: "Auto-commit before checklist generation" - before_analyze: - command: speckit.git.commit - optional: true - prompt: "Commit outstanding changes before analysis?" - description: "Auto-commit before analysis" - before_taskstoissues: - command: speckit.git.commit - optional: true - prompt: "Commit outstanding changes before issue sync?" - description: "Auto-commit before tasks-to-issues conversion" - after_constitution: - command: speckit.git.commit - optional: true - prompt: "Commit constitution changes?" - description: "Auto-commit after constitution update" - after_specify: - command: speckit.git.commit - optional: true - prompt: "Commit specification changes?" - description: "Auto-commit after specification" - after_clarify: - command: speckit.git.commit - optional: true - prompt: "Commit clarification changes?" - description: "Auto-commit after spec clarification" - after_plan: - command: speckit.git.commit - optional: true - prompt: "Commit plan changes?" - description: "Auto-commit after implementation planning" - after_tasks: - command: speckit.git.commit - optional: true - prompt: "Commit task changes?" - description: "Auto-commit after task generation" - after_implement: - command: speckit.git.commit - optional: true - prompt: "Commit implementation changes?" - description: "Auto-commit after implementation" - after_checklist: - command: speckit.git.commit - optional: true - prompt: "Commit checklist changes?" - description: "Auto-commit after checklist generation" - after_analyze: - command: speckit.git.commit - optional: true - prompt: "Commit analysis results?" - description: "Auto-commit after analysis" - after_taskstoissues: - command: speckit.git.commit - optional: true - prompt: "Commit after syncing issues?" - description: "Auto-commit after tasks-to-issues conversion" - tags: - "git" - "branching" diff --git a/extensions/git/scripts/bash/git-common.sh b/extensions/git/scripts/bash/git-common.sh index 882a385e28..f135d53114 100755 --- a/extensions/git/scripts/bash/git-common.sh +++ b/extensions/git/scripts/bash/git-common.sh @@ -11,31 +11,12 @@ has_git() { git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 } -# Validate that a branch name matches the expected feature branch pattern. -# Accepts sequential (###-* with >=3 digits) or timestamp (YYYYMMDD-HHMMSS-*) formats. +# AF fork: disabled branch name pattern validation. +# The upstream check required branches to match NNN-* or YYYYMMDD-HHMMSS-*, +# but feature.json is now the source of truth for feature directory resolution, +# and GIT_BRANCH_NAME allows arbitrary branch names. The pattern check is +# redundant — main/master blocking is handled by the AF before-specify hook. +# See: github/spec-kit#1680, #1900, #1901 check_feature_branch() { - local branch="$1" - local has_git_repo="$2" - - # For non-git repos, we can't enforce branch naming but still provide output - if [[ "$has_git_repo" != "true" ]]; then - echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 - return 0 - fi - - # Reject malformed timestamps (7-digit date, 8-digit date without trailing slug, or 7-digit with slug) - if [[ "$branch" =~ ^[0-9]{7}-[0-9]{6} ]] || [[ "$branch" =~ ^[0-9]{8}-[0-9]{6}$ ]]; then - echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2 - return 1 - fi - - # Accept sequential (>=3 digits followed by hyphen) or timestamp (YYYYMMDD-HHMMSS-*) - if [[ "$branch" =~ ^[0-9]{3,}- ]] || [[ "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then - return 0 - fi - - echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2 - return 1 + return 0 } diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 04af7d794f..da7d87466f 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -115,27 +115,12 @@ has_git() { } check_feature_branch() { - local branch="$1" - local has_git_repo="$2" - - # For non-git repos, we can't enforce branch naming but still provide output - if [[ "$has_git_repo" != "true" ]]; then - echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 - return 0 - fi - - # Accept sequential prefix (3+ digits) but exclude malformed timestamps - # Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022") - local is_sequential=false - if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then - is_sequential=true - fi - if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then - echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2 - return 1 - fi - + # AF fork: disabled branch name pattern validation. + # The upstream check required branches to match NNN-* or YYYYMMDD-HHMMSS-*, + # but feature.json is now the source of truth for feature directory resolution, + # and GIT_BRANCH_NAME allows arbitrary branch names. The pattern check is + # redundant — main/master blocking is handled by the AF before-specify hook. + # See: github/spec-kit#1680, #1900, #1901 return 0 } diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index a062fd5c83..752795c42b 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -650,12 +650,18 @@ def _install_shared_infra( project_path: Path, script_type: str, tracker: StepTracker | None = None, + force: bool = False, ) -> bool: """Install shared infrastructure files into *project_path*. Copies ``.specify/scripts/`` and ``.specify/templates/`` from the bundled core_pack or source checkout. Tracks all installed files in ``speckit.manifest.json``. + + AF fork: when *force* is True, overwrite existing files instead of + skipping them. This allows ``specify-af init --force`` to refresh + init-managed files without deleting user content. + Returns ``True`` on success. """ from .integrations.manifest import IntegrationManifest @@ -680,12 +686,12 @@ def _install_shared_infra( if variant_src.is_dir(): dest_variant = dest_scripts / variant_dir dest_variant.mkdir(parents=True, exist_ok=True) - # Merge without overwriting — only add files that don't exist yet + # Merge mode: skip existing files unless force is True for src_path in variant_src.rglob("*"): if src_path.is_file(): rel_path = src_path.relative_to(variant_src) dst_path = dest_variant / rel_path - if dst_path.exists(): + if dst_path.exists() and not force: skipped_files.append(str(dst_path.relative_to(project_path))) else: dst_path.parent.mkdir(parents=True, exist_ok=True) @@ -706,7 +712,7 @@ def _install_shared_infra( for f in templates_src.iterdir(): if f.is_file() and f.name != "vscode-settings.json" and not f.name.startswith("."): dst = dest_templates / f.name - if dst.exists(): + if dst.exists() and not force: skipped_files.append(str(dst.relative_to(project_path))) else: shutil.copy2(f, dst) @@ -1207,11 +1213,16 @@ def init( # Install shared infrastructure (scripts, templates) tracker.start("shared-infra") - _install_shared_infra(project_path, selected_script, tracker=tracker) + _install_shared_infra(project_path, selected_script, tracker=tracker, force=force) tracker.complete("shared-infra", f"scripts ({selected_script}) + templates") ensure_constitution_from_template(project_path, tracker=tracker) + # Auto-install bundled AF extensions (before git so AF hooks + # register first and fire before git hooks at each lifecycle phase) + from .af_init import install_af_extensions + install_af_extensions(project_path, tracker, get_speckit_version(), force=force) + if not no_git: tracker.start("git") git_messages = [] @@ -1285,10 +1296,6 @@ def init( init_opts["ai_skills"] = True save_init_options(project_path, init_opts) - # Auto-install bundled AF extensions - from .af_init import install_af_extensions - install_af_extensions(project_path, tracker, get_speckit_version()) - # Install preset if specified if preset: try: @@ -1493,52 +1500,6 @@ def check(): if not any(agent_results.values()): console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]") -@app.command() -def upgrade(): - """Update installed AppFolio extensions to match bundled versions.""" - show_banner() - - project_path = Path.cwd() - specify_dir = project_path / ".specify" - if not specify_dir.is_dir(): - console.print("[red]Error:[/red] No .specify directory found. Run [cyan]specify-af init[/cyan] first.") - raise typer.Exit(1) - - from .af_init import upgrade_af_extensions - - result = upgrade_af_extensions(project_path) - counts = result["counts"] - details = result["details"] - - tree = Tree("[cyan]Upgrade AppFolio Extensions[/cyan]", guide_style="grey50") - for ext_id, action, info in details: - if action == "failed": - tree.add(f"[red]●[/red] [white]{ext_id}[/white] [bright_black]({info})[/bright_black]") - elif action == "updated": - tree.add(f"[green]●[/green] [white]{ext_id}[/white] [bright_black]({info})[/bright_black]") - elif action == "installed": - tree.add(f"[green]●[/green] [white]{ext_id}[/white] [bright_black](installed {info})[/bright_black]") - else: - tree.add(f"[green]●[/green] [white]{ext_id}[/white] [bright_black](up-to-date {info})[/bright_black]") - - console.print(tree) - console.print() - - summary_parts = [] - if counts["updated"]: - summary_parts.append(f"{counts['updated']} updated") - if counts["installed"]: - summary_parts.append(f"{counts['installed']} installed") - if counts["up_to_date"]: - summary_parts.append(f"{counts['up_to_date']} up-to-date") - if counts["failed"]: - summary_parts.append(f"[red]{counts['failed']} failed[/red]") - - console.print(f"[bold]{', '.join(summary_parts)}[/bold]") - - if counts["failed"]: - raise typer.Exit(1) - @app.command() def version(): diff --git a/src/specify_cli/af_init.py b/src/specify_cli/af_init.py index cc6fe1fc4c..2e11ed476d 100644 --- a/src/specify_cli/af_init.py +++ b/src/specify_cli/af_init.py @@ -9,7 +9,7 @@ from . import StepTracker # Single source of truth for which AF extensions are bundled. -AF_EXTENSION_IDS = ["af"] +AF_EXTENSION_IDS = ["af", "git"] def _bundled_extension_path(ext_id: str) -> Path | None: @@ -40,11 +40,16 @@ def install_af_extensions( project_dir: Path, tracker: StepTracker, speckit_version: str, + force: bool = False, ) -> None: """Auto-install all bundled AF extensions during init. Idempotent: skips extensions already installed at the correct version, reinstalls if version differs, installs if missing. + + AF fork: when *force* is True, always remove and reinstall regardless + of version match. This allows ``--force`` to refresh extension files + during development without bumping the version each time. """ from .extensions import ExtensionManager, ExtensionError @@ -68,14 +73,17 @@ def install_af_extensions( installed_meta = manager.registry.get(ext_id) installed_version = installed_meta.get("version") if installed_meta else None - if installed_version == bundled_version: + if installed_version == bundled_version and not force: messages.append(f"{ext_id} up-to-date ({installed_version})") continue - # Version differs — remove then reinstall + # Version differs or force — remove then reinstall manager.remove(ext_id) manager.install_from_directory(bundled_path, speckit_version) - messages.append(f"{ext_id} updated ({installed_version} → {bundled_version})") + if force and installed_version == bundled_version: + messages.append(f"{ext_id} reinstalled ({bundled_version})") + else: + messages.append(f"{ext_id} updated ({installed_version} → {bundled_version})") else: manager.install_from_directory(bundled_path, speckit_version) messages.append(f"{ext_id} installed ({bundled_version})") @@ -85,6 +93,10 @@ def install_af_extensions( sanitized = str(err).replace("\n", " ").strip() messages.append(f"{ext_id}: {sanitized[:120]}") + # AF fork: extension install registers commands for all detected agents. + # Clean up Copilot files — we only use Claude. + _cleanup_copilot_files(project_dir) + summary = "; ".join(messages) if has_error: tracker.error("af-extensions", summary) @@ -92,50 +104,14 @@ def install_af_extensions( tracker.complete("af-extensions", summary) -def upgrade_af_extensions(project_dir: Path) -> dict: - """Compare installed AF extensions against bundled versions and sync. - - Returns a summary dict: {updated: N, installed: N, up_to_date: N, failed: N} - """ - from .extensions import ExtensionManager, ExtensionError - from . import get_speckit_version - - manager = ExtensionManager(project_dir) - speckit_version = get_speckit_version() - - counts = {"updated": 0, "installed": 0, "up_to_date": 0, "failed": 0} - details: list[tuple[str, str, str]] = [] # (ext_id, action, info) - - for ext_id in AF_EXTENSION_IDS: - try: - bundled_path = _bundled_extension_path(ext_id) - if bundled_path is None: - counts["failed"] += 1 - details.append((ext_id, "failed", "bundled path not found")) - continue - - bundled_version = _read_bundled_version(ext_id) - - if manager.registry.is_installed(ext_id): - installed_meta = manager.registry.get(ext_id) - installed_version = installed_meta.get("version") if installed_meta else None - - if installed_version == bundled_version: - counts["up_to_date"] += 1 - details.append((ext_id, "up-to-date", f"{installed_version}")) - continue - - manager.remove(ext_id) - manager.install_from_directory(bundled_path, speckit_version) - counts["updated"] += 1 - details.append((ext_id, "updated", f"{installed_version} → {bundled_version}")) - else: - manager.install_from_directory(bundled_path, speckit_version) - counts["installed"] += 1 - details.append((ext_id, "installed", f"{bundled_version}")) +def _cleanup_copilot_files(project_dir: Path) -> None: + """Remove Copilot agent/prompt files created by extension command registration.""" + import glob + for pattern in [ + str(project_dir / ".github" / "agents" / "speckit.*.agent.md"), + str(project_dir / ".github" / "prompts" / "speckit.*.prompt.md"), + ]: + for f in glob.glob(pattern): + Path(f).unlink(missing_ok=True) - except (ExtensionError, Exception) as err: - counts["failed"] += 1 - details.append((ext_id, "failed", str(err)[:120])) - return {"counts": counts, "details": details} diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index ec7af88768..d340628e4c 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -282,9 +282,11 @@ def build_skill_frontmatter( } if agent_name == "claude": # Claude skills should be user-invocable (accessible via /command) - # and only run when explicitly invoked (not auto-triggered by the model). + # AF fork: allow model invocation so lifecycle hooks can dispatch skills. + # Upstream tracks this via --allow-model-invocation flag (github/spec-kit#2098). + # Revert to `True` when upstream merges that fix and we integrate it. skill_frontmatter["user-invocable"] = True - skill_frontmatter["disable-model-invocation"] = True + skill_frontmatter["disable-model-invocation"] = False return skill_frontmatter @staticmethod diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 31972c4b0e..90859ba8cc 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -158,6 +158,14 @@ def setup( """Install Claude skills, then inject user-invocable, disable-model-invocation, and argument-hint.""" created = super().setup(project_root, manifest, parsed_options, **opts) + # Clean up stale .claude/commands/speckit.*.md files from pre-fork installs. + # The AF fork installs everything as skills (.claude/skills/), not commands. + # Leftover command files confuse Claude when both exist side-by-side. + commands_dir = project_root / ".claude" / "commands" + if commands_dir.is_dir(): + for stale in commands_dir.glob("speckit.*.md"): + stale.unlink() + # Post-process generated skill files skills_dir = self.skills_dest(project_root).resolve() diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index 43e6d225b1..b6bbfeeb50 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -23,28 +23,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Goal. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Goal @@ -208,26 +197,16 @@ After reporting, check if `.specify/extensions.yml` exists in the project root. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Operating Principles diff --git a/templates/commands/checklist.md b/templates/commands/checklist.md index 533046566b..9704ca53e2 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -44,28 +44,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Execution Steps. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Execution Steps @@ -341,24 +330,14 @@ Check if `.specify/extensions.yml` exists in the project root. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index d6d6bbe910..e5a53d878a 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -27,28 +27,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -227,24 +216,14 @@ Check if `.specify/extensions.yml` exists in the project root. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/constitution.md b/templates/commands/constitution.md index 29ae9a09e2..b5cd2f8ed5 100644 --- a/templates/commands/constitution.md +++ b/templates/commands/constitution.md @@ -24,28 +24,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -127,24 +116,14 @@ Check if `.specify/extensions.yml` exists in the project root. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 9a91d2dc4b..0889a104be 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -23,28 +23,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -178,24 +167,14 @@ Note: This command assumes a complete task breakdown exists in tasks.md. If task - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - - For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` + - Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. + - For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). + - After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 4f1e9ed295..908b1fcb9d 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -34,28 +34,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -82,26 +71,16 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - - For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` + - Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. + - For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). + - After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Phases @@ -145,7 +124,8 @@ You **MUST** consider the user input before proceeding (if not empty). - Skip if project is purely internal (build scripts, one-off tools, etc.) 3. **Agent context update**: - - Run `{AGENT_SCRIPT}` + - If `.specify/extensions/af/` exists, skip this step — the AF extension manages agent context separately. + - Otherwise, run `{AGENT_SCRIPT}` - These scripts detect which AI agent is in use - Update the appropriate agent-specific context file - Add only new technology from current plan diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 15c75ec396..f4578556ea 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -28,33 +28,22 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline -The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command. +The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. If `$ARGUMENTS` is empty, a before-hook may have provided context (e.g., a PRD). Check whether the Pre-Execution hooks produced a feature description or PRD context before treating empty arguments as an error. Given that feature description, do this: @@ -111,7 +100,7 @@ Given that feature description, do this: 5. Follow this execution flow: 1. Parse user description from arguments - If empty: ERROR "No feature description provided" + If empty and no before-hook provided context: ERROR "No feature description provided" 2. Extract key concepts from description Identify: actors, actions, data, constraints 3. For unclear aspects: @@ -241,26 +230,16 @@ Given that feature description, do this: - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - - For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` + - Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. + - For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). + - After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently **NOTE:** Branch creation is handled by the `before_specify` hook (git extension). Spec directory and file creation are always handled by this core command. diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 4e204abc1b..3e715fc7b2 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -32,28 +32,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -104,26 +93,16 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - - For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` + - Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. + - For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). + - After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently Context for task generation: {ARGS} diff --git a/templates/commands/taskstoissues.md b/templates/commands/taskstoissues.md index 77db7be130..b03530bd1c 100644 --- a/templates/commands/taskstoissues.md +++ b/templates/commands/taskstoissues.md @@ -24,28 +24,17 @@ You **MUST** consider the user input before proceeding (if not empty). - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` + - Wait for the user to choose before proceeding to the Outline. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -76,24 +65,14 @@ Check if `.specify/extensions.yml` exists in the project root. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` +- Execute hooks sequentially in YAML array order. Do not parallelize — each hook may depend on the output or side effects of the previous one. +- For each executable hook: + - **Mandatory hook** (`optional: false`): Execute it immediately via `EXECUTE_COMMAND: {command}`. Wait for it to complete before proceeding. + - **Optional hook** (`optional: true`): Collect it for presentation (see below). +- After all mandatory hooks have completed, if there are optional hooks to present: + - **IMPORTANT**: Remove any hook whose command contains `speckit.af.before-` or `speckit.af.after-` from this list. These are internal lifecycle hooks that already executed as mandatory hooks — they must NEVER appear in the user-facing list. + - Present the remaining hooks as a numbered list, with a final option to skip all: + - If the hook has a `prompt`: `#. {prompt} (/{command})` + - If no `prompt`: `#. To run /{command}` + - Last entry: `#. Continue without running any` - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py index e0222fd4de..bb42c1c13e 100644 --- a/tests/integrations/conftest.py +++ b/tests/integrations/conftest.py @@ -6,12 +6,13 @@ # 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.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.common", "af.spec-picker", ] AF_EXTENSION_FILES = [ @@ -19,8 +20,30 @@ ".specify/extensions/.registry", ".specify/extensions/af/README.md", ".specify/extensions/af/extension.yml", + ".specify/extensions/af/scripts/bash/check-version.sh", ] + [f".specify/extensions/af/commands/speckit.{cmd}.md" for cmd in AF_EXTENSION_COMMANDS] +GIT_EXTENSION_COMMANDS = [ + "git.commit", "git.feature", "git.initialize", "git.remote", "git.validate", +] + +GIT_EXTENSION_FILES = [ + ".specify/extensions/git/README.md", + ".specify/extensions/git/config-template.yml", + ".specify/extensions/git/extension.yml", + ".specify/extensions/git/git-config.yml", +] + [ + f".specify/extensions/git/commands/speckit.{cmd}.md" for cmd in GIT_EXTENSION_COMMANDS +] + [ + f".specify/extensions/git/scripts/bash/{s}" for s in [ + "auto-commit.sh", "create-new-feature.sh", "git-common.sh", "initialize-repo.sh", + ] +] + [ + f".specify/extensions/git/scripts/powershell/{s}" for s in [ + "auto-commit.ps1", "create-new-feature.ps1", "git-common.ps1", "initialize-repo.ps1", + ] +] + 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 49441dc709..3f8969bcb9 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -11,7 +11,7 @@ from specify_cli.integrations.base import MarkdownIntegration from specify_cli.integrations.manifest import IntegrationManifest -from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES, GIT_EXTENSION_COMMANDS, GIT_EXTENSION_FILES class MarkdownIntegrationTests: @@ -251,6 +251,10 @@ def _expected_files(self, script_variant: str) -> list[str]: files += AF_EXTENSION_FILES for cmd in AF_EXTENSION_COMMANDS: files.append(f"{cmd_dir}/speckit.{cmd}.md") + # Git bundled extension (installed via AF_EXTENSION_IDS) + files += GIT_EXTENSION_FILES + for cmd in GIT_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 5bf71b8061..941f492eb8 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -16,7 +16,7 @@ from specify_cli.integrations.base import SkillsIntegration from specify_cli.integrations.manifest import IntegrationManifest -from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES, GIT_EXTENSION_COMMANDS, GIT_EXTENSION_FILES class SkillsIntegrationTests: @@ -353,6 +353,10 @@ def _expected_files(self, script_variant: str) -> list[str]: files += AF_EXTENSION_FILES for cmd in AF_EXTENSION_COMMANDS: files.append(f"{skills_prefix}/speckit-{cmd.replace('.', '-')}/SKILL.md") + # Git bundled extension (installed via AF_EXTENSION_IDS) + files += GIT_EXTENSION_FILES + for cmd in GIT_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 4c62826ce1..a864bd0c0c 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -17,7 +17,7 @@ from specify_cli.integrations.base import TomlIntegration from specify_cli.integrations.manifest import IntegrationManifest -from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES, GIT_EXTENSION_COMMANDS, GIT_EXTENSION_FILES class TomlIntegrationTests: @@ -451,6 +451,10 @@ def _expected_files(self, script_variant: str) -> list[str]: files += AF_EXTENSION_FILES for cmd in AF_EXTENSION_COMMANDS: files.append(f"{cmd_dir}/speckit.{cmd}.toml") + # Git bundled extension (installed via AF_EXTENSION_IDS) + files += GIT_EXTENSION_FILES + for cmd in GIT_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 890315ca4b..c408f9109a 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -6,7 +6,7 @@ from specify_cli.integrations import get_integration from specify_cli.integrations.manifest import IntegrationManifest -from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES +from .conftest import AF_EXTENSION_COMMANDS, AF_EXTENSION_FILES, GIT_EXTENSION_COMMANDS, GIT_EXTENSION_FILES class TestCopilotIntegration: @@ -201,11 +201,7 @@ 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 - ]) + ] + AF_EXTENSION_FILES + GIT_EXTENSION_FILES) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}" @@ -265,11 +261,7 @@ 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 - ]) + ] + AF_EXTENSION_FILES + GIT_EXTENSION_FILES) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}" diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index 2fa8dac3d9..b4bd902262 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -8,7 +8,7 @@ from specify_cli.integrations.base import MarkdownIntegration from specify_cli.integrations.manifest import IntegrationManifest -from .conftest import AF_EXTENSION_FILES +from .conftest import AF_EXTENSION_FILES, GIT_EXTENSION_COMMANDS, GIT_EXTENSION_FILES class TestGenericIntegration: @@ -250,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) + ] + AF_EXTENSION_FILES + GIT_EXTENSION_FILES) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}" @@ -306,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) + ] + AF_EXTENSION_FILES + GIT_EXTENSION_FILES) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}"