diff --git a/templates/commands/implement.md b/templates/commands/implement.md index a565f50e7..7c0f90403 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"). @@ -136,3 +170,32 @@ 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 + - 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 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 7320b6f30..9ad199634 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"). @@ -63,6 +97,35 @@ 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 + - 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 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. diff --git a/tests/hooks/.specify/extensions.yml b/tests/hooks/.specify/extensions.yml new file mode 100644 index 000000000..0a73b3254 --- /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 000000000..6ab704442 --- /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 000000000..e2694887d --- /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 000000000..0285468a6 --- /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 000000000..3c22b0b2a --- /dev/null +++ b/tests/hooks/tasks.md @@ -0,0 +1 @@ +- [ ] T001 [US1] Create script that prints 'Hello hooks!' in hello.py