From 69ea6b873cb7caf068e77c8843030479948cf9a5 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:37:46 -0600 Subject: [PATCH 1/3] fix: wire after_tasks and after_implement hook events into command templates (#1701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HookExecutor backend in extensions.py was fully implemented but check_hooks_for_event() was never called by anything — the core command templates had no instructions to check .specify/extensions.yml. Add a final step to templates/commands/tasks.md (step 6, after_tasks) and templates/commands/implement.md (step 10, after_implement) that instructs the AI agent to: - Read .specify/extensions.yml if it exists - Filter hooks.{event} to enabled: true entries - Evaluate any condition fields and skip non-matching hooks - Output the RFC-specified hook message format, including EXECUTE_COMMAND: markers for mandatory (optional: false) hooks Bumps version to 0.1.7. --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- templates/commands/implement.md | 26 ++++++++++++++++++++++++++ templates/commands/tasks.md | 26 ++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 741ce5d0cb..02a6ffb304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ Recent changes to the Specify CLI and templates are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.7] - 2026-02-26 + +### Fixed + +- **Extension hooks never triggered (#1701)**: Wired `after_tasks` and `after_implement` hook events into their respective command templates + - `templates/commands/tasks.md` now includes a final step that reads `.specify/extensions.yml`, evaluates registered `after_tasks` hooks (respecting `enabled` flag and `condition`), and outputs the correct hook message format — including `EXECUTE_COMMAND:` markers for mandatory hooks + - `templates/commands/implement.md` does the same for `after_implement` hooks + - The `HookExecutor` backend in `extensions.py` was already complete; only the template wiring was missing + ## [0.1.6] - 2026-02-23 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 5f6a2eb7ab..ff327ea405 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.1.6" +version = "0.1.7" description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." requires-python = ">=3.11" dependencies = [ diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 39abb1e6c8..bdfb3a7d8b 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -136,3 +136,29 @@ You **MUST** consider the user input before proceeding (if not empty). - Report final status with summary of completed work Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list. + +10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_implement` key + - Filter to only hooks where `enabled: true` + - For each remaining hook, evaluate any `condition` value; skip the hook if the condition is not met + - 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} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 7320b6f305..84494791cb 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -63,6 +63,32 @@ You **MUST** consider the user input before proceeding (if not empty). - Suggested MVP scope (typically just User Story 1) - Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths) +6. **Check for extension hooks**: After tasks.md is generated, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_tasks` key + - Filter to only hooks where `enabled: true` + - For each remaining hook, evaluate any `condition` value; skip the hook if the condition is not met + - 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} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + Context for task generation: {ARGS} The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context. From cf6c7ae5194c6b36e7bd73048f057d0554ab8cb4 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:24:42 -0600 Subject: [PATCH 2/3] fix: clarify hook condition handling and add YAML error guidance in templates - Replace ambiguous "evaluate any condition value" instruction with explicit guidance to skip hooks with non-empty conditions, deferring evaluation to HookExecutor - Add instruction to skip hook checking silently if extensions.yml cannot be parsed or is invalid --- templates/commands/implement.md | 5 ++++- templates/commands/tasks.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/templates/commands/implement.md b/templates/commands/implement.md index bdfb3a7d8b..0046421072 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -139,8 +139,11 @@ Note: This command assumes a complete task breakdown exists in tasks.md. If task 10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_implement` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally - Filter to only hooks where `enabled: true` - - For each remaining hook, evaluate any `condition` value; skip the hook if the condition is not met + - 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`): ``` diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 84494791cb..df45a2d94c 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -65,8 +65,11 @@ You **MUST** consider the user input before proceeding (if not empty). 6. **Check for extension hooks**: After tasks.md is generated, check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_tasks` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally - Filter to only hooks where `enabled: true` - - For each remaining hook, evaluate any `condition` value; skip the hook if the condition is not met + - 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`): ``` From 5728b5c61107175cee9c70593c1b54378c24e07d Mon Sep 17 00:00:00 2001 From: Dhilip Date: Wed, 4 Mar 2026 08:55:40 -0500 Subject: [PATCH 3/3] Fix/extension hooks not triggered (#1) * feat(templates): implement before-hooks check as pre-execution phase * test(hooks): create scenario for LLMs/Agents on hooks --- templates/commands/implement.md | 34 +++++++++++++++++++++++++++++ templates/commands/tasks.md | 34 +++++++++++++++++++++++++++++ tests/hooks/.specify/extensions.yml | 34 +++++++++++++++++++++++++++++ tests/hooks/TESTING.md | 30 +++++++++++++++++++++++++ tests/hooks/plan.md | 3 +++ tests/hooks/spec.md | 1 + tests/hooks/tasks.md | 1 + 7 files changed, 137 insertions(+) create mode 100644 tests/hooks/.specify/extensions.yml create mode 100644 tests/hooks/TESTING.md create mode 100644 tests/hooks/plan.md create mode 100644 tests/hooks/spec.md create mode 100644 tests/hooks/tasks.md diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 0046421072..e67fd8c44c 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -13,6 +13,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before implementation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_implement` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter to only hooks where `enabled: true` +- 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. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index df45a2d94c..9ad199634d 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -22,6 +22,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before tasks generation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_tasks` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter to only hooks where `enabled: true` +- 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. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). diff --git a/tests/hooks/.specify/extensions.yml b/tests/hooks/.specify/extensions.yml new file mode 100644 index 0000000000..0a73b3254d --- /dev/null +++ b/tests/hooks/.specify/extensions.yml @@ -0,0 +1,34 @@ +hooks: + before_implement: + - id: pre_test + enabled: true + optional: false + extension: "test-extension" + command: "pre_implement_test" + description: "Test before implement hook execution" + + after_implement: + - id: post_test + enabled: true + optional: true + extension: "test-extension" + command: "post_implement_test" + description: "Test after implement hook execution" + prompt: "Would you like to run the post-implement test?" + + before_tasks: + - id: pre_tasks_test + enabled: true + optional: false + extension: "test-extension" + command: "pre_tasks_test" + description: "Test before tasks hook execution" + + after_tasks: + - id: post_tasks_test + enabled: true + optional: true + extension: "test-extension" + command: "post_tasks_test" + description: "Test after tasks hook execution" + prompt: "Would you like to run the post-tasks test?" diff --git a/tests/hooks/TESTING.md b/tests/hooks/TESTING.md new file mode 100644 index 0000000000..6ab704442f --- /dev/null +++ b/tests/hooks/TESTING.md @@ -0,0 +1,30 @@ +# Testing Extension Hooks + +This directory contains a mock project to verify that LLM agents correctly identify and execute hook commands defined in `.specify/extensions.yml`. + +## Test 1: Testing `before_tasks` and `after_tasks` + +1. Open a chat with an LLM (like GitHub Copilot) in this project. +2. Ask it to generate tasks for the current directory: + > "Please follow `/speckit.tasks` for the `./tests/hooks` directory." +3. **Expected Behavior**: + - Before doing any generation, the LLM should notice the `AUTOMATIC Pre-Hook` in `.specify/extensions.yml` under `before_tasks`. + - It should state it is executing `EXECUTE_COMMAND: pre_tasks_test`. + - It should then proceed to read the `.md` docs and produce a `tasks.md`. + - After generation, it should output the optional `after_tasks` hook (`post_tasks_test`) block, asking if you want to run it. + +## Test 2: Testing `before_implement` and `after_implement` + +*(Requires `tasks.md` from Test 1 to exist)* + +1. In the same (or new) chat, ask the LLM to implement the tasks: + > "Please follow `/speckit.implement` for the `./tests/hooks` directory." +2. **Expected Behavior**: + - The LLM should first check for `before_implement` hooks. + - It should state it is executing `EXECUTE_COMMAND: pre_implement_test` BEFORE doing any actual task execution. + - It should evaluate the checklists and execute the code writing tasks. + - Upon completion, it should output the optional `after_implement` hook (`post_implement_test`) block. + +## How it works + +The templates for these commands in `templates/commands/tasks.md` and `templates/commands/implement.md` contains strict ordered lists. The new `before_*` hooks are explicitly formulated in a **Pre-Execution Checks** section prior to the outline to ensure they're evaluated first without breaking template step numbers. diff --git a/tests/hooks/plan.md b/tests/hooks/plan.md new file mode 100644 index 0000000000..e2694887d1 --- /dev/null +++ b/tests/hooks/plan.md @@ -0,0 +1,3 @@ +# Test Setup for Hooks + +This feature is designed to test if LLMs correctly invoke Spec Kit extensions hooks when generating tasks and implementing code. diff --git a/tests/hooks/spec.md b/tests/hooks/spec.md new file mode 100644 index 0000000000..0285468a67 --- /dev/null +++ b/tests/hooks/spec.md @@ -0,0 +1 @@ +- **User Story 1:** I want a test script that prints "Hello hooks!". diff --git a/tests/hooks/tasks.md b/tests/hooks/tasks.md new file mode 100644 index 0000000000..3c22b0b2ac --- /dev/null +++ b/tests/hooks/tasks.md @@ -0,0 +1 @@ +- [ ] T001 [US1] Create script that prints 'Hello hooks!' in hello.py