Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions design/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ adding per-agent wrapper scripts. For `generic`, extension registration
resolves the persisted `--commands-dir` rather than the static registry
placeholder; `--skills` emits skills into that same directory.

Core command templates also use `{PRE_HOOK_SCRIPT}` and `{POST_HOOK_SCRIPT}`.
Rendering selects the same `sh`, `ps`, or `py` variant as the command's main
script; extension installation provisions the native pre/post entry points
for each hook-bearing command's selected variant, including fallback variants
when the project preference is unavailable. The CLI validates hook configuration on write and
materializes ordered per-event JSON with a snapshot of `.specify/extensions.yml`.
CLI writers serialize publication, and each variant checks the snapshot,
event-index digest, and generated response digest after normalizing CRLF line
endings to LF, so tracked projections survive Git checkout conversion. Each
reads the projection in its own runtime, without depending on another variant
or on runtime PyYAML; it rejects stale, corrupt, or missing projections.
The legacy YAML resolver remains for projects not yet refreshed. The resolver
returns ordered hook metadata as JSON; agent commands themselves remain the
responsibility of the agent. The command/skill registrar resolves these
placeholders too when a preset wraps a core command.

## External adapter package contract

A standalone ZIP, tar.gz, or tgz archive needs only these root files (a single
Expand Down
20 changes: 15 additions & 5 deletions docs/reference/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Spec Kit stores project-level extension registration and hook configuration in:
.specify/extensions.yml
```

The file contains installed extensions, global settings, and hooks that are surfaced before or after Spec Kit commands.
The file contains installed extensions, global settings, and hooks that are resolved by pre/post scripts before or after Spec Kit commands.

```yaml
installed:
Expand Down Expand Up @@ -337,17 +337,27 @@ Each hook entry supports the following fields:
| `extension` | ID of the extension that registered the hook. |
| `command` | Extension command associated with the hook. |
| `enabled` | Whether the hook is active. Hooks with `enabled: false` are skipped. |
| `optional` | Whether the hook is optional. If `true`, the hook is presented with its `prompt` and can be skipped; if `false`, the hook is emitted as an automatic hook (includes `EXECUTE_COMMAND` markers). |
| `priority` | Priority metadata for the hook. Registered hook entries use integer values >= 1; entries installed from manifests default to `10` when no priority is declared. Current command templates surface hooks in their configured YAML order and do not sort them by `priority`. |
| `optional` | Whether the hook is optional. If `true`, the hook is presented with its `prompt` and can be skipped; if `false`, the agent invokes it and waits for completion. |
| `priority` | Registered hook entries use integer values >= 1; entries installed from manifests default to `10` when no priority is declared. Core command dispatchers sort by ascending priority, preserving file order for ties. |
| `prompt` | Message shown when asking whether to run an optional hook. |
| `description` | Human-readable explanation of what the hook does. |
| `condition` | Optional expression evaluated by `HookExecutor` (using `config.<path>` or `env.<VAR>` with `is set`, `==`, or `!=`). Current command templates do not evaluate conditions and skip hooks with a non-empty condition. |
| `condition` | Optional expression evaluated by `HookExecutor` (using `config.<path>` or `env.<VAR>` with `is set`, `==`, or `!=`). Core command dispatchers preserve the existing prompt behavior: skip hooks with a non-empty condition rather than evaluating it. |

Hook event names identify when a hook is invoked. They generally use `before_<command>` or `after_<command>`, such as `before_implement`, `after_implement`, `before_tasks`, and `after_tasks`.

Extension manifests reject invalid hook priorities during installation. For existing `.specify/extensions.yml` entries, `HookExecutor.get_hooks_for_event()` sorts with `normalize_priority()`: missing values, booleans, non-numeric values rejected by `int()`, and values less than `1` fall back to `10`; numeric strings and finite floats are coerced with `int()`, while non-finite floats are unsupported and may fail instead of falling back.

`HookExecutor.get_hooks_for_event()` returns hooks ordered by `priority`, with lower values first. However, current command templates read hook lists directly and surface them in their configured YAML order rather than using priority ordering.
Core commands run two hook dispatchers, one before and one after their main work, using the selected `sh`, `ps`, or `py` variant of each command (which may fall back from the project's preferred script type). Installing a hook-bearing extension command provisions its selected dispatchers, including when the extension is updated. Each variant returns JSON containing the event and an ordered `hooks` array (`extension`, `command`, `optional`, `description`, `prompt`, `priority`). An absent configuration or event returns an empty array. An unreadable or invalid configuration returns an `error` and a nonzero exit code; agents must report that no hooks were checked, including mandatory hooks, and then continue the core command. The dispatchers determine the order but do not execute agent commands.

`specify extension add` and other CLI operations that save `.specify/extensions.yml` also materialize validated, ordered JSON responses under `.specify/hook-dispatch/`. All three script variants use this projection without parsing YAML; the Python script needs only the standard library (but still requires Python 3.11+ on the execution host), and Bash and PowerShell do not invoke Python. CLI saves serialize publication, and the scripts compare the saved YAML snapshot and verify the event index and each response against their generated SHA-256 digests. Readers normalize CRLF to LF before comparison and hashing, so Git line-ending conversion of tracked projection files does not invalidate the generated digests; standalone CR bytes are not normalized. They recheck the snapshot before returning, so a concurrent save cannot silently return a mixed response; a reader caught mid-save fails and can be retried. They fail with refresh guidance if the configuration was manually edited, a projected response was corrupted, the projection is incomplete, or its directory is missing. Bash requires `sed` and either `sha256sum` or `shasum` on the execution host to verify projected responses. Reinstall the affected extension with `specify extension add <name> --force` or refresh the project to regenerate it. Existing projects without a projection continue using their installed YAML resolvers until refreshed; their Python resolver still requires PyYAML.

Direct CLI configuration saves refuse a symlinked `.specify/extensions.yml` or `.specify` directory before changing the projection, rather than following the link outside the project.

The CLI validates hook fields across every configured event when materializing the projection. Extension installation checks projected hook event names (`before_...` or `after_...`) before modifying the project; manifest inspection still accepts other event names. Priorities are truncated to integers in the range `1` through `2147483647`; values outside this range, booleans, and non-integer strings fall back to `10`. Quoted integer strings are accepted, but quoted decimal strings are not.

Native dispatchers also recognize the numeric YAML forms accepted by the Python resolver, including digit separators, hex/binary/octal integers, sexagesimal integers, and decimal floats with exponents. The canonical writer quotes strings that would otherwise be ambiguous to the native parsers and preserves YAML control-character escapes. A NUL character in hook configuration is rejected explicitly because Bash cannot represent it.

Legacy Bash and PowerShell resolvers (before projection generation) support the CLI's pretty-printed `.specify/extensions.yml` layout: a `hooks:` mapping, `before_...` or `after_...` event names indented two spaces, list items under each event, and single-line scalar fields (including quoted strings and common escapes). All three legacy resolvers reject unsupported event names, including names for events other than the one being resolved. An inline empty `hooks: {}` mapping cannot contain nested entries. Other YAML constructs such as anchors, flow-style hook lists, and block scalars are not supported by the legacy path. The CLI writes hook configuration in this canonical layout without wrapping long scalar values; installing or upgrading shared infrastructure rewrites existing valid YAML to this layout and generates its projection (YAML comments and formatting are not retained). The Python legacy path accepts general YAML through PyYAML. A hook `condition` must be a string or null in all variants.

## FAQ

Expand Down
33 changes: 17 additions & 16 deletions extensions/EXTENSION-API-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,26 +663,27 @@ hooks:
condition: null
```

### Hook Message Format
### Core Command Hook Dispatch

```markdown
## Extension Hooks

**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Core commands call a pre-hook script before their main work and a post-hook script afterward. The scripts return one JSON result for each event:

Prompt: {prompt}
To execute: `/{command}`
```json
{
"event": "after_tasks",
"hooks": [
{
"extension": "jira",
"command": "speckit.jira.specstoissues",
"optional": true,
"priority": 10,
"prompt": "Create Jira issues from tasks?",
"description": "Create issues"
}
]
}
```

Or for mandatory hooks:

```markdown
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
Enabled, unconditional hooks are returned in ascending priority order (ties retain configuration order). The agent invokes mandatory commands and offers optional commands without automatically running them. Conditions remain unsupported in core-command dispatch and hooks with non-empty conditions are skipped. Invalid or unreadable configuration produces a nonzero exit and JSON with an `error` and empty `hooks` array; the agent reports that no hooks were checked.

---

Expand Down
2 changes: 1 addition & 1 deletion extensions/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"github": {
"name": "GitHub Integration",
"id": "github",
"version": "1.0.2",
"version": "1.0.3",
"description": "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list",
"author": "spec-kit-core",
"repository": "https://github.com/github/spec-kit",
Expand Down
10 changes: 6 additions & 4 deletions extensions/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ specify extension remove github

This extension consumes the existing `before_taskstoissues` and `after_taskstoissues` hook points, which are read from `.specify/extensions.yml` at run time. The hook keys are unchanged from the core command, so hooks registered by other extensions — for example the `git` extension's auto-commit hooks — keep firing exactly as before.

Installing or updating the extension also installs the selected shared pre/post
hook dispatchers under `.specify/scripts/` if an older project lacks them.
Unmodified managed copies are refreshed; customized scripts are preserved.

## Requirements

- Spec Kit **0.12.17 or newer**. Extension-local `scripts/...` path rewriting arrived in
0.12.6, but auto-registered skills did not resolve `__SPECKIT_COMMAND_*__` references
until 0.12.17. Earlier releases cannot render this command correctly in every supported
layout, so `specify extension add github` refuses to install below 0.12.17.
- Spec Kit **1.1.3 or newer**. Older releases cannot resolve the hook
placeholders or install the shared pre/post dispatchers used by this command.
- A Git remote pointing at GitHub.
- The **GitHub MCP server** available to your coding agent, providing the `list_issues` and `issue_write` tools.

Expand Down
64 changes: 2 additions & 62 deletions extensions/github/commands/speckit.github.taskstoissues.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,7 @@ You **MUST** consider the user input before proceeding (if not empty).

## Pre-Execution Checks

**Check for extension hooks (before tasks-to-issues conversion)**:
- Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.before_taskstoissues` key
- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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.
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
Run `{PRE_HOOK_SCRIPT} taskstoissues` from the project root and read its JSON result. If it fails or returns `error`, tell the user why no hooks were checked (including mandatory hooks), then continue the core command. For each returned hook in order: invoke mandatory commands in this agent and wait for completion before proceeding; surface optional commands with their prompt and description without executing them automatically. Use the invocation syntax for the installed agent/skills mode. If `hooks` is empty, continue silently.

## Outline

Expand All @@ -74,33 +43,4 @@ git config --get remote.origin.url

## Post-Execution Checks

**Check for extension hooks (after tasks-to-issues conversion)**:
Check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_taskstoissues` key
- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
- 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}
```
After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook.
- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently
Run `{POST_HOOK_SCRIPT} taskstoissues` from the project root before ending the command and read its JSON result. If it fails or returns `error`, tell the user why no hooks were checked (including mandatory hooks), then continue reporting completion. For each returned hook in order: invoke mandatory commands in this agent and wait for completion before ending; surface optional commands with their prompt and description without executing them automatically. Use the invocation syntax for the installed agent/skills mode. If `hooks` is empty, continue silently.
10 changes: 4 additions & 6 deletions extensions/github/extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ schema_version: "1.0"
extension:
id: github
name: "GitHub Integration"
version: "1.0.2"
version: "1.0.3"
Comment thread
Copilot marked this conversation as resolved.
description: "GitHub platform integration for Spec Kit - create GitHub issues from a feature's task list"
author: spec-kit-core
repository: https://github.com/github/spec-kit
license: MIT

requires:
# 0.12.17 is the first release that both rewrites extension-local
# "scripts/..." paths (#3364) and resolves __SPECKIT_COMMAND_*__ tokens in
# auto-registered skills (#3544). Earlier versions cannot render this
# command correctly in every registerable layout.
speckit_version: ">=0.12.17"
# Hook placeholders and shared dispatcher installation first ship in 1.1.3.
# Allow its development build when testing this bundled extension.
speckit_version: ">=1.1.3.dev0"
tools:
- name: git
required: true
Expand Down
5 changes: 5 additions & 0 deletions scripts/bash/post-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

SCRIPT_DIR=${BASH_SOURCE[0]%/*}
source "$SCRIPT_DIR/pre-hooks.sh"
resolve_hooks after "$1"
Loading
Loading