Skip to content

Commit 4064b02

Browse files
committed
feat: wire before/after hooks for specify and plan commands (#1788)
1 parent 56095f0 commit 4064b02

6 files changed

Lines changed: 157 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14+
- feat: wire `before_specify`, `after_specify`, `before_plan`, and `after_plan` hook events into command templates (#1788)
1415
- feat(extensions): support `.extensionignore` to exclude files/folders during `specify extension add` (#1781)
1516

1617
## [0.2.0] - 2026-03-09

extensions/EXTENSION-DEVELOPMENT-GUIDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,13 @@ Integration hooks for automatic execution.
196196

197197
Available hook points:
198198

199+
- `before_specify`: Before `/speckit.specify` starts
200+
- `after_specify`: After `/speckit.specify` completes
201+
- `before_plan`: Before `/speckit.plan` starts
202+
- `after_plan`: After `/speckit.plan` completes
203+
- `before_tasks`: Before `/speckit.tasks` starts
199204
- `after_tasks`: After `/speckit.tasks` completes
205+
- `before_implement`: Before `/speckit.implement` starts
200206
- `after_implement`: After `/speckit.implement` completes (future)
201207

202208
Hook object:

extensions/EXTENSION-USER-GUIDE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,28 @@ settings:
388388
389389
# Hook configuration
390390
hooks:
391+
before_specify:
392+
- extension: contextual-research
393+
command: speckit.research.pre-spec
394+
enabled: true
395+
optional: true
396+
prompt: "Perform pre-specification research?"
397+
after_specify:
398+
- extension: linter
399+
command: speckit.linter.check-spec
400+
enabled: true
401+
optional: false
402+
before_plan:
403+
- extension: setup-env
404+
command: speckit.env.prepare
405+
enabled: true
406+
optional: false
407+
after_plan:
408+
- extension: architect
409+
command: speckit.architect.validate-plan
410+
enabled: true
411+
optional: true
412+
prompt: "Validate architecture before proceeding?"
391413
after_tasks:
392414
- extension: jira
393415
command: speckit.jira.specstoissues

src/specify_cli/extensions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,7 +1884,7 @@ def get_hooks_for_event(self, event_name: str) -> List[Dict[str, Any]]:
18841884
"""Get all registered hooks for a specific event.
18851885
18861886
Args:
1887-
event_name: Name of the event (e.g., 'after_tasks')
1887+
event_name: Name of the event (e.g., 'after_specify', 'after_plan', 'after_tasks')
18881888
18891889
Returns:
18901890
List of hook configurations
@@ -2036,7 +2036,7 @@ def check_hooks_for_event(self, event_name: str) -> Dict[str, Any]:
20362036
This method is designed to be called by AI agents after core commands complete.
20372037
20382038
Args:
2039-
event_name: Name of the event (e.g., 'after_spec', 'after_tasks')
2039+
event_name: Name of the event (e.g., 'after_specify', 'after_plan', 'after_tasks')
20402040
20412041
Returns:
20422042
Dictionary with hook information:

templates/commands/plan.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,40 @@ $ARGUMENTS
2424

2525
You **MUST** consider the user input before proceeding (if not empty).
2626

27+
## Pre-Execution Checks
28+
29+
**Check for extension hooks (before planning generation)**:
30+
- Check if `.specify/extensions.yml` exists in the project root.
31+
- If it exists, read it and look for entries under the `hooks.before_plan` key
32+
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
33+
- Filter to only hooks where `enabled: true`
34+
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
35+
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
36+
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
37+
- For each executable hook, output the following based on its `optional` flag:
38+
- **Optional hook** (`optional: true`):
39+
```
40+
## Extension Hooks
41+
42+
**Optional Pre-Hook**: {extension}
43+
Command: `/{command}`
44+
Description: {description}
45+
46+
Prompt: {prompt}
47+
To execute: `/{command}`
48+
```
49+
- **Mandatory hook** (`optional: false`):
50+
```
51+
## Extension Hooks
52+
53+
**Automatic Pre-Hook**: {extension}
54+
Executing: `/{command}`
55+
EXECUTE_COMMAND: {command}
56+
57+
Wait for the result of the hook command before proceeding to the Outline.
58+
```
59+
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
60+
2761
## Outline
2862
2963
1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. 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").
@@ -41,6 +75,35 @@ You **MUST** consider the user input before proceeding (if not empty).
4175
4276
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
4377
78+
5. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root.
79+
- If it exists, read it and look for entries under the `hooks.after_plan` key
80+
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
81+
- Filter to only hooks where `enabled: true`
82+
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
83+
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
84+
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
85+
- For each executable hook, output the following based on its `optional` flag:
86+
- **Optional hook** (`optional: true`):
87+
```
88+
## Extension Hooks
89+
90+
**Optional Hook**: {extension}
91+
Command: `/{command}`
92+
Description: {description}
93+
94+
Prompt: {prompt}
95+
To execute: `/{command}`
96+
```
97+
- **Mandatory hook** (`optional: false`):
98+
```
99+
## Extension Hooks
100+
101+
**Automatic Hook**: {extension}
102+
Executing: `/{command}`
103+
EXECUTE_COMMAND: {command}
104+
```
105+
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
106+
44107
## Phases
45108
46109
### Phase 0: Outline & Research

templates/commands/specify.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,40 @@ $ARGUMENTS
2121

2222
You **MUST** consider the user input before proceeding (if not empty).
2323

24+
## Pre-Execution Checks
25+
26+
**Check for extension hooks (before specification generation)**:
27+
- Check if `.specify/extensions.yml` exists in the project root.
28+
- If it exists, read it and look for entries under the `hooks.before_specify` key
29+
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
30+
- Filter to only hooks where `enabled: true`
31+
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
32+
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
33+
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
34+
- For each executable hook, output the following based on its `optional` flag:
35+
- **Optional hook** (`optional: true`):
36+
```
37+
## Extension Hooks
38+
39+
**Optional Pre-Hook**: {extension}
40+
Command: `/{command}`
41+
Description: {description}
42+
43+
Prompt: {prompt}
44+
To execute: `/{command}`
45+
```
46+
- **Mandatory hook** (`optional: false`):
47+
```
48+
## Extension Hooks
49+
50+
**Automatic Pre-Hook**: {extension}
51+
Executing: `/{command}`
52+
EXECUTE_COMMAND: {command}
53+
54+
Wait for the result of the hook command before proceeding to the Outline.
55+
```
56+
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
57+
2458
## Outline
2559
2660
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.
@@ -176,6 +210,35 @@ Given that feature description, do this:
176210
177211
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
178212
213+
8. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root.
214+
- If it exists, read it and look for entries under the `hooks.after_specify` key
215+
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
216+
- Filter to only hooks where `enabled: true`
217+
- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions:
218+
- If the hook has no `condition` field, or it is null/empty, treat the hook as executable
219+
- If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation
220+
- For each executable hook, output the following based on its `optional` flag:
221+
- **Optional hook** (`optional: true`):
222+
```
223+
## Extension Hooks
224+
225+
**Optional Hook**: {extension}
226+
Command: `/{command}`
227+
Description: {description}
228+
229+
Prompt: {prompt}
230+
To execute: `/{command}`
231+
```
232+
- **Mandatory hook** (`optional: false`):
233+
```
234+
## Extension Hooks
235+
236+
**Automatic Hook**: {extension}
237+
Executing: `/{command}`
238+
EXECUTE_COMMAND: {command}
239+
```
240+
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
241+
179242
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
180243
181244
## General Guidelines

0 commit comments

Comments
 (0)