Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 35 additions & 29 deletions extensions/git/commands/speckit.git.commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,49 @@ description: "Auto-commit changes after a Spec Kit command completes"

Automatically stage and commit all changes after a Spec Kit command completes.

## Behavior
## Instructions

This command is invoked as a hook after (or before) core commands. It:
Follow these steps **exactly**. Do NOT skip the config file read or assume default values.

1. Determines the event name from the hook context (e.g., if invoked as an `after_specify` hook, the event is `after_specify`; if `before_plan`, the event is `before_plan`)
2. Checks `.specify/extensions/git/git-config.yml` for the `auto_commit` section
3. Looks up the specific event key to see if auto-commit is enabled
4. Falls back to `auto_commit.default` if no event-specific key exists
5. Uses the per-command `message` if configured, otherwise a default message
6. If enabled and there are uncommitted changes, runs `git add .` + `git commit`
### Step 1 — Determine the event name

## Execution
Identify the hook event name to use:
- If the invocation includes an explicit event name argument (e.g., `/speckit.git.commit after_tasks`), use it.
- Otherwise infer it from the surrounding hook context (e.g., `after_tasks`, `before_plan`), matching `hooks.<event>` in `.specify/extensions.yml`.
- If invoked manually with no hook context and no event argument, ask the user which event to use.

Determine the event name from the hook that triggered this command, then run the script:
### Step 2 — Read the configuration file

- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>`
- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>`
**You MUST read** the file `.specify/extensions/git/git-config.yml` before deciding whether to commit. Do NOT assume its contents — the user may have changed the defaults.

Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`).
If the file does not exist, auto-commit is disabled. Exit silently.

## Configuration
### Step 3 — Check whether auto-commit is enabled

In `.specify/extensions/git/git-config.yml`:
Look under the `auto_commit:` section in the config file you just read:

```yaml
auto_commit:
default: false # Global toggle — set true to enable for all commands
after_specify:
enabled: true # Override per-command
message: "[Spec Kit] Add specification"
after_plan:
enabled: false
message: "[Spec Kit] Add implementation plan"
```
1. Find the key matching the event name (e.g., `after_tasks:`).
2. If the event key exists **and** has `enabled: true` → auto-commit is **enabled**. Use the `message` value from that key.
3. If the event key exists **and** has `enabled: false` → auto-commit is **disabled**. Exit silently.
Comment on lines +30 to +32
4. If the event key does **not** exist at all, check `auto_commit.default`:
- `default: true` → auto-commit is **enabled**. Use a default message `"[Spec Kit] Auto-commit <before|after> <command>"` (e.g., `after_tasks` → `"[Spec Kit] Auto-commit after tasks"`).
- `default: false` or missing → auto-commit is **disabled**. Exit silently.

## Graceful Degradation
### Step 4 — Execute the commit (only if enabled)

- If Git is not available or the current directory is not a repository: skips with a warning
- If no config file exists: skips (disabled by default)
- If no changes to commit: skips with a message
If auto-commit is enabled:

**Option A — Run the script** (preferred, handles edge cases):
- **Bash (macOS/Linux)**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>`
- **PowerShell (Windows)**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>`

**Option B — Run git directly** (if scripts are unavailable):
1. Check for uncommitted changes: `git status --porcelain`
2. If there are changes, run `git add .` then `git commit -m "<message>"`
3. Report the result

### Graceful Degradation

- Git not available or not a repository → skip with a warning
- No config file → skip silently (disabled by default)
- No changes to commit → skip with a brief message
8 changes: 5 additions & 3 deletions src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2799,16 +2799,18 @@ def format_hook_message(

if optional:
lines.append(f"\n**Optional Hook**: {extension}")
lines.append(f"Hook event: {event_name}")
lines.append(f"Command: `{display_invocation}`")
if description:
lines.append(f"Description: {description}")
lines.append(f"\nPrompt: {prompt}")
lines.append(f"To execute: `{display_invocation}`")
lines.append(f"To execute: `{display_invocation} {event_name}`")
else:
lines.append(f"\n**Automatic Hook**: {extension}")
lines.append(f"Executing: `{display_invocation}`")
lines.append(f"Hook event: {event_name}")
lines.append(f"Executing: `{display_invocation} {event_name}`")
lines.append(f"EXECUTE_COMMAND: {command_text}")
lines.append(f"EXECUTE_COMMAND_INVOCATION: {display_invocation}")
lines.append(f"EXECUTE_COMMAND_INVOCATION: {display_invocation} {event_name}")
Comment thread
mnriem marked this conversation as resolved.
Comment on lines 2809 to +2813

return "\n".join(lines)

Expand Down
5 changes: 3 additions & 2 deletions tests/integrations/test_integration_claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,10 @@ def test_claude_hooks_render_skill_invocation(self, tmp_path):
],
)

assert "Executing: `/speckit-plan`" in message
assert "Hook event: before_plan" in message
assert "Executing: `/speckit-plan before_plan`" in message
assert "EXECUTE_COMMAND: speckit.plan" in message
assert "EXECUTE_COMMAND_INVOCATION: /speckit-plan" in message
assert "EXECUTE_COMMAND_INVOCATION: /speckit-plan before_plan" in message

def test_claude_preset_creates_new_skill_without_commands_dir(self, tmp_path):
from specify_cli import save_init_options
Expand Down
20 changes: 12 additions & 8 deletions tests/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4269,9 +4269,10 @@ def test_kimi_hooks_render_skill_invocation(self, project_dir):
],
)

assert "Executing: `/skill:speckit-plan`" in message
assert "Hook event: before_plan" in message
assert "Executing: `/skill:speckit-plan before_plan`" in message
assert "EXECUTE_COMMAND: speckit.plan" in message
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-plan" in message
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-plan before_plan" in message

def test_codex_hooks_render_dollar_skill_invocation(self, project_dir):
"""Codex projects with --ai-skills should render $speckit-* invocations."""
Expand Down Expand Up @@ -4309,9 +4310,10 @@ def test_non_skill_command_keeps_slash_invocation(self, project_dir):
],
)

assert "Executing: `/pre_tasks_test`" in message
assert "Hook event: before_tasks" in message
assert "Executing: `/pre_tasks_test before_tasks`" in message
assert "EXECUTE_COMMAND: pre_tasks_test" in message
assert "EXECUTE_COMMAND_INVOCATION: /pre_tasks_test" in message
assert "EXECUTE_COMMAND_INVOCATION: /pre_tasks_test before_tasks" in message

def test_extension_command_uses_hyphenated_skill_invocation(self, project_dir):
"""Multi-segment extension command ids should map to hyphenated skills."""
Expand All @@ -4331,9 +4333,10 @@ def test_extension_command_uses_hyphenated_skill_invocation(self, project_dir):
],
)

assert "Executing: `/skill:speckit-test-ext-hello`" in message
assert "Hook event: after_tasks" in message
assert "Executing: `/skill:speckit-test-ext-hello after_tasks`" in message
assert "EXECUTE_COMMAND: speckit.test-ext.hello" in message
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-test-ext-hello" in message
assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-test-ext-hello after_tasks" in message

def test_hook_executor_caches_init_options_lookup(self, project_dir, monkeypatch):
"""Init options should be loaded once per executor instance."""
Expand Down Expand Up @@ -4368,9 +4371,10 @@ def test_hook_message_falls_back_when_invocation_is_empty(self, project_dir):
],
)

assert "Executing: `/<missing command>`" in message
assert "Hook event: after_tasks" in message
assert "Executing: `/<missing command> after_tasks`" in message
assert "EXECUTE_COMMAND: <missing command>" in message
assert "EXECUTE_COMMAND_INVOCATION: /<missing command>" in message
assert "EXECUTE_COMMAND_INVOCATION: /<missing command> after_tasks" in message


class TestExtensionRemoveCLI:
Expand Down
Loading