chore: migrate repo to managed spec-kit (#266)
## Summary Selective migration to the managed Spec Kit project structure. ## Included - add managed Spec Kit integration metadata under `.specify/` - add bundled `speckit` workflow registry - add bundled `git` extension, scripts, and config - add new `speckit.git.*` command surfaces for Copilot, Gemini, and `.agents` - add the Spec Kit plan marker block to `.github/copilot-instructions.md` ## Intentionally excluded - no replacement of the existing customized core `speckit.*.agent.md` files - no `.vscode/settings.json` commit; the copied manifest was adjusted accordingly - no changes to the active `specs/231-finding-outcome-taxonomy` work ## Validation - `specify integration list` - `specify workflow list` - `specify extension list` - focused managed-file diff review Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #266
This commit is contained in:
parent
742d65f0d9
commit
76334cb096
53
.agents/skills/speckit-git-commit/SKILL.md
Normal file
53
.agents/skills/speckit-git-commit/SKILL.md
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
name: speckit-git-commit
|
||||
description: Auto-commit changes after a Spec Kit command completes
|
||||
compatibility: Requires spec-kit project structure with .specify/ directory
|
||||
metadata:
|
||||
author: github-spec-kit
|
||||
source: git:commands/speckit.git.commit.md
|
||||
---
|
||||
|
||||
# Auto-Commit Changes
|
||||
|
||||
Automatically stage and commit all changes after a Spec Kit command completes.
|
||||
|
||||
## Behavior
|
||||
|
||||
This command is invoked as a hook after (or before) core commands. It:
|
||||
|
||||
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`
|
||||
|
||||
## Execution
|
||||
|
||||
Determine the event name from the hook that triggered this command, then run the script:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>`
|
||||
|
||||
Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`).
|
||||
|
||||
## Configuration
|
||||
|
||||
In `.specify/extensions/git/git-config.yml`:
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
- 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
|
||||
72
.agents/skills/speckit-git-feature/SKILL.md
Normal file
72
.agents/skills/speckit-git-feature/SKILL.md
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
name: speckit-git-feature
|
||||
description: Create a feature branch with sequential or timestamp numbering
|
||||
compatibility: Requires spec-kit project structure with .specify/ directory
|
||||
metadata:
|
||||
author: github-spec-kit
|
||||
source: git:commands/speckit.git.feature.md
|
||||
---
|
||||
|
||||
# Create Feature Branch
|
||||
|
||||
Create and switch to a new git feature branch for the given specification. This command handles **branch creation only** — the spec directory and files are created by the core `/speckit.specify` workflow.
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Environment Variable Override
|
||||
|
||||
If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set:
|
||||
- The script uses the exact value as the branch name, bypassing all prefix/suffix generation
|
||||
- `--short-name`, `--number`, and `--timestamp` flags are ignored
|
||||
- `FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Verify Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, warn the user and skip branch creation
|
||||
|
||||
## Branch Numbering Mode
|
||||
|
||||
Determine the branch numbering strategy by checking configuration in this order:
|
||||
|
||||
1. Check `.specify/extensions/git/git-config.yml` for `branch_numbering` value
|
||||
2. Check `.specify/init-options.json` for `branch_numbering` value (backward compatibility)
|
||||
3. Default to `sequential` if neither exists
|
||||
|
||||
## Execution
|
||||
|
||||
Generate a concise short name (2-4 words) for the branch:
|
||||
- Analyze the feature description and extract the most meaningful keywords
|
||||
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
|
||||
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
|
||||
|
||||
Run the appropriate script based on your platform:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --short-name "<short-name>" "<feature description>"`
|
||||
- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "<short-name>" "<feature description>"`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "<short-name>" "<feature description>"`
|
||||
- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "<short-name>" "<feature description>"`
|
||||
|
||||
**IMPORTANT**:
|
||||
- Do NOT pass `--number` — the script determines the correct next number automatically
|
||||
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
|
||||
- You must only ever run this script once per feature
|
||||
- The JSON output will contain `BRANCH_NAME` and `FEATURE_NUM`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the current directory is not a Git repository:
|
||||
- Branch creation is skipped with a warning: `[specify] Warning: Git repository not detected; skipped branch creation`
|
||||
- The script still outputs `BRANCH_NAME` and `FEATURE_NUM` so the caller can reference them
|
||||
|
||||
## Output
|
||||
|
||||
The script outputs JSON with:
|
||||
- `BRANCH_NAME`: The branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`)
|
||||
- `FEATURE_NUM`: The numeric or timestamp prefix used
|
||||
54
.agents/skills/speckit-git-initialize/SKILL.md
Normal file
54
.agents/skills/speckit-git-initialize/SKILL.md
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
name: speckit-git-initialize
|
||||
description: Initialize a Git repository with an initial commit
|
||||
compatibility: Requires spec-kit project structure with .specify/ directory
|
||||
metadata:
|
||||
author: github-spec-kit
|
||||
source: git:commands/speckit.git.initialize.md
|
||||
---
|
||||
|
||||
# Initialize Git Repository
|
||||
|
||||
Initialize a Git repository in the current project directory if one does not already exist.
|
||||
|
||||
## Execution
|
||||
|
||||
Run the appropriate script from the project root:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/initialize-repo.sh`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/initialize-repo.ps1`
|
||||
|
||||
If the extension scripts are not found, fall back to:
|
||||
- **Bash**: `git init && git add . && git commit -m "Initial commit from Specify template"`
|
||||
- **PowerShell**: `git init; git add .; git commit -m "Initial commit from Specify template"`
|
||||
|
||||
The script handles all checks internally:
|
||||
- Skips if Git is not available
|
||||
- Skips if already inside a Git repository
|
||||
- Runs `git init`, `git add .`, and `git commit` with an initial commit message
|
||||
|
||||
## Customization
|
||||
|
||||
Replace the script to add project-specific Git initialization steps:
|
||||
- Custom `.gitignore` templates
|
||||
- Default branch naming (`git config init.defaultBranch`)
|
||||
- Git LFS setup
|
||||
- Git hooks installation
|
||||
- Commit signing configuration
|
||||
- Git Flow initialization
|
||||
|
||||
## Output
|
||||
|
||||
On success:
|
||||
- `✓ Git repository initialized`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed:
|
||||
- Warn the user
|
||||
- Skip repository initialization
|
||||
- The project continues to function without Git (specs can still be created under `specs/`)
|
||||
|
||||
If Git is installed but `git init`, `git add .`, or `git commit` fails:
|
||||
- Surface the error to the user
|
||||
- Stop this command rather than continuing with a partially initialized repository
|
||||
50
.agents/skills/speckit-git-remote/SKILL.md
Normal file
50
.agents/skills/speckit-git-remote/SKILL.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
name: speckit-git-remote
|
||||
description: Detect Git remote URL for GitHub integration
|
||||
compatibility: Requires spec-kit project structure with .specify/ directory
|
||||
metadata:
|
||||
author: github-spec-kit
|
||||
source: git:commands/speckit.git.remote.md
|
||||
---
|
||||
|
||||
# Detect Git Remote URL
|
||||
|
||||
Detect the Git remote URL for integration with GitHub services (e.g., issue creation).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and return empty:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; cannot determine remote URL
|
||||
```
|
||||
|
||||
## Execution
|
||||
|
||||
Run the following command to get the remote URL:
|
||||
|
||||
```bash
|
||||
git config --get remote.origin.url
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Parse the remote URL and determine:
|
||||
|
||||
1. **Repository owner**: Extract from the URL (e.g., `github` from `https://github.com/github/spec-kit.git`)
|
||||
2. **Repository name**: Extract from the URL (e.g., `spec-kit` from `https://github.com/github/spec-kit.git`)
|
||||
3. **Is GitHub**: Whether the remote points to a GitHub repository
|
||||
|
||||
Supported URL formats:
|
||||
- HTTPS: `https://github.com/<owner>/<repo>.git`
|
||||
- SSH: `git@github.com:<owner>/<repo>.git`
|
||||
|
||||
> [!CAUTION]
|
||||
> ONLY report a GitHub repository if the remote URL actually points to github.com.
|
||||
> Do NOT assume the remote is GitHub if the URL format doesn't match.
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed, the directory is not a Git repository, or no remote is configured:
|
||||
- Return an empty result
|
||||
- Do NOT error — other workflows should continue without Git remote information
|
||||
54
.agents/skills/speckit-git-validate/SKILL.md
Normal file
54
.agents/skills/speckit-git-validate/SKILL.md
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
name: speckit-git-validate
|
||||
description: Validate current branch follows feature branch naming conventions
|
||||
compatibility: Requires spec-kit project structure with .specify/ directory
|
||||
metadata:
|
||||
author: github-spec-kit
|
||||
source: git:commands/speckit.git.validate.md
|
||||
---
|
||||
|
||||
# Validate Feature Branch
|
||||
|
||||
Validate that the current Git branch follows the expected feature branch naming conventions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and skip validation:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; skipped branch validation
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
Get the current branch name:
|
||||
|
||||
```bash
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
```
|
||||
|
||||
The branch name must match one of these patterns:
|
||||
|
||||
1. **Sequential**: `^[0-9]{3,}-` (e.g., `001-feature-name`, `042-fix-bug`, `1000-big-feature`)
|
||||
2. **Timestamp**: `^[0-9]{8}-[0-9]{6}-` (e.g., `20260319-143022-feature-name`)
|
||||
|
||||
## Execution
|
||||
|
||||
If on a feature branch (matches either pattern):
|
||||
- Output: `✓ On feature branch: <branch-name>`
|
||||
- Check if the corresponding spec directory exists under `specs/`:
|
||||
- For sequential branches, look for `specs/<prefix>-*` where prefix matches the numeric portion
|
||||
- For timestamp branches, look for `specs/<prefix>-*` where prefix matches the `YYYYMMDD-HHMMSS` portion
|
||||
- If spec directory exists: `✓ Spec directory found: <path>`
|
||||
- If spec directory missing: `⚠ No spec directory found for prefix <prefix>`
|
||||
|
||||
If NOT on a feature branch:
|
||||
- Output: `✗ Not on a feature branch. Current branch: <branch-name>`
|
||||
- Output: `Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the directory is not a Git repository:
|
||||
- Check the `SPECIFY_FEATURE` environment variable as a fallback
|
||||
- If set, validate that value against the naming patterns
|
||||
- If not set, skip validation with a warning
|
||||
50
.gemini/commands/speckit.git.commit.toml
Normal file
50
.gemini/commands/speckit.git.commit.toml
Normal file
@ -0,0 +1,50 @@
|
||||
description = "Auto-commit changes after a Spec Kit command completes"
|
||||
|
||||
# Source: git
|
||||
|
||||
prompt = """
|
||||
# Auto-Commit Changes
|
||||
|
||||
Automatically stage and commit all changes after a Spec Kit command completes.
|
||||
|
||||
## Behavior
|
||||
|
||||
This command is invoked as a hook after (or before) core commands. It:
|
||||
|
||||
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`
|
||||
|
||||
## Execution
|
||||
|
||||
Determine the event name from the hook that triggered this command, then run the script:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>`
|
||||
|
||||
Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`).
|
||||
|
||||
## Configuration
|
||||
|
||||
In `.specify/extensions/git/git-config.yml`:
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
- 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
|
||||
"""
|
||||
69
.gemini/commands/speckit.git.feature.toml
Normal file
69
.gemini/commands/speckit.git.feature.toml
Normal file
@ -0,0 +1,69 @@
|
||||
description = "Create a feature branch with sequential or timestamp numbering"
|
||||
|
||||
# Source: git
|
||||
|
||||
prompt = """
|
||||
# Create Feature Branch
|
||||
|
||||
Create and switch to a new git feature branch for the given specification. This command handles **branch creation only** — the spec directory and files are created by the core `/speckit.specify` workflow.
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
{{args}}
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Environment Variable Override
|
||||
|
||||
If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set:
|
||||
- The script uses the exact value as the branch name, bypassing all prefix/suffix generation
|
||||
- `--short-name`, `--number`, and `--timestamp` flags are ignored
|
||||
- `FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Verify Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, warn the user and skip branch creation
|
||||
|
||||
## Branch Numbering Mode
|
||||
|
||||
Determine the branch numbering strategy by checking configuration in this order:
|
||||
|
||||
1. Check `.specify/extensions/git/git-config.yml` for `branch_numbering` value
|
||||
2. Check `.specify/init-options.json` for `branch_numbering` value (backward compatibility)
|
||||
3. Default to `sequential` if neither exists
|
||||
|
||||
## Execution
|
||||
|
||||
Generate a concise short name (2-4 words) for the branch:
|
||||
- Analyze the feature description and extract the most meaningful keywords
|
||||
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
|
||||
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
|
||||
|
||||
Run the appropriate script based on your platform:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --short-name "<short-name>" "<feature description>"`
|
||||
- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "<short-name>" "<feature description>"`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "<short-name>" "<feature description>"`
|
||||
- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "<short-name>" "<feature description>"`
|
||||
|
||||
**IMPORTANT**:
|
||||
- Do NOT pass `--number` — the script determines the correct next number automatically
|
||||
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
|
||||
- You must only ever run this script once per feature
|
||||
- The JSON output will contain `BRANCH_NAME` and `FEATURE_NUM`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the current directory is not a Git repository:
|
||||
- Branch creation is skipped with a warning: `[specify] Warning: Git repository not detected; skipped branch creation`
|
||||
- The script still outputs `BRANCH_NAME` and `FEATURE_NUM` so the caller can reference them
|
||||
|
||||
## Output
|
||||
|
||||
The script outputs JSON with:
|
||||
- `BRANCH_NAME`: The branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`)
|
||||
- `FEATURE_NUM`: The numeric or timestamp prefix used
|
||||
"""
|
||||
51
.gemini/commands/speckit.git.initialize.toml
Normal file
51
.gemini/commands/speckit.git.initialize.toml
Normal file
@ -0,0 +1,51 @@
|
||||
description = "Initialize a Git repository with an initial commit"
|
||||
|
||||
# Source: git
|
||||
|
||||
prompt = """
|
||||
# Initialize Git Repository
|
||||
|
||||
Initialize a Git repository in the current project directory if one does not already exist.
|
||||
|
||||
## Execution
|
||||
|
||||
Run the appropriate script from the project root:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/initialize-repo.sh`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/initialize-repo.ps1`
|
||||
|
||||
If the extension scripts are not found, fall back to:
|
||||
- **Bash**: `git init && git add . && git commit -m "Initial commit from Specify template"`
|
||||
- **PowerShell**: `git init; git add .; git commit -m "Initial commit from Specify template"`
|
||||
|
||||
The script handles all checks internally:
|
||||
- Skips if Git is not available
|
||||
- Skips if already inside a Git repository
|
||||
- Runs `git init`, `git add .`, and `git commit` with an initial commit message
|
||||
|
||||
## Customization
|
||||
|
||||
Replace the script to add project-specific Git initialization steps:
|
||||
- Custom `.gitignore` templates
|
||||
- Default branch naming (`git config init.defaultBranch`)
|
||||
- Git LFS setup
|
||||
- Git hooks installation
|
||||
- Commit signing configuration
|
||||
- Git Flow initialization
|
||||
|
||||
## Output
|
||||
|
||||
On success:
|
||||
- `✓ Git repository initialized`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed:
|
||||
- Warn the user
|
||||
- Skip repository initialization
|
||||
- The project continues to function without Git (specs can still be created under `specs/`)
|
||||
|
||||
If Git is installed but `git init`, `git add .`, or `git commit` fails:
|
||||
- Surface the error to the user
|
||||
- Stop this command rather than continuing with a partially initialized repository
|
||||
"""
|
||||
47
.gemini/commands/speckit.git.remote.toml
Normal file
47
.gemini/commands/speckit.git.remote.toml
Normal file
@ -0,0 +1,47 @@
|
||||
description = "Detect Git remote URL for GitHub integration"
|
||||
|
||||
# Source: git
|
||||
|
||||
prompt = """
|
||||
# Detect Git Remote URL
|
||||
|
||||
Detect the Git remote URL for integration with GitHub services (e.g., issue creation).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and return empty:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; cannot determine remote URL
|
||||
```
|
||||
|
||||
## Execution
|
||||
|
||||
Run the following command to get the remote URL:
|
||||
|
||||
```bash
|
||||
git config --get remote.origin.url
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Parse the remote URL and determine:
|
||||
|
||||
1. **Repository owner**: Extract from the URL (e.g., `github` from `https://github.com/github/spec-kit.git`)
|
||||
2. **Repository name**: Extract from the URL (e.g., `spec-kit` from `https://github.com/github/spec-kit.git`)
|
||||
3. **Is GitHub**: Whether the remote points to a GitHub repository
|
||||
|
||||
Supported URL formats:
|
||||
- HTTPS: `https://github.com/<owner>/<repo>.git`
|
||||
- SSH: `git@github.com:<owner>/<repo>.git`
|
||||
|
||||
> [!CAUTION]
|
||||
> ONLY report a GitHub repository if the remote URL actually points to github.com.
|
||||
> Do NOT assume the remote is GitHub if the URL format doesn't match.
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed, the directory is not a Git repository, or no remote is configured:
|
||||
- Return an empty result
|
||||
- Do NOT error — other workflows should continue without Git remote information
|
||||
"""
|
||||
51
.gemini/commands/speckit.git.validate.toml
Normal file
51
.gemini/commands/speckit.git.validate.toml
Normal file
@ -0,0 +1,51 @@
|
||||
description = "Validate current branch follows feature branch naming conventions"
|
||||
|
||||
# Source: git
|
||||
|
||||
prompt = """
|
||||
# Validate Feature Branch
|
||||
|
||||
Validate that the current Git branch follows the expected feature branch naming conventions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and skip validation:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; skipped branch validation
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
Get the current branch name:
|
||||
|
||||
```bash
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
```
|
||||
|
||||
The branch name must match one of these patterns:
|
||||
|
||||
1. **Sequential**: `^[0-9]{3,}-` (e.g., `001-feature-name`, `042-fix-bug`, `1000-big-feature`)
|
||||
2. **Timestamp**: `^[0-9]{8}-[0-9]{6}-` (e.g., `20260319-143022-feature-name`)
|
||||
|
||||
## Execution
|
||||
|
||||
If on a feature branch (matches either pattern):
|
||||
- Output: `✓ On feature branch: <branch-name>`
|
||||
- Check if the corresponding spec directory exists under `specs/`:
|
||||
- For sequential branches, look for `specs/<prefix>-*` where prefix matches the numeric portion
|
||||
- For timestamp branches, look for `specs/<prefix>-*` where prefix matches the `YYYYMMDD-HHMMSS` portion
|
||||
- If spec directory exists: `✓ Spec directory found: <path>`
|
||||
- If spec directory missing: `⚠ No spec directory found for prefix <prefix>`
|
||||
|
||||
If NOT on a feature branch:
|
||||
- Output: `✗ Not on a feature branch. Current branch: <branch-name>`
|
||||
- Output: `Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the directory is not a Git repository:
|
||||
- Check the `SPECIFY_FEATURE` environment variable as a fallback
|
||||
- If set, validate that value against the naming patterns
|
||||
- If not set, skip validation with a warning
|
||||
"""
|
||||
51
.github/agents/speckit.git.commit.agent.md
vendored
Normal file
51
.github/agents/speckit.git.commit.agent.md
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
description: Auto-commit changes after a Spec Kit command completes
|
||||
---
|
||||
|
||||
|
||||
<!-- Extension: git -->
|
||||
<!-- Config: .specify/extensions/git/ -->
|
||||
# Auto-Commit Changes
|
||||
|
||||
Automatically stage and commit all changes after a Spec Kit command completes.
|
||||
|
||||
## Behavior
|
||||
|
||||
This command is invoked as a hook after (or before) core commands. It:
|
||||
|
||||
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`
|
||||
|
||||
## Execution
|
||||
|
||||
Determine the event name from the hook that triggered this command, then run the script:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>`
|
||||
|
||||
Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`).
|
||||
|
||||
## Configuration
|
||||
|
||||
In `.specify/extensions/git/git-config.yml`:
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
- 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
|
||||
70
.github/agents/speckit.git.feature.agent.md
vendored
Normal file
70
.github/agents/speckit.git.feature.agent.md
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
description: Create a feature branch with sequential or timestamp numbering
|
||||
---
|
||||
|
||||
|
||||
<!-- Extension: git -->
|
||||
<!-- Config: .specify/extensions/git/ -->
|
||||
# Create Feature Branch
|
||||
|
||||
Create and switch to a new git feature branch for the given specification. This command handles **branch creation only** — the spec directory and files are created by the core `/speckit.specify` workflow.
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Environment Variable Override
|
||||
|
||||
If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set:
|
||||
- The script uses the exact value as the branch name, bypassing all prefix/suffix generation
|
||||
- `--short-name`, `--number`, and `--timestamp` flags are ignored
|
||||
- `FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Verify Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, warn the user and skip branch creation
|
||||
|
||||
## Branch Numbering Mode
|
||||
|
||||
Determine the branch numbering strategy by checking configuration in this order:
|
||||
|
||||
1. Check `.specify/extensions/git/git-config.yml` for `branch_numbering` value
|
||||
2. Check `.specify/init-options.json` for `branch_numbering` value (backward compatibility)
|
||||
3. Default to `sequential` if neither exists
|
||||
|
||||
## Execution
|
||||
|
||||
Generate a concise short name (2-4 words) for the branch:
|
||||
- Analyze the feature description and extract the most meaningful keywords
|
||||
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
|
||||
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
|
||||
|
||||
Run the appropriate script based on your platform:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --short-name "<short-name>" "<feature description>"`
|
||||
- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "<short-name>" "<feature description>"`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "<short-name>" "<feature description>"`
|
||||
- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "<short-name>" "<feature description>"`
|
||||
|
||||
**IMPORTANT**:
|
||||
- Do NOT pass `--number` — the script determines the correct next number automatically
|
||||
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
|
||||
- You must only ever run this script once per feature
|
||||
- The JSON output will contain `BRANCH_NAME` and `FEATURE_NUM`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the current directory is not a Git repository:
|
||||
- Branch creation is skipped with a warning: `[specify] Warning: Git repository not detected; skipped branch creation`
|
||||
- The script still outputs `BRANCH_NAME` and `FEATURE_NUM` so the caller can reference them
|
||||
|
||||
## Output
|
||||
|
||||
The script outputs JSON with:
|
||||
- `BRANCH_NAME`: The branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`)
|
||||
- `FEATURE_NUM`: The numeric or timestamp prefix used
|
||||
52
.github/agents/speckit.git.initialize.agent.md
vendored
Normal file
52
.github/agents/speckit.git.initialize.agent.md
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
description: Initialize a Git repository with an initial commit
|
||||
---
|
||||
|
||||
|
||||
<!-- Extension: git -->
|
||||
<!-- Config: .specify/extensions/git/ -->
|
||||
# Initialize Git Repository
|
||||
|
||||
Initialize a Git repository in the current project directory if one does not already exist.
|
||||
|
||||
## Execution
|
||||
|
||||
Run the appropriate script from the project root:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/initialize-repo.sh`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/initialize-repo.ps1`
|
||||
|
||||
If the extension scripts are not found, fall back to:
|
||||
- **Bash**: `git init && git add . && git commit -m "Initial commit from Specify template"`
|
||||
- **PowerShell**: `git init; git add .; git commit -m "Initial commit from Specify template"`
|
||||
|
||||
The script handles all checks internally:
|
||||
- Skips if Git is not available
|
||||
- Skips if already inside a Git repository
|
||||
- Runs `git init`, `git add .`, and `git commit` with an initial commit message
|
||||
|
||||
## Customization
|
||||
|
||||
Replace the script to add project-specific Git initialization steps:
|
||||
- Custom `.gitignore` templates
|
||||
- Default branch naming (`git config init.defaultBranch`)
|
||||
- Git LFS setup
|
||||
- Git hooks installation
|
||||
- Commit signing configuration
|
||||
- Git Flow initialization
|
||||
|
||||
## Output
|
||||
|
||||
On success:
|
||||
- `✓ Git repository initialized`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed:
|
||||
- Warn the user
|
||||
- Skip repository initialization
|
||||
- The project continues to function without Git (specs can still be created under `specs/`)
|
||||
|
||||
If Git is installed but `git init`, `git add .`, or `git commit` fails:
|
||||
- Surface the error to the user
|
||||
- Stop this command rather than continuing with a partially initialized repository
|
||||
48
.github/agents/speckit.git.remote.agent.md
vendored
Normal file
48
.github/agents/speckit.git.remote.agent.md
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
description: Detect Git remote URL for GitHub integration
|
||||
---
|
||||
|
||||
|
||||
<!-- Extension: git -->
|
||||
<!-- Config: .specify/extensions/git/ -->
|
||||
# Detect Git Remote URL
|
||||
|
||||
Detect the Git remote URL for integration with GitHub services (e.g., issue creation).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and return empty:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; cannot determine remote URL
|
||||
```
|
||||
|
||||
## Execution
|
||||
|
||||
Run the following command to get the remote URL:
|
||||
|
||||
```bash
|
||||
git config --get remote.origin.url
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Parse the remote URL and determine:
|
||||
|
||||
1. **Repository owner**: Extract from the URL (e.g., `github` from `https://github.com/github/spec-kit.git`)
|
||||
2. **Repository name**: Extract from the URL (e.g., `spec-kit` from `https://github.com/github/spec-kit.git`)
|
||||
3. **Is GitHub**: Whether the remote points to a GitHub repository
|
||||
|
||||
Supported URL formats:
|
||||
- HTTPS: `https://github.com/<owner>/<repo>.git`
|
||||
- SSH: `git@github.com:<owner>/<repo>.git`
|
||||
|
||||
> [!CAUTION]
|
||||
> ONLY report a GitHub repository if the remote URL actually points to github.com.
|
||||
> Do NOT assume the remote is GitHub if the URL format doesn't match.
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed, the directory is not a Git repository, or no remote is configured:
|
||||
- Return an empty result
|
||||
- Do NOT error — other workflows should continue without Git remote information
|
||||
52
.github/agents/speckit.git.validate.agent.md
vendored
Normal file
52
.github/agents/speckit.git.validate.agent.md
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
description: Validate current branch follows feature branch naming conventions
|
||||
---
|
||||
|
||||
|
||||
<!-- Extension: git -->
|
||||
<!-- Config: .specify/extensions/git/ -->
|
||||
# Validate Feature Branch
|
||||
|
||||
Validate that the current Git branch follows the expected feature branch naming conventions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and skip validation:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; skipped branch validation
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
Get the current branch name:
|
||||
|
||||
```bash
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
```
|
||||
|
||||
The branch name must match one of these patterns:
|
||||
|
||||
1. **Sequential**: `^[0-9]{3,}-` (e.g., `001-feature-name`, `042-fix-bug`, `1000-big-feature`)
|
||||
2. **Timestamp**: `^[0-9]{8}-[0-9]{6}-` (e.g., `20260319-143022-feature-name`)
|
||||
|
||||
## Execution
|
||||
|
||||
If on a feature branch (matches either pattern):
|
||||
- Output: `✓ On feature branch: <branch-name>`
|
||||
- Check if the corresponding spec directory exists under `specs/`:
|
||||
- For sequential branches, look for `specs/<prefix>-*` where prefix matches the numeric portion
|
||||
- For timestamp branches, look for `specs/<prefix>-*` where prefix matches the `YYYYMMDD-HHMMSS` portion
|
||||
- If spec directory exists: `✓ Spec directory found: <path>`
|
||||
- If spec directory missing: `⚠ No spec directory found for prefix <prefix>`
|
||||
|
||||
If NOT on a feature branch:
|
||||
- Output: `✗ Not on a feature branch. Current branch: <branch-name>`
|
||||
- Output: `Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the directory is not a Git repository:
|
||||
- Check the `SPECIFY_FEATURE` environment variable as a fallback
|
||||
- If set, validate that value against the naming patterns
|
||||
- If not set, skip validation with a warning
|
||||
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@ -673,3 +673,8 @@ ### Replaced Utilities
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
</laravel-boost-guidelines>
|
||||
|
||||
<!-- SPECKIT START -->
|
||||
For additional context about technologies to be used, project structure,
|
||||
shell commands, and other important information, read the current plan
|
||||
<!-- SPECKIT END -->
|
||||
|
||||
3
.github/prompts/speckit.git.commit.prompt.md
vendored
Normal file
3
.github/prompts/speckit.git.commit.prompt.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.git.commit
|
||||
---
|
||||
3
.github/prompts/speckit.git.feature.prompt.md
vendored
Normal file
3
.github/prompts/speckit.git.feature.prompt.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.git.feature
|
||||
---
|
||||
3
.github/prompts/speckit.git.initialize.prompt.md
vendored
Normal file
3
.github/prompts/speckit.git.initialize.prompt.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.git.initialize
|
||||
---
|
||||
3
.github/prompts/speckit.git.remote.prompt.md
vendored
Normal file
3
.github/prompts/speckit.git.remote.prompt.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.git.remote
|
||||
---
|
||||
3
.github/prompts/speckit.git.validate.prompt.md
vendored
Normal file
3
.github/prompts/speckit.git.validate.prompt.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
agent: speckit.git.validate
|
||||
---
|
||||
148
.specify/extensions.yml
Normal file
148
.specify/extensions.yml
Normal file
@ -0,0 +1,148 @@
|
||||
installed: []
|
||||
settings:
|
||||
auto_execute_hooks: true
|
||||
hooks:
|
||||
before_constitution:
|
||||
- extension: git
|
||||
command: speckit.git.initialize
|
||||
enabled: true
|
||||
optional: false
|
||||
prompt: Execute speckit.git.initialize?
|
||||
description: Initialize Git repository before constitution setup
|
||||
condition: null
|
||||
before_specify:
|
||||
- extension: git
|
||||
command: speckit.git.feature
|
||||
enabled: true
|
||||
optional: false
|
||||
prompt: Execute speckit.git.feature?
|
||||
description: Create feature branch before specification
|
||||
condition: null
|
||||
before_clarify:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit outstanding changes before clarification?
|
||||
description: Auto-commit before spec clarification
|
||||
condition: null
|
||||
before_plan:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit outstanding changes before planning?
|
||||
description: Auto-commit before implementation planning
|
||||
condition: null
|
||||
before_tasks:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit outstanding changes before task generation?
|
||||
description: Auto-commit before task generation
|
||||
condition: null
|
||||
before_implement:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit outstanding changes before implementation?
|
||||
description: Auto-commit before implementation
|
||||
condition: null
|
||||
before_checklist:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit outstanding changes before checklist?
|
||||
description: Auto-commit before checklist generation
|
||||
condition: null
|
||||
before_analyze:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit outstanding changes before analysis?
|
||||
description: Auto-commit before analysis
|
||||
condition: null
|
||||
before_taskstoissues:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit outstanding changes before issue sync?
|
||||
description: Auto-commit before tasks-to-issues conversion
|
||||
condition: null
|
||||
after_constitution:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit constitution changes?
|
||||
description: Auto-commit after constitution update
|
||||
condition: null
|
||||
after_specify:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit specification changes?
|
||||
description: Auto-commit after specification
|
||||
condition: null
|
||||
after_clarify:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit clarification changes?
|
||||
description: Auto-commit after spec clarification
|
||||
condition: null
|
||||
after_plan:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit plan changes?
|
||||
description: Auto-commit after implementation planning
|
||||
condition: null
|
||||
after_tasks:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit task changes?
|
||||
description: Auto-commit after task generation
|
||||
condition: null
|
||||
after_implement:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit implementation changes?
|
||||
description: Auto-commit after implementation
|
||||
condition: null
|
||||
after_checklist:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit checklist changes?
|
||||
description: Auto-commit after checklist generation
|
||||
condition: null
|
||||
after_analyze:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit analysis results?
|
||||
description: Auto-commit after analysis
|
||||
condition: null
|
||||
after_taskstoissues:
|
||||
- extension: git
|
||||
command: speckit.git.commit
|
||||
enabled: true
|
||||
optional: true
|
||||
prompt: Commit after syncing issues?
|
||||
description: Auto-commit after tasks-to-issues conversion
|
||||
condition: null
|
||||
44
.specify/extensions/.registry
Normal file
44
.specify/extensions/.registry
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"extensions": {
|
||||
"git": {
|
||||
"version": "1.0.0",
|
||||
"source": "local",
|
||||
"manifest_hash": "sha256:9731aa8143a72fbebfdb440f155038ab42642517c2b2bdbbf67c8fdbe076ed79",
|
||||
"enabled": true,
|
||||
"priority": 10,
|
||||
"registered_commands": {
|
||||
"agy": [
|
||||
"speckit.git.feature",
|
||||
"speckit.git.validate",
|
||||
"speckit.git.remote",
|
||||
"speckit.git.initialize",
|
||||
"speckit.git.commit"
|
||||
],
|
||||
"codex": [
|
||||
"speckit.git.feature",
|
||||
"speckit.git.validate",
|
||||
"speckit.git.remote",
|
||||
"speckit.git.initialize",
|
||||
"speckit.git.commit"
|
||||
],
|
||||
"copilot": [
|
||||
"speckit.git.feature",
|
||||
"speckit.git.validate",
|
||||
"speckit.git.remote",
|
||||
"speckit.git.initialize",
|
||||
"speckit.git.commit"
|
||||
],
|
||||
"gemini": [
|
||||
"speckit.git.feature",
|
||||
"speckit.git.validate",
|
||||
"speckit.git.remote",
|
||||
"speckit.git.initialize",
|
||||
"speckit.git.commit"
|
||||
]
|
||||
},
|
||||
"registered_skills": [],
|
||||
"installed_at": "2026-04-22T21:58:03.029565+00:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
100
.specify/extensions/git/README.md
Normal file
100
.specify/extensions/git/README.md
Normal file
@ -0,0 +1,100 @@
|
||||
# Git Branching Workflow Extension
|
||||
|
||||
Git repository initialization, feature branch creation, numbering (sequential/timestamp), validation, remote detection, and auto-commit for Spec Kit.
|
||||
|
||||
## Overview
|
||||
|
||||
This extension provides Git operations as an optional, self-contained module. It manages:
|
||||
|
||||
- **Repository initialization** with configurable commit messages
|
||||
- **Feature branch creation** with sequential (`001-feature-name`) or timestamp (`20260319-143022-feature-name`) numbering
|
||||
- **Branch validation** to ensure branches follow naming conventions
|
||||
- **Git remote detection** for GitHub integration (e.g., issue creation)
|
||||
- **Auto-commit** after core commands (configurable per-command with custom messages)
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `speckit.git.initialize` | Initialize a Git repository with a configurable commit message |
|
||||
| `speckit.git.feature` | Create a feature branch with sequential or timestamp numbering |
|
||||
| `speckit.git.validate` | Validate current branch follows feature branch naming conventions |
|
||||
| `speckit.git.remote` | Detect Git remote URL for GitHub integration |
|
||||
| `speckit.git.commit` | Auto-commit changes (configurable per-command enable/disable and messages) |
|
||||
|
||||
## Hooks
|
||||
|
||||
| Event | Command | Optional | Description |
|
||||
|-------|---------|----------|-------------|
|
||||
| `before_constitution` | `speckit.git.initialize` | No | Init git repo before constitution |
|
||||
| `before_specify` | `speckit.git.feature` | No | Create feature branch before specification |
|
||||
| `before_clarify` | `speckit.git.commit` | Yes | Commit outstanding changes before clarification |
|
||||
| `before_plan` | `speckit.git.commit` | Yes | Commit outstanding changes before planning |
|
||||
| `before_tasks` | `speckit.git.commit` | Yes | Commit outstanding changes before task generation |
|
||||
| `before_implement` | `speckit.git.commit` | Yes | Commit outstanding changes before implementation |
|
||||
| `before_checklist` | `speckit.git.commit` | Yes | Commit outstanding changes before checklist |
|
||||
| `before_analyze` | `speckit.git.commit` | Yes | Commit outstanding changes before analysis |
|
||||
| `before_taskstoissues` | `speckit.git.commit` | Yes | Commit outstanding changes before issue sync |
|
||||
| `after_constitution` | `speckit.git.commit` | Yes | Auto-commit after constitution update |
|
||||
| `after_specify` | `speckit.git.commit` | Yes | Auto-commit after specification |
|
||||
| `after_clarify` | `speckit.git.commit` | Yes | Auto-commit after clarification |
|
||||
| `after_plan` | `speckit.git.commit` | Yes | Auto-commit after planning |
|
||||
| `after_tasks` | `speckit.git.commit` | Yes | Auto-commit after task generation |
|
||||
| `after_implement` | `speckit.git.commit` | Yes | Auto-commit after implementation |
|
||||
| `after_checklist` | `speckit.git.commit` | Yes | Auto-commit after checklist |
|
||||
| `after_analyze` | `speckit.git.commit` | Yes | Auto-commit after analysis |
|
||||
| `after_taskstoissues` | `speckit.git.commit` | Yes | Auto-commit after issue sync |
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is stored in `.specify/extensions/git/git-config.yml`:
|
||||
|
||||
```yaml
|
||||
# Branch numbering strategy: "sequential" or "timestamp"
|
||||
branch_numbering: sequential
|
||||
|
||||
# Custom commit message for git init
|
||||
init_commit_message: "[Spec Kit] Initial commit"
|
||||
|
||||
# Auto-commit per command (all disabled by default)
|
||||
# Example: enable auto-commit after specify
|
||||
auto_commit:
|
||||
default: false
|
||||
after_specify:
|
||||
enabled: true
|
||||
message: "[Spec Kit] Add specification"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install the bundled git extension (no network required)
|
||||
specify extension add git
|
||||
```
|
||||
|
||||
## Disabling
|
||||
|
||||
```bash
|
||||
# Disable the git extension (spec creation continues without branching)
|
||||
specify extension disable git
|
||||
|
||||
# Re-enable it
|
||||
specify extension enable git
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
When Git is not installed or the directory is not a Git repository:
|
||||
- Spec directories are still created under `specs/`
|
||||
- Branch creation is skipped with a warning
|
||||
- Branch validation is skipped with a warning
|
||||
- Remote detection returns empty results
|
||||
|
||||
## Scripts
|
||||
|
||||
The extension bundles cross-platform scripts:
|
||||
|
||||
- `scripts/bash/create-new-feature.sh` — Bash implementation
|
||||
- `scripts/bash/git-common.sh` — Shared Git utilities (Bash)
|
||||
- `scripts/powershell/create-new-feature.ps1` — PowerShell implementation
|
||||
- `scripts/powershell/git-common.ps1` — Shared Git utilities (PowerShell)
|
||||
48
.specify/extensions/git/commands/speckit.git.commit.md
Normal file
48
.specify/extensions/git/commands/speckit.git.commit.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
description: "Auto-commit changes after a Spec Kit command completes"
|
||||
---
|
||||
|
||||
# Auto-Commit Changes
|
||||
|
||||
Automatically stage and commit all changes after a Spec Kit command completes.
|
||||
|
||||
## Behavior
|
||||
|
||||
This command is invoked as a hook after (or before) core commands. It:
|
||||
|
||||
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`
|
||||
|
||||
## Execution
|
||||
|
||||
Determine the event name from the hook that triggered this command, then run the script:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>`
|
||||
|
||||
Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`).
|
||||
|
||||
## Configuration
|
||||
|
||||
In `.specify/extensions/git/git-config.yml`:
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
- 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
|
||||
67
.specify/extensions/git/commands/speckit.git.feature.md
Normal file
67
.specify/extensions/git/commands/speckit.git.feature.md
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "Create a feature branch with sequential or timestamp numbering"
|
||||
---
|
||||
|
||||
# Create Feature Branch
|
||||
|
||||
Create and switch to a new git feature branch for the given specification. This command handles **branch creation only** — the spec directory and files are created by the core `/speckit.specify` workflow.
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Environment Variable Override
|
||||
|
||||
If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set:
|
||||
- The script uses the exact value as the branch name, bypassing all prefix/suffix generation
|
||||
- `--short-name`, `--number`, and `--timestamp` flags are ignored
|
||||
- `FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Verify Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, warn the user and skip branch creation
|
||||
|
||||
## Branch Numbering Mode
|
||||
|
||||
Determine the branch numbering strategy by checking configuration in this order:
|
||||
|
||||
1. Check `.specify/extensions/git/git-config.yml` for `branch_numbering` value
|
||||
2. Check `.specify/init-options.json` for `branch_numbering` value (backward compatibility)
|
||||
3. Default to `sequential` if neither exists
|
||||
|
||||
## Execution
|
||||
|
||||
Generate a concise short name (2-4 words) for the branch:
|
||||
- Analyze the feature description and extract the most meaningful keywords
|
||||
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
|
||||
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
|
||||
|
||||
Run the appropriate script based on your platform:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --short-name "<short-name>" "<feature description>"`
|
||||
- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "<short-name>" "<feature description>"`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "<short-name>" "<feature description>"`
|
||||
- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "<short-name>" "<feature description>"`
|
||||
|
||||
**IMPORTANT**:
|
||||
- Do NOT pass `--number` — the script determines the correct next number automatically
|
||||
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
|
||||
- You must only ever run this script once per feature
|
||||
- The JSON output will contain `BRANCH_NAME` and `FEATURE_NUM`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the current directory is not a Git repository:
|
||||
- Branch creation is skipped with a warning: `[specify] Warning: Git repository not detected; skipped branch creation`
|
||||
- The script still outputs `BRANCH_NAME` and `FEATURE_NUM` so the caller can reference them
|
||||
|
||||
## Output
|
||||
|
||||
The script outputs JSON with:
|
||||
- `BRANCH_NAME`: The branch name (e.g., `003-user-auth` or `20260319-143022-user-auth`)
|
||||
- `FEATURE_NUM`: The numeric or timestamp prefix used
|
||||
49
.specify/extensions/git/commands/speckit.git.initialize.md
Normal file
49
.specify/extensions/git/commands/speckit.git.initialize.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
description: "Initialize a Git repository with an initial commit"
|
||||
---
|
||||
|
||||
# Initialize Git Repository
|
||||
|
||||
Initialize a Git repository in the current project directory if one does not already exist.
|
||||
|
||||
## Execution
|
||||
|
||||
Run the appropriate script from the project root:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/initialize-repo.sh`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/initialize-repo.ps1`
|
||||
|
||||
If the extension scripts are not found, fall back to:
|
||||
- **Bash**: `git init && git add . && git commit -m "Initial commit from Specify template"`
|
||||
- **PowerShell**: `git init; git add .; git commit -m "Initial commit from Specify template"`
|
||||
|
||||
The script handles all checks internally:
|
||||
- Skips if Git is not available
|
||||
- Skips if already inside a Git repository
|
||||
- Runs `git init`, `git add .`, and `git commit` with an initial commit message
|
||||
|
||||
## Customization
|
||||
|
||||
Replace the script to add project-specific Git initialization steps:
|
||||
- Custom `.gitignore` templates
|
||||
- Default branch naming (`git config init.defaultBranch`)
|
||||
- Git LFS setup
|
||||
- Git hooks installation
|
||||
- Commit signing configuration
|
||||
- Git Flow initialization
|
||||
|
||||
## Output
|
||||
|
||||
On success:
|
||||
- `✓ Git repository initialized`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed:
|
||||
- Warn the user
|
||||
- Skip repository initialization
|
||||
- The project continues to function without Git (specs can still be created under `specs/`)
|
||||
|
||||
If Git is installed but `git init`, `git add .`, or `git commit` fails:
|
||||
- Surface the error to the user
|
||||
- Stop this command rather than continuing with a partially initialized repository
|
||||
45
.specify/extensions/git/commands/speckit.git.remote.md
Normal file
45
.specify/extensions/git/commands/speckit.git.remote.md
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
description: "Detect Git remote URL for GitHub integration"
|
||||
---
|
||||
|
||||
# Detect Git Remote URL
|
||||
|
||||
Detect the Git remote URL for integration with GitHub services (e.g., issue creation).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and return empty:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; cannot determine remote URL
|
||||
```
|
||||
|
||||
## Execution
|
||||
|
||||
Run the following command to get the remote URL:
|
||||
|
||||
```bash
|
||||
git config --get remote.origin.url
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Parse the remote URL and determine:
|
||||
|
||||
1. **Repository owner**: Extract from the URL (e.g., `github` from `https://github.com/github/spec-kit.git`)
|
||||
2. **Repository name**: Extract from the URL (e.g., `spec-kit` from `https://github.com/github/spec-kit.git`)
|
||||
3. **Is GitHub**: Whether the remote points to a GitHub repository
|
||||
|
||||
Supported URL formats:
|
||||
- HTTPS: `https://github.com/<owner>/<repo>.git`
|
||||
- SSH: `git@github.com:<owner>/<repo>.git`
|
||||
|
||||
> [!CAUTION]
|
||||
> ONLY report a GitHub repository if the remote URL actually points to github.com.
|
||||
> Do NOT assume the remote is GitHub if the URL format doesn't match.
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed, the directory is not a Git repository, or no remote is configured:
|
||||
- Return an empty result
|
||||
- Do NOT error — other workflows should continue without Git remote information
|
||||
49
.specify/extensions/git/commands/speckit.git.validate.md
Normal file
49
.specify/extensions/git/commands/speckit.git.validate.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
description: "Validate current branch follows feature branch naming conventions"
|
||||
---
|
||||
|
||||
# Validate Feature Branch
|
||||
|
||||
Validate that the current Git branch follows the expected feature branch naming conventions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null`
|
||||
- If Git is not available, output a warning and skip validation:
|
||||
```
|
||||
[specify] Warning: Git repository not detected; skipped branch validation
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
Get the current branch name:
|
||||
|
||||
```bash
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
```
|
||||
|
||||
The branch name must match one of these patterns:
|
||||
|
||||
1. **Sequential**: `^[0-9]{3,}-` (e.g., `001-feature-name`, `042-fix-bug`, `1000-big-feature`)
|
||||
2. **Timestamp**: `^[0-9]{8}-[0-9]{6}-` (e.g., `20260319-143022-feature-name`)
|
||||
|
||||
## Execution
|
||||
|
||||
If on a feature branch (matches either pattern):
|
||||
- Output: `✓ On feature branch: <branch-name>`
|
||||
- Check if the corresponding spec directory exists under `specs/`:
|
||||
- For sequential branches, look for `specs/<prefix>-*` where prefix matches the numeric portion
|
||||
- For timestamp branches, look for `specs/<prefix>-*` where prefix matches the `YYYYMMDD-HHMMSS` portion
|
||||
- If spec directory exists: `✓ Spec directory found: <path>`
|
||||
- If spec directory missing: `⚠ No spec directory found for prefix <prefix>`
|
||||
|
||||
If NOT on a feature branch:
|
||||
- Output: `✗ Not on a feature branch. Current branch: <branch-name>`
|
||||
- Output: `Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name`
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
If Git is not installed or the directory is not a Git repository:
|
||||
- Check the `SPECIFY_FEATURE` environment variable as a fallback
|
||||
- If set, validate that value against the naming patterns
|
||||
- If not set, skip validation with a warning
|
||||
62
.specify/extensions/git/config-template.yml
Normal file
62
.specify/extensions/git/config-template.yml
Normal file
@ -0,0 +1,62 @@
|
||||
# Git Branching Workflow Extension Configuration
|
||||
# Copied to .specify/extensions/git/git-config.yml on install
|
||||
|
||||
# Branch numbering strategy: "sequential" (001, 002, ...) or "timestamp" (YYYYMMDD-HHMMSS)
|
||||
branch_numbering: sequential
|
||||
|
||||
# Commit message used by `git commit` during repository initialization
|
||||
init_commit_message: "[Spec Kit] Initial commit"
|
||||
|
||||
# Auto-commit before/after core commands.
|
||||
# Set "default" to enable for all commands, then override per-command.
|
||||
# Each key can be true/false. Message is customizable per-command.
|
||||
auto_commit:
|
||||
default: false
|
||||
before_clarify:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before clarification"
|
||||
before_plan:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before planning"
|
||||
before_tasks:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before task generation"
|
||||
before_implement:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before implementation"
|
||||
before_checklist:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before checklist"
|
||||
before_analyze:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before analysis"
|
||||
before_taskstoissues:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before issue sync"
|
||||
after_constitution:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add project constitution"
|
||||
after_specify:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add specification"
|
||||
after_clarify:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Clarify specification"
|
||||
after_plan:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add implementation plan"
|
||||
after_tasks:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add tasks"
|
||||
after_implement:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Implementation progress"
|
||||
after_checklist:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add checklist"
|
||||
after_analyze:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add analysis report"
|
||||
after_taskstoissues:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Sync tasks to issues"
|
||||
140
.specify/extensions/git/extension.yml
Normal file
140
.specify/extensions/git/extension.yml
Normal file
@ -0,0 +1,140 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
extension:
|
||||
id: git
|
||||
name: "Git Branching Workflow"
|
||||
version: "1.0.0"
|
||||
description: "Feature branch creation, numbering (sequential/timestamp), validation, and Git remote detection"
|
||||
author: spec-kit-core
|
||||
repository: https://github.com/github/spec-kit
|
||||
license: MIT
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.2.0"
|
||||
tools:
|
||||
- name: git
|
||||
required: false
|
||||
|
||||
provides:
|
||||
commands:
|
||||
- name: speckit.git.feature
|
||||
file: commands/speckit.git.feature.md
|
||||
description: "Create a feature branch with sequential or timestamp numbering"
|
||||
- name: speckit.git.validate
|
||||
file: commands/speckit.git.validate.md
|
||||
description: "Validate current branch follows feature branch naming conventions"
|
||||
- name: speckit.git.remote
|
||||
file: commands/speckit.git.remote.md
|
||||
description: "Detect Git remote URL for GitHub integration"
|
||||
- name: speckit.git.initialize
|
||||
file: commands/speckit.git.initialize.md
|
||||
description: "Initialize a Git repository with an initial commit"
|
||||
- name: speckit.git.commit
|
||||
file: commands/speckit.git.commit.md
|
||||
description: "Auto-commit changes after a Spec Kit command completes"
|
||||
|
||||
config:
|
||||
- name: "git-config.yml"
|
||||
template: "config-template.yml"
|
||||
description: "Git branching configuration"
|
||||
required: false
|
||||
|
||||
hooks:
|
||||
before_constitution:
|
||||
command: speckit.git.initialize
|
||||
optional: false
|
||||
description: "Initialize Git repository before constitution setup"
|
||||
before_specify:
|
||||
command: speckit.git.feature
|
||||
optional: false
|
||||
description: "Create feature branch before specification"
|
||||
before_clarify:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit outstanding changes before clarification?"
|
||||
description: "Auto-commit before spec clarification"
|
||||
before_plan:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit outstanding changes before planning?"
|
||||
description: "Auto-commit before implementation planning"
|
||||
before_tasks:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit outstanding changes before task generation?"
|
||||
description: "Auto-commit before task generation"
|
||||
before_implement:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit outstanding changes before implementation?"
|
||||
description: "Auto-commit before implementation"
|
||||
before_checklist:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit outstanding changes before checklist?"
|
||||
description: "Auto-commit before checklist generation"
|
||||
before_analyze:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit outstanding changes before analysis?"
|
||||
description: "Auto-commit before analysis"
|
||||
before_taskstoissues:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit outstanding changes before issue sync?"
|
||||
description: "Auto-commit before tasks-to-issues conversion"
|
||||
after_constitution:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit constitution changes?"
|
||||
description: "Auto-commit after constitution update"
|
||||
after_specify:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit specification changes?"
|
||||
description: "Auto-commit after specification"
|
||||
after_clarify:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit clarification changes?"
|
||||
description: "Auto-commit after spec clarification"
|
||||
after_plan:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit plan changes?"
|
||||
description: "Auto-commit after implementation planning"
|
||||
after_tasks:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit task changes?"
|
||||
description: "Auto-commit after task generation"
|
||||
after_implement:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit implementation changes?"
|
||||
description: "Auto-commit after implementation"
|
||||
after_checklist:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit checklist changes?"
|
||||
description: "Auto-commit after checklist generation"
|
||||
after_analyze:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit analysis results?"
|
||||
description: "Auto-commit after analysis"
|
||||
after_taskstoissues:
|
||||
command: speckit.git.commit
|
||||
optional: true
|
||||
prompt: "Commit after syncing issues?"
|
||||
description: "Auto-commit after tasks-to-issues conversion"
|
||||
|
||||
tags:
|
||||
- "git"
|
||||
- "branching"
|
||||
- "workflow"
|
||||
|
||||
config:
|
||||
defaults:
|
||||
branch_numbering: sequential
|
||||
init_commit_message: "[Spec Kit] Initial commit"
|
||||
62
.specify/extensions/git/git-config.yml
Normal file
62
.specify/extensions/git/git-config.yml
Normal file
@ -0,0 +1,62 @@
|
||||
# Git Branching Workflow Extension Configuration
|
||||
# Copied to .specify/extensions/git/git-config.yml on install
|
||||
|
||||
# Branch numbering strategy: "sequential" (001, 002, ...) or "timestamp" (YYYYMMDD-HHMMSS)
|
||||
branch_numbering: sequential
|
||||
|
||||
# Commit message used by `git commit` during repository initialization
|
||||
init_commit_message: "[Spec Kit] Initial commit"
|
||||
|
||||
# Auto-commit before/after core commands.
|
||||
# Set "default" to enable for all commands, then override per-command.
|
||||
# Each key can be true/false. Message is customizable per-command.
|
||||
auto_commit:
|
||||
default: false
|
||||
before_clarify:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before clarification"
|
||||
before_plan:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before planning"
|
||||
before_tasks:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before task generation"
|
||||
before_implement:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before implementation"
|
||||
before_checklist:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before checklist"
|
||||
before_analyze:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before analysis"
|
||||
before_taskstoissues:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Save progress before issue sync"
|
||||
after_constitution:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add project constitution"
|
||||
after_specify:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add specification"
|
||||
after_clarify:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Clarify specification"
|
||||
after_plan:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add implementation plan"
|
||||
after_tasks:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add tasks"
|
||||
after_implement:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Implementation progress"
|
||||
after_checklist:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add checklist"
|
||||
after_analyze:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Add analysis report"
|
||||
after_taskstoissues:
|
||||
enabled: false
|
||||
message: "[Spec Kit] Sync tasks to issues"
|
||||
140
.specify/extensions/git/scripts/bash/auto-commit.sh
Executable file
140
.specify/extensions/git/scripts/bash/auto-commit.sh
Executable file
@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Git extension: auto-commit.sh
|
||||
# Automatically commit changes after a Spec Kit command completes.
|
||||
# Checks per-command config keys in git-config.yml before committing.
|
||||
#
|
||||
# Usage: auto-commit.sh <event_name>
|
||||
# e.g.: auto-commit.sh after_specify
|
||||
|
||||
set -e
|
||||
|
||||
EVENT_NAME="${1:-}"
|
||||
if [ -z "$EVENT_NAME" ]; then
|
||||
echo "Usage: $0 <event_name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
_find_project_root() {
|
||||
local dir="$1"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -d "$dir/.specify" ] || [ -d "$dir/.git" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
REPO_ROOT=$(_find_project_root "$SCRIPT_DIR") || REPO_ROOT="$(pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Check if git is available
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "[specify] Warning: Git not found; skipped auto-commit" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "[specify] Warning: Not a Git repository; skipped auto-commit" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Read per-command config from git-config.yml
|
||||
_config_file="$REPO_ROOT/.specify/extensions/git/git-config.yml"
|
||||
_enabled=false
|
||||
_commit_msg=""
|
||||
|
||||
if [ -f "$_config_file" ]; then
|
||||
# Parse the auto_commit section for this event.
|
||||
# Look for auto_commit.<event_name>.enabled and .message
|
||||
# Also check auto_commit.default as fallback.
|
||||
_in_auto_commit=false
|
||||
_in_event=false
|
||||
_default_enabled=false
|
||||
|
||||
while IFS= read -r _line; do
|
||||
# Detect auto_commit: section
|
||||
if echo "$_line" | grep -q '^auto_commit:'; then
|
||||
_in_auto_commit=true
|
||||
_in_event=false
|
||||
continue
|
||||
fi
|
||||
|
||||
# Exit auto_commit section on next top-level key
|
||||
if $_in_auto_commit && echo "$_line" | grep -Eq '^[a-z]'; then
|
||||
break
|
||||
fi
|
||||
|
||||
if $_in_auto_commit; then
|
||||
# Check default key
|
||||
if echo "$_line" | grep -Eq "^[[:space:]]+default:[[:space:]]"; then
|
||||
_val=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
||||
[ "$_val" = "true" ] && _default_enabled=true
|
||||
fi
|
||||
|
||||
# Detect our event subsection
|
||||
if echo "$_line" | grep -Eq "^[[:space:]]+${EVENT_NAME}:"; then
|
||||
_in_event=true
|
||||
continue
|
||||
fi
|
||||
|
||||
# Inside our event subsection
|
||||
if $_in_event; then
|
||||
# Exit on next sibling key (same indent level as event name)
|
||||
if echo "$_line" | grep -Eq '^[[:space:]]{2}[a-z]' && ! echo "$_line" | grep -Eq '^[[:space:]]{4}'; then
|
||||
_in_event=false
|
||||
continue
|
||||
fi
|
||||
if echo "$_line" | grep -Eq '[[:space:]]+enabled:'; then
|
||||
_val=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
||||
[ "$_val" = "true" ] && _enabled=true
|
||||
[ "$_val" = "false" ] && _enabled=false
|
||||
fi
|
||||
if echo "$_line" | grep -Eq '[[:space:]]+message:'; then
|
||||
_commit_msg=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | sed 's/^["'\'']//' | sed 's/["'\'']*$//')
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done < "$_config_file"
|
||||
|
||||
# If event-specific key not found, use default
|
||||
if [ "$_enabled" = "false" ] && [ "$_default_enabled" = "true" ]; then
|
||||
# Only use default if the event wasn't explicitly set to false
|
||||
# Check if event section existed at all
|
||||
if ! grep -q "^[[:space:]]*${EVENT_NAME}:" "$_config_file" 2>/dev/null; then
|
||||
_enabled=true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# No config file — auto-commit disabled by default
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$_enabled" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --quiet HEAD 2>/dev/null && git diff --cached --quiet 2>/dev/null && [ -z "$(git ls-files --others --exclude-standard 2>/dev/null)" ]; then
|
||||
echo "[specify] No changes to commit after $EVENT_NAME" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Derive a human-readable command name from the event
|
||||
# e.g., after_specify -> specify, before_plan -> plan
|
||||
_command_name=$(echo "$EVENT_NAME" | sed 's/^after_//' | sed 's/^before_//')
|
||||
_phase=$(echo "$EVENT_NAME" | grep -q '^before_' && echo 'before' || echo 'after')
|
||||
|
||||
# Use custom message if configured, otherwise default
|
||||
if [ -z "$_commit_msg" ]; then
|
||||
_commit_msg="[Spec Kit] Auto-commit ${_phase} ${_command_name}"
|
||||
fi
|
||||
|
||||
# Stage and commit
|
||||
_git_out=$(git add . 2>&1) || { echo "[specify] Error: git add failed: $_git_out" >&2; exit 1; }
|
||||
_git_out=$(git commit -q -m "$_commit_msg" 2>&1) || { echo "[specify] Error: git commit failed: $_git_out" >&2; exit 1; }
|
||||
|
||||
echo "[OK] Changes committed ${_phase} ${_command_name}" >&2
|
||||
453
.specify/extensions/git/scripts/bash/create-new-feature.sh
Executable file
453
.specify/extensions/git/scripts/bash/create-new-feature.sh
Executable file
@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env bash
|
||||
# Git extension: create-new-feature.sh
|
||||
# Adapted from core scripts/bash/create-new-feature.sh for extension layout.
|
||||
# Sources common.sh from the project's installed scripts, falling back to
|
||||
# git-common.sh for minimal git helpers.
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
DRY_RUN=false
|
||||
ALLOW_EXISTING=false
|
||||
SHORT_NAME=""
|
||||
BRANCH_NUMBER=""
|
||||
USE_TIMESTAMP=false
|
||||
ARGS=()
|
||||
i=1
|
||||
while [ $i -le $# ]; do
|
||||
arg="${!i}"
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
;;
|
||||
--allow-existing-branch)
|
||||
ALLOW_EXISTING=true
|
||||
;;
|
||||
--short-name)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
SHORT_NAME="$next_arg"
|
||||
;;
|
||||
--number)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
BRANCH_NUMBER="$next_arg"
|
||||
if [[ ! "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||
echo 'Error: --number must be a non-negative integer' >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--timestamp)
|
||||
USE_TIMESTAMP=true
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --json Output in JSON format"
|
||||
echo " --dry-run Compute branch name without creating the branch"
|
||||
echo " --allow-existing-branch Switch to branch if it already exists instead of failing"
|
||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Environment variables:"
|
||||
echo " GIT_BRANCH_NAME Use this exact branch name, bypassing all prefix/suffix generation"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
|
||||
echo " GIT_BRANCH_NAME=my-branch $0 'feature description'"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Trim whitespace and validate description is not empty
|
||||
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to get highest number from specs directory
|
||||
get_highest_from_specs() {
|
||||
local specs_dir="$1"
|
||||
local highest=0
|
||||
|
||||
if [ -d "$specs_dir" ]; then
|
||||
for dir in "$specs_dir"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
dirname=$(basename "$dir")
|
||||
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
|
||||
if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
||||
number=$(echo "$dirname" | grep -Eo '^[0-9]+')
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to get highest number from git branches
|
||||
get_highest_from_branches() {
|
||||
git branch -a 2>/dev/null | sed 's/^[* ]*//; s|^remotes/[^/]*/||' | _extract_highest_number
|
||||
}
|
||||
|
||||
# Extract the highest sequential feature number from a list of ref names (one per line).
|
||||
_extract_highest_number() {
|
||||
local highest=0
|
||||
while IFS= read -r name; do
|
||||
[ -z "$name" ] && continue
|
||||
if echo "$name" | grep -Eq '^[0-9]{3,}-' && ! echo "$name" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
||||
number=$(echo "$name" | grep -Eo '^[0-9]+' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to get highest number from remote branches without fetching (side-effect-free)
|
||||
get_highest_from_remote_refs() {
|
||||
local highest=0
|
||||
|
||||
for remote in $(git remote 2>/dev/null); do
|
||||
local remote_highest
|
||||
remote_highest=$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads "$remote" 2>/dev/null | sed 's|.*refs/heads/||' | _extract_highest_number)
|
||||
if [ "$remote_highest" -gt "$highest" ]; then
|
||||
highest=$remote_highest
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to check existing branches and return next available number.
|
||||
check_existing_branches() {
|
||||
local specs_dir="$1"
|
||||
local skip_fetch="${2:-false}"
|
||||
|
||||
if [ "$skip_fetch" = true ]; then
|
||||
local highest_remote=$(get_highest_from_remote_refs)
|
||||
local highest_branch=$(get_highest_from_branches)
|
||||
if [ "$highest_remote" -gt "$highest_branch" ]; then
|
||||
highest_branch=$highest_remote
|
||||
fi
|
||||
else
|
||||
git fetch --all --prune >/dev/null 2>&1 || true
|
||||
local highest_branch=$(get_highest_from_branches)
|
||||
fi
|
||||
|
||||
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
||||
|
||||
local max_num=$highest_branch
|
||||
if [ "$highest_spec" -gt "$max_num" ]; then
|
||||
max_num=$highest_spec
|
||||
fi
|
||||
|
||||
echo $((max_num + 1))
|
||||
}
|
||||
|
||||
# Function to clean and format a branch name
|
||||
clean_branch_name() {
|
||||
local name="$1"
|
||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Source common.sh for resolve_template, json_escape, get_repo_root, has_git.
|
||||
#
|
||||
# Search locations in priority order:
|
||||
# 1. .specify/scripts/bash/common.sh under the project root (installed project)
|
||||
# 2. scripts/bash/common.sh under the project root (source checkout fallback)
|
||||
# 3. git-common.sh next to this script (minimal fallback — lacks resolve_template)
|
||||
# ---------------------------------------------------------------------------
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Find project root by walking up from the script location
|
||||
_find_project_root() {
|
||||
local dir="$1"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -d "$dir/.specify" ] || [ -d "$dir/.git" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_common_loaded=false
|
||||
_PROJECT_ROOT=$(_find_project_root "$SCRIPT_DIR") || true
|
||||
|
||||
if [ -n "$_PROJECT_ROOT" ] && [ -f "$_PROJECT_ROOT/.specify/scripts/bash/common.sh" ]; then
|
||||
source "$_PROJECT_ROOT/.specify/scripts/bash/common.sh"
|
||||
_common_loaded=true
|
||||
elif [ -n "$_PROJECT_ROOT" ] && [ -f "$_PROJECT_ROOT/scripts/bash/common.sh" ]; then
|
||||
source "$_PROJECT_ROOT/scripts/bash/common.sh"
|
||||
_common_loaded=true
|
||||
elif [ -f "$SCRIPT_DIR/git-common.sh" ]; then
|
||||
source "$SCRIPT_DIR/git-common.sh"
|
||||
_common_loaded=true
|
||||
fi
|
||||
|
||||
if [ "$_common_loaded" != "true" ]; then
|
||||
echo "Error: Could not locate common.sh or git-common.sh. Please ensure the Specify core scripts are installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve repository root
|
||||
if type get_repo_root >/dev/null 2>&1; then
|
||||
REPO_ROOT=$(get_repo_root)
|
||||
elif git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
elif [ -n "$_PROJECT_ROOT" ]; then
|
||||
REPO_ROOT="$_PROJECT_ROOT"
|
||||
else
|
||||
echo "Error: Could not determine repository root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if git is available at this repo root
|
||||
if type has_git >/dev/null 2>&1; then
|
||||
if has_git "$REPO_ROOT"; then
|
||||
HAS_GIT=true
|
||||
else
|
||||
HAS_GIT=false
|
||||
fi
|
||||
elif git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
HAS_GIT=true
|
||||
else
|
||||
HAS_GIT=false
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
SPECS_DIR="$REPO_ROOT/specs"
|
||||
|
||||
# Function to generate branch name with stop word filtering
|
||||
generate_branch_name() {
|
||||
local description="$1"
|
||||
|
||||
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
|
||||
|
||||
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
|
||||
local meaningful_words=()
|
||||
for word in $clean_name; do
|
||||
[ -z "$word" ] && continue
|
||||
if ! echo "$word" | grep -qiE "$stop_words"; then
|
||||
if [ ${#word} -ge 3 ]; then
|
||||
meaningful_words+=("$word")
|
||||
elif echo "$description" | grep -qw -- "${word^^}"; then
|
||||
meaningful_words+=("$word")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
||||
local max_words=3
|
||||
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
||||
|
||||
local result=""
|
||||
local count=0
|
||||
for word in "${meaningful_words[@]}"; do
|
||||
if [ $count -ge $max_words ]; then break; fi
|
||||
if [ -n "$result" ]; then result="$result-"; fi
|
||||
result="$result$word"
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "$result"
|
||||
else
|
||||
local cleaned=$(clean_branch_name "$description")
|
||||
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for GIT_BRANCH_NAME env var override (exact branch name, no prefix/suffix)
|
||||
if [ -n "${GIT_BRANCH_NAME:-}" ]; then
|
||||
BRANCH_NAME="$GIT_BRANCH_NAME"
|
||||
# Extract FEATURE_NUM from the branch name if it starts with a numeric prefix
|
||||
# Check timestamp pattern first (YYYYMMDD-HHMMSS-) since it also matches the simpler ^[0-9]+ pattern
|
||||
if echo "$BRANCH_NAME" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
||||
FEATURE_NUM=$(echo "$BRANCH_NAME" | grep -Eo '^[0-9]{8}-[0-9]{6}')
|
||||
BRANCH_SUFFIX="${BRANCH_NAME#${FEATURE_NUM}-}"
|
||||
elif echo "$BRANCH_NAME" | grep -Eq '^[0-9]+-'; then
|
||||
FEATURE_NUM=$(echo "$BRANCH_NAME" | grep -Eo '^[0-9]+')
|
||||
BRANCH_SUFFIX="${BRANCH_NAME#${FEATURE_NUM}-}"
|
||||
else
|
||||
FEATURE_NUM="$BRANCH_NAME"
|
||||
BRANCH_SUFFIX="$BRANCH_NAME"
|
||||
fi
|
||||
else
|
||||
# Generate branch name
|
||||
if [ -n "$SHORT_NAME" ]; then
|
||||
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
||||
else
|
||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||
fi
|
||||
|
||||
# Warn if --number and --timestamp are both specified
|
||||
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
|
||||
>&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
|
||||
BRANCH_NUMBER=""
|
||||
fi
|
||||
|
||||
# Determine branch prefix
|
||||
if [ "$USE_TIMESTAMP" = true ]; then
|
||||
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
|
||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
else
|
||||
if [ -z "$BRANCH_NUMBER" ]; then
|
||||
if [ "$DRY_RUN" = true ] && [ "$HAS_GIT" = true ]; then
|
||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR" true)
|
||||
elif [ "$DRY_RUN" = true ]; then
|
||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||
elif [ "$HAS_GIT" = true ]; then
|
||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
||||
else
|
||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names
|
||||
MAX_BRANCH_LENGTH=244
|
||||
_byte_length() { printf '%s' "$1" | LC_ALL=C wc -c | tr -d ' '; }
|
||||
BRANCH_BYTE_LEN=$(_byte_length "$BRANCH_NAME")
|
||||
if [ -n "${GIT_BRANCH_NAME:-}" ] && [ "$BRANCH_BYTE_LEN" -gt $MAX_BRANCH_LENGTH ]; then
|
||||
>&2 echo "Error: GIT_BRANCH_NAME must be 244 bytes or fewer in UTF-8. Provided value is ${BRANCH_BYTE_LEN} bytes."
|
||||
exit 1
|
||||
elif [ "$BRANCH_BYTE_LEN" -gt $MAX_BRANCH_LENGTH ]; then
|
||||
PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 ))
|
||||
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
|
||||
|
||||
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
||||
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
||||
|
||||
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
||||
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
||||
|
||||
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
||||
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" != true ]; then
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
branch_create_error=""
|
||||
if ! branch_create_error=$(git checkout -q -b "$BRANCH_NAME" 2>&1); then
|
||||
current_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
||||
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
||||
if [ "$ALLOW_EXISTING" = true ]; then
|
||||
if [ "$current_branch" = "$BRANCH_NAME" ]; then
|
||||
:
|
||||
elif ! switch_branch_error=$(git checkout -q "$BRANCH_NAME" 2>&1); then
|
||||
>&2 echo "Error: Failed to switch to existing branch '$BRANCH_NAME'. Please resolve any local changes or conflicts and try again."
|
||||
if [ -n "$switch_branch_error" ]; then
|
||||
>&2 printf '%s\n' "$switch_branch_error"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$USE_TIMESTAMP" = true ]; then
|
||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
|
||||
exit 1
|
||||
else
|
||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'."
|
||||
if [ -n "$branch_create_error" ]; then
|
||||
>&2 printf '%s\n' "$branch_create_error"
|
||||
else
|
||||
>&2 echo "Please check your git configuration and try again."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||
fi
|
||||
|
||||
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
|
||||
fi
|
||||
|
||||
if $JSON_MODE; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
jq -cn \
|
||||
--arg branch_name "$BRANCH_NAME" \
|
||||
--arg feature_num "$FEATURE_NUM" \
|
||||
'{BRANCH_NAME:$branch_name,FEATURE_NUM:$feature_num,DRY_RUN:true}'
|
||||
else
|
||||
jq -cn \
|
||||
--arg branch_name "$BRANCH_NAME" \
|
||||
--arg feature_num "$FEATURE_NUM" \
|
||||
'{BRANCH_NAME:$branch_name,FEATURE_NUM:$feature_num}'
|
||||
fi
|
||||
else
|
||||
if type json_escape >/dev/null 2>&1; then
|
||||
_je_branch=$(json_escape "$BRANCH_NAME")
|
||||
_je_num=$(json_escape "$FEATURE_NUM")
|
||||
else
|
||||
_je_branch="$BRANCH_NAME"
|
||||
_je_num="$FEATURE_NUM"
|
||||
fi
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
printf '{"BRANCH_NAME":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$_je_branch" "$_je_num"
|
||||
else
|
||||
printf '{"BRANCH_NAME":"%s","FEATURE_NUM":"%s"}\n' "$_je_branch" "$_je_num"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||
if [ "$DRY_RUN" != true ]; then
|
||||
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
|
||||
fi
|
||||
fi
|
||||
54
.specify/extensions/git/scripts/bash/git-common.sh
Executable file
54
.specify/extensions/git/scripts/bash/git-common.sh
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Git-specific common functions for the git extension.
|
||||
# Extracted from scripts/bash/common.sh — contains only git-specific
|
||||
# branch validation and detection logic.
|
||||
|
||||
# Check if we have git available at the repo root
|
||||
has_git() {
|
||||
local repo_root="${1:-$(pwd)}"
|
||||
{ [ -d "$repo_root/.git" ] || [ -f "$repo_root/.git" ]; } && \
|
||||
command -v git >/dev/null 2>&1 && \
|
||||
git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Strip a single optional path segment (e.g. gitflow "feat/004-name" -> "004-name").
|
||||
# Only when the full name is exactly two slash-free segments; otherwise returns the raw name.
|
||||
spec_kit_effective_branch_name() {
|
||||
local raw="$1"
|
||||
if [[ "$raw" =~ ^([^/]+)/([^/]+)$ ]]; then
|
||||
printf '%s\n' "${BASH_REMATCH[2]}"
|
||||
else
|
||||
printf '%s\n' "$raw"
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate that a branch name matches the expected feature branch pattern.
|
||||
# Accepts sequential (###-* with >=3 digits) or timestamp (YYYYMMDD-HHMMSS-*) formats.
|
||||
# Logic aligned with scripts/bash/common.sh check_feature_branch after effective-name normalization.
|
||||
check_feature_branch() {
|
||||
local raw="$1"
|
||||
local has_git_repo="$2"
|
||||
|
||||
# For non-git repos, we can't enforce branch naming but still provide output
|
||||
if [[ "$has_git_repo" != "true" ]]; then
|
||||
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
local branch
|
||||
branch=$(spec_kit_effective_branch_name "$raw")
|
||||
|
||||
# Accept sequential prefix (3+ digits) but exclude malformed timestamps
|
||||
# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
|
||||
local is_sequential=false
|
||||
if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then
|
||||
is_sequential=true
|
||||
fi
|
||||
if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then
|
||||
echo "ERROR: Not on a feature branch. Current branch: $raw" >&2
|
||||
echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
54
.specify/extensions/git/scripts/bash/initialize-repo.sh
Executable file
54
.specify/extensions/git/scripts/bash/initialize-repo.sh
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Git extension: initialize-repo.sh
|
||||
# Initialize a Git repository with an initial commit.
|
||||
# Customizable — replace this script to add .gitignore templates,
|
||||
# default branch config, git-flow, LFS, signing, etc.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Find project root
|
||||
_find_project_root() {
|
||||
local dir="$1"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -d "$dir/.specify" ] || [ -d "$dir/.git" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
REPO_ROOT=$(_find_project_root "$SCRIPT_DIR") || REPO_ROOT="$(pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Read commit message from extension config, fall back to default
|
||||
COMMIT_MSG="[Spec Kit] Initial commit"
|
||||
_config_file="$REPO_ROOT/.specify/extensions/git/git-config.yml"
|
||||
if [ -f "$_config_file" ]; then
|
||||
_msg=$(grep '^init_commit_message:' "$_config_file" 2>/dev/null | sed 's/^init_commit_message:[[:space:]]*//' | sed 's/^["'\'']//' | sed 's/["'\'']*$//')
|
||||
if [ -n "$_msg" ]; then
|
||||
COMMIT_MSG="$_msg"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if git is available
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "[specify] Warning: Git not found; skipped repository initialization" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if already a git repo
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "[specify] Git repository already initialized; skipping" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize
|
||||
_git_out=$(git init -q 2>&1) || { echo "[specify] Error: git init failed: $_git_out" >&2; exit 1; }
|
||||
_git_out=$(git add . 2>&1) || { echo "[specify] Error: git add failed: $_git_out" >&2; exit 1; }
|
||||
_git_out=$(git commit --allow-empty -q -m "$COMMIT_MSG" 2>&1) || { echo "[specify] Error: git commit failed: $_git_out" >&2; exit 1; }
|
||||
|
||||
echo "✓ Git repository initialized" >&2
|
||||
169
.specify/extensions/git/scripts/powershell/auto-commit.ps1
Normal file
169
.specify/extensions/git/scripts/powershell/auto-commit.ps1
Normal file
@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Git extension: auto-commit.ps1
|
||||
# Automatically commit changes after a Spec Kit command completes.
|
||||
# Checks per-command config keys in git-config.yml before committing.
|
||||
#
|
||||
# Usage: auto-commit.ps1 <event_name>
|
||||
# e.g.: auto-commit.ps1 after_specify
|
||||
param(
|
||||
[Parameter(Position = 0, Mandatory = $true)]
|
||||
[string]$EventName
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Find-ProjectRoot {
|
||||
param([string]$StartDir)
|
||||
$current = Resolve-Path $StartDir
|
||||
while ($true) {
|
||||
foreach ($marker in @('.specify', '.git')) {
|
||||
if (Test-Path (Join-Path $current $marker)) {
|
||||
return $current
|
||||
}
|
||||
}
|
||||
$parent = Split-Path $current -Parent
|
||||
if ($parent -eq $current) { return $null }
|
||||
$current = $parent
|
||||
}
|
||||
}
|
||||
|
||||
$repoRoot = Find-ProjectRoot -StartDir $PSScriptRoot
|
||||
if (-not $repoRoot) { $repoRoot = Get-Location }
|
||||
Set-Location $repoRoot
|
||||
|
||||
# Check if git is available
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
Write-Warning "[specify] Warning: Git not found; skipped auto-commit"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Temporarily relax ErrorActionPreference so git stderr warnings
|
||||
# (e.g. CRLF notices on Windows) do not become terminating errors.
|
||||
$savedEAP = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
git rev-parse --is-inside-work-tree 2>$null | Out-Null
|
||||
$isRepo = $LASTEXITCODE -eq 0
|
||||
} finally {
|
||||
$ErrorActionPreference = $savedEAP
|
||||
}
|
||||
if (-not $isRepo) {
|
||||
Write-Warning "[specify] Warning: Not a Git repository; skipped auto-commit"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read per-command config from git-config.yml
|
||||
$configFile = Join-Path $repoRoot ".specify/extensions/git/git-config.yml"
|
||||
$enabled = $false
|
||||
$commitMsg = ""
|
||||
|
||||
if (Test-Path $configFile) {
|
||||
# Parse YAML to find auto_commit section
|
||||
$inAutoCommit = $false
|
||||
$inEvent = $false
|
||||
$defaultEnabled = $false
|
||||
|
||||
foreach ($line in Get-Content $configFile) {
|
||||
# Detect auto_commit: section
|
||||
if ($line -match '^auto_commit:') {
|
||||
$inAutoCommit = $true
|
||||
$inEvent = $false
|
||||
continue
|
||||
}
|
||||
|
||||
# Exit auto_commit section on next top-level key
|
||||
if ($inAutoCommit -and $line -match '^[a-z]') {
|
||||
break
|
||||
}
|
||||
|
||||
if ($inAutoCommit) {
|
||||
# Check default key
|
||||
if ($line -match '^\s+default:\s*(.+)$') {
|
||||
$val = $matches[1].Trim().ToLower()
|
||||
if ($val -eq 'true') { $defaultEnabled = $true }
|
||||
}
|
||||
|
||||
# Detect our event subsection
|
||||
if ($line -match "^\s+${EventName}:") {
|
||||
$inEvent = $true
|
||||
continue
|
||||
}
|
||||
|
||||
# Inside our event subsection
|
||||
if ($inEvent) {
|
||||
# Exit on next sibling key (2-space indent, not 4+)
|
||||
if ($line -match '^\s{2}[a-z]' -and $line -notmatch '^\s{4}') {
|
||||
$inEvent = $false
|
||||
continue
|
||||
}
|
||||
if ($line -match '\s+enabled:\s*(.+)$') {
|
||||
$val = $matches[1].Trim().ToLower()
|
||||
if ($val -eq 'true') { $enabled = $true }
|
||||
if ($val -eq 'false') { $enabled = $false }
|
||||
}
|
||||
if ($line -match '\s+message:\s*(.+)$') {
|
||||
$commitMsg = $matches[1].Trim() -replace '^["'']' -replace '["'']$'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# If event-specific key not found, use default
|
||||
if (-not $enabled -and $defaultEnabled) {
|
||||
$hasEventKey = Select-String -Path $configFile -Pattern "^\s*${EventName}:" -Quiet
|
||||
if (-not $hasEventKey) {
|
||||
$enabled = $true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# No config file — auto-commit disabled by default
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $enabled) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if there are changes to commit
|
||||
# Relax ErrorActionPreference so CRLF warnings on stderr do not terminate.
|
||||
$savedEAP = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE
|
||||
git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE
|
||||
$untracked = git ls-files --others --exclude-standard 2>$null
|
||||
} finally {
|
||||
$ErrorActionPreference = $savedEAP
|
||||
}
|
||||
|
||||
if ($d1 -eq 0 -and $d2 -eq 0 -and -not $untracked) {
|
||||
Write-Host "[specify] No changes to commit after $EventName" -ForegroundColor DarkGray
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Derive a human-readable command name from the event
|
||||
$commandName = $EventName -replace '^after_', '' -replace '^before_', ''
|
||||
$phase = if ($EventName -match '^before_') { 'before' } else { 'after' }
|
||||
|
||||
# Use custom message if configured, otherwise default
|
||||
if (-not $commitMsg) {
|
||||
$commitMsg = "[Spec Kit] Auto-commit $phase $commandName"
|
||||
}
|
||||
|
||||
# Stage and commit
|
||||
# Relax ErrorActionPreference so CRLF warnings on stderr do not terminate,
|
||||
# while still allowing redirected error output to be captured for diagnostics.
|
||||
$savedEAP = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
$out = git add . 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
|
||||
$out = git commit -q -m $commitMsg 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw "git commit failed: $out" }
|
||||
} catch {
|
||||
Write-Warning "[specify] Error: $_"
|
||||
exit 1
|
||||
} finally {
|
||||
$ErrorActionPreference = $savedEAP
|
||||
}
|
||||
|
||||
Write-Host "[OK] Changes committed $phase $commandName"
|
||||
@ -0,0 +1,403 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Git extension: create-new-feature.ps1
|
||||
# Adapted from core scripts/powershell/create-new-feature.ps1 for extension layout.
|
||||
# Sources common.ps1 from the project's installed scripts, falling back to
|
||||
# git-common.ps1 for minimal git helpers.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Json,
|
||||
[switch]$AllowExistingBranch,
|
||||
[switch]$DryRun,
|
||||
[string]$ShortName,
|
||||
[Parameter()]
|
||||
[long]$Number = 0,
|
||||
[switch]$Timestamp,
|
||||
[switch]$Help,
|
||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
||||
[string[]]$FeatureDescription
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($Help) {
|
||||
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
||||
Write-Host ""
|
||||
Write-Host "Options:"
|
||||
Write-Host " -Json Output in JSON format"
|
||||
Write-Host " -DryRun Compute branch name without creating the branch"
|
||||
Write-Host " -AllowExistingBranch Switch to branch if it already exists instead of failing"
|
||||
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
||||
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
||||
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
||||
Write-Host " -Help Show this help message"
|
||||
Write-Host ""
|
||||
Write-Host "Environment variables:"
|
||||
Write-Host " GIT_BRANCH_NAME Use this exact branch name, bypassing all prefix/suffix generation"
|
||||
Write-Host ""
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
||||
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$featureDesc = ($FeatureDescription -join ' ').Trim()
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($featureDesc)) {
|
||||
Write-Error "Error: Feature description cannot be empty or contain only whitespace"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-HighestNumberFromSpecs {
|
||||
param([string]$SpecsDir)
|
||||
|
||||
[long]$highest = 0
|
||||
if (Test-Path $SpecsDir) {
|
||||
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
|
||||
if ($_.Name -match '^(\d{3,})-' -and $_.Name -notmatch '^\d{8}-\d{6}-') {
|
||||
[long]$num = 0
|
||||
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
|
||||
$highest = $num
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $highest
|
||||
}
|
||||
|
||||
function Get-HighestNumberFromNames {
|
||||
param([string[]]$Names)
|
||||
|
||||
[long]$highest = 0
|
||||
foreach ($name in $Names) {
|
||||
if ($name -match '^(\d{3,})-' -and $name -notmatch '^\d{8}-\d{6}-') {
|
||||
[long]$num = 0
|
||||
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
|
||||
$highest = $num
|
||||
}
|
||||
}
|
||||
}
|
||||
return $highest
|
||||
}
|
||||
|
||||
function Get-HighestNumberFromBranches {
|
||||
param()
|
||||
|
||||
try {
|
||||
$branches = git branch -a 2>$null
|
||||
if ($LASTEXITCODE -eq 0 -and $branches) {
|
||||
$cleanNames = $branches | ForEach-Object {
|
||||
$_.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
|
||||
}
|
||||
return Get-HighestNumberFromNames -Names $cleanNames
|
||||
}
|
||||
} catch {
|
||||
Write-Verbose "Could not check Git branches: $_"
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function Get-HighestNumberFromRemoteRefs {
|
||||
[long]$highest = 0
|
||||
try {
|
||||
$remotes = git remote 2>$null
|
||||
if ($remotes) {
|
||||
foreach ($remote in $remotes) {
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
$refs = git ls-remote --heads $remote 2>$null
|
||||
$env:GIT_TERMINAL_PROMPT = $null
|
||||
if ($LASTEXITCODE -eq 0 -and $refs) {
|
||||
$refNames = $refs | ForEach-Object {
|
||||
if ($_ -match 'refs/heads/(.+)$') { $matches[1] }
|
||||
} | Where-Object { $_ }
|
||||
$remoteHighest = Get-HighestNumberFromNames -Names $refNames
|
||||
if ($remoteHighest -gt $highest) { $highest = $remoteHighest }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Verbose "Could not query remote refs: $_"
|
||||
}
|
||||
return $highest
|
||||
}
|
||||
|
||||
function Get-NextBranchNumber {
|
||||
param(
|
||||
[string]$SpecsDir,
|
||||
[switch]$SkipFetch
|
||||
)
|
||||
|
||||
if ($SkipFetch) {
|
||||
$highestBranch = Get-HighestNumberFromBranches
|
||||
$highestRemote = Get-HighestNumberFromRemoteRefs
|
||||
$highestBranch = [Math]::Max($highestBranch, $highestRemote)
|
||||
} else {
|
||||
try {
|
||||
git fetch --all --prune 2>$null | Out-Null
|
||||
} catch { }
|
||||
$highestBranch = Get-HighestNumberFromBranches
|
||||
}
|
||||
|
||||
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
|
||||
$maxNum = [Math]::Max($highestBranch, $highestSpec)
|
||||
return $maxNum + 1
|
||||
}
|
||||
|
||||
function ConvertTo-CleanBranchName {
|
||||
param([string]$Name)
|
||||
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Source common.ps1 from the project's installed scripts.
|
||||
# Search locations in priority order:
|
||||
# 1. .specify/scripts/powershell/common.ps1 under the project root
|
||||
# 2. scripts/powershell/common.ps1 under the project root (source checkout)
|
||||
# 3. git-common.ps1 next to this script (minimal fallback)
|
||||
# ---------------------------------------------------------------------------
|
||||
function Find-ProjectRoot {
|
||||
param([string]$StartDir)
|
||||
$current = Resolve-Path $StartDir
|
||||
while ($true) {
|
||||
foreach ($marker in @('.specify', '.git')) {
|
||||
if (Test-Path (Join-Path $current $marker)) {
|
||||
return $current
|
||||
}
|
||||
}
|
||||
$parent = Split-Path $current -Parent
|
||||
if ($parent -eq $current) { return $null }
|
||||
$current = $parent
|
||||
}
|
||||
}
|
||||
|
||||
$projectRoot = Find-ProjectRoot -StartDir $PSScriptRoot
|
||||
$commonLoaded = $false
|
||||
|
||||
if ($projectRoot) {
|
||||
$candidates = @(
|
||||
(Join-Path $projectRoot ".specify/scripts/powershell/common.ps1"),
|
||||
(Join-Path $projectRoot "scripts/powershell/common.ps1")
|
||||
)
|
||||
foreach ($candidate in $candidates) {
|
||||
if (Test-Path $candidate) {
|
||||
. $candidate
|
||||
$commonLoaded = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $commonLoaded -and (Test-Path "$PSScriptRoot/git-common.ps1")) {
|
||||
. "$PSScriptRoot/git-common.ps1"
|
||||
$commonLoaded = $true
|
||||
}
|
||||
|
||||
if (-not $commonLoaded) {
|
||||
throw "Unable to locate common script file. Please ensure the Specify core scripts are installed."
|
||||
}
|
||||
|
||||
# Resolve repository root
|
||||
if (Get-Command Get-RepoRoot -ErrorAction SilentlyContinue) {
|
||||
$repoRoot = Get-RepoRoot
|
||||
} elseif ($projectRoot) {
|
||||
$repoRoot = $projectRoot
|
||||
} else {
|
||||
throw "Could not determine repository root."
|
||||
}
|
||||
|
||||
# Check if git is available
|
||||
if (Get-Command Test-HasGit -ErrorAction SilentlyContinue) {
|
||||
# Call without parameters for compatibility with core common.ps1 (no -RepoRoot param)
|
||||
# and git-common.ps1 (has -RepoRoot param with default).
|
||||
$hasGit = Test-HasGit
|
||||
} else {
|
||||
try {
|
||||
git -C $repoRoot rev-parse --is-inside-work-tree 2>$null | Out-Null
|
||||
$hasGit = ($LASTEXITCODE -eq 0)
|
||||
} catch {
|
||||
$hasGit = $false
|
||||
}
|
||||
}
|
||||
|
||||
Set-Location $repoRoot
|
||||
|
||||
$specsDir = Join-Path $repoRoot 'specs'
|
||||
|
||||
function Get-BranchName {
|
||||
param([string]$Description)
|
||||
|
||||
$stopWords = @(
|
||||
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
|
||||
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
|
||||
'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may', 'might', 'must', 'shall',
|
||||
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
|
||||
'want', 'need', 'add', 'get', 'set'
|
||||
)
|
||||
|
||||
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
|
||||
$words = $cleanName -split '\s+' | Where-Object { $_ }
|
||||
|
||||
$meaningfulWords = @()
|
||||
foreach ($word in $words) {
|
||||
if ($stopWords -contains $word) { continue }
|
||||
if ($word.Length -ge 3) {
|
||||
$meaningfulWords += $word
|
||||
} elseif ($Description -match "\b$($word.ToUpper())\b") {
|
||||
$meaningfulWords += $word
|
||||
}
|
||||
}
|
||||
|
||||
if ($meaningfulWords.Count -gt 0) {
|
||||
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
|
||||
$result = ($meaningfulWords | Select-Object -First $maxWords) -join '-'
|
||||
return $result
|
||||
} else {
|
||||
$result = ConvertTo-CleanBranchName -Name $Description
|
||||
$fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
|
||||
return [string]::Join('-', $fallbackWords)
|
||||
}
|
||||
}
|
||||
|
||||
# Check for GIT_BRANCH_NAME env var override (exact branch name, no prefix/suffix)
|
||||
if ($env:GIT_BRANCH_NAME) {
|
||||
$branchName = $env:GIT_BRANCH_NAME
|
||||
# Check 244-byte limit (UTF-8) for override names
|
||||
$branchNameUtf8ByteCount = [System.Text.Encoding]::UTF8.GetByteCount($branchName)
|
||||
if ($branchNameUtf8ByteCount -gt 244) {
|
||||
throw "GIT_BRANCH_NAME must be 244 bytes or fewer in UTF-8. Provided value is $branchNameUtf8ByteCount bytes; please supply a shorter override branch name."
|
||||
}
|
||||
# Extract FEATURE_NUM from the branch name if it starts with a numeric prefix
|
||||
# Check timestamp pattern first (YYYYMMDD-HHMMSS-) since it also matches the simpler ^\d+ pattern
|
||||
if ($branchName -match '^(\d{8}-\d{6})-') {
|
||||
$featureNum = $matches[1]
|
||||
} elseif ($branchName -match '^(\d+)-') {
|
||||
$featureNum = $matches[1]
|
||||
} else {
|
||||
$featureNum = $branchName
|
||||
}
|
||||
} else {
|
||||
if ($ShortName) {
|
||||
$branchSuffix = ConvertTo-CleanBranchName -Name $ShortName
|
||||
} else {
|
||||
$branchSuffix = Get-BranchName -Description $featureDesc
|
||||
}
|
||||
|
||||
if ($Timestamp -and $Number -ne 0) {
|
||||
Write-Warning "[specify] Warning: -Number is ignored when -Timestamp is used"
|
||||
$Number = 0
|
||||
}
|
||||
|
||||
if ($Timestamp) {
|
||||
$featureNum = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$branchName = "$featureNum-$branchSuffix"
|
||||
} else {
|
||||
if ($Number -eq 0) {
|
||||
if ($DryRun -and $hasGit) {
|
||||
$Number = Get-NextBranchNumber -SpecsDir $specsDir -SkipFetch
|
||||
} elseif ($DryRun) {
|
||||
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
||||
} elseif ($hasGit) {
|
||||
$Number = Get-NextBranchNumber -SpecsDir $specsDir
|
||||
} else {
|
||||
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
||||
}
|
||||
}
|
||||
|
||||
$featureNum = ('{0:000}' -f $Number)
|
||||
$branchName = "$featureNum-$branchSuffix"
|
||||
}
|
||||
}
|
||||
|
||||
$maxBranchLength = 244
|
||||
if ($branchName.Length -gt $maxBranchLength) {
|
||||
$prefixLength = $featureNum.Length + 1
|
||||
$maxSuffixLength = $maxBranchLength - $prefixLength
|
||||
|
||||
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
|
||||
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
||||
|
||||
$originalBranchName = $branchName
|
||||
$branchName = "$featureNum-$truncatedSuffix"
|
||||
|
||||
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
|
||||
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
|
||||
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
|
||||
}
|
||||
|
||||
if (-not $DryRun) {
|
||||
if ($hasGit) {
|
||||
$branchCreated = $false
|
||||
$branchCreateError = ''
|
||||
try {
|
||||
$branchCreateError = git checkout -q -b $branchName 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$branchCreated = $true
|
||||
}
|
||||
} catch {
|
||||
$branchCreateError = $_.Exception.Message
|
||||
}
|
||||
|
||||
if (-not $branchCreated) {
|
||||
$currentBranch = ''
|
||||
try { $currentBranch = (git rev-parse --abbrev-ref HEAD 2>$null).Trim() } catch {}
|
||||
$existingBranch = git branch --list $branchName 2>$null
|
||||
if ($existingBranch) {
|
||||
if ($AllowExistingBranch) {
|
||||
if ($currentBranch -eq $branchName) {
|
||||
# Already on the target branch
|
||||
} else {
|
||||
$switchBranchError = git checkout -q $branchName 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
if ($switchBranchError) {
|
||||
Write-Error "Error: Branch '$branchName' exists but could not be checked out.`n$($switchBranchError.Trim())"
|
||||
} else {
|
||||
Write-Error "Error: Branch '$branchName' exists but could not be checked out. Resolve any uncommitted changes or conflicts and try again."
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} elseif ($Timestamp) {
|
||||
Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName."
|
||||
exit 1
|
||||
} else {
|
||||
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
if ($branchCreateError) {
|
||||
Write-Error "Error: Failed to create git branch '$branchName'.`n$($branchCreateError.Trim())"
|
||||
} else {
|
||||
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($Json) {
|
||||
[Console]::Error.WriteLine("[specify] Warning: Git repository not detected; skipped branch creation for $branchName")
|
||||
} else {
|
||||
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
|
||||
}
|
||||
}
|
||||
|
||||
$env:SPECIFY_FEATURE = $branchName
|
||||
}
|
||||
|
||||
if ($Json) {
|
||||
$obj = [PSCustomObject]@{
|
||||
BRANCH_NAME = $branchName
|
||||
FEATURE_NUM = $featureNum
|
||||
HAS_GIT = $hasGit
|
||||
}
|
||||
if ($DryRun) {
|
||||
$obj | Add-Member -NotePropertyName 'DRY_RUN' -NotePropertyValue $true
|
||||
}
|
||||
$obj | ConvertTo-Json -Compress
|
||||
} else {
|
||||
Write-Output "BRANCH_NAME: $branchName"
|
||||
Write-Output "FEATURE_NUM: $featureNum"
|
||||
Write-Output "HAS_GIT: $hasGit"
|
||||
if (-not $DryRun) {
|
||||
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
||||
}
|
||||
}
|
||||
51
.specify/extensions/git/scripts/powershell/git-common.ps1
Normal file
51
.specify/extensions/git/scripts/powershell/git-common.ps1
Normal file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Git-specific common functions for the git extension.
|
||||
# Extracted from scripts/powershell/common.ps1 — contains only git-specific
|
||||
# branch validation and detection logic.
|
||||
|
||||
function Test-HasGit {
|
||||
param([string]$RepoRoot = (Get-Location))
|
||||
try {
|
||||
if (-not (Test-Path (Join-Path $RepoRoot '.git'))) { return $false }
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { return $false }
|
||||
git -C $RepoRoot rev-parse --is-inside-work-tree 2>$null | Out-Null
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SpecKitEffectiveBranchName {
|
||||
param([string]$Branch)
|
||||
if ($Branch -match '^([^/]+)/([^/]+)$') {
|
||||
return $Matches[2]
|
||||
}
|
||||
return $Branch
|
||||
}
|
||||
|
||||
function Test-FeatureBranch {
|
||||
param(
|
||||
[string]$Branch,
|
||||
[bool]$HasGit = $true
|
||||
)
|
||||
|
||||
# For non-git repos, we can't enforce branch naming but still provide output
|
||||
if (-not $HasGit) {
|
||||
Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation"
|
||||
return $true
|
||||
}
|
||||
|
||||
$raw = $Branch
|
||||
$Branch = Get-SpecKitEffectiveBranchName $raw
|
||||
|
||||
# Accept sequential prefix (3+ digits) but exclude malformed timestamps
|
||||
# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
|
||||
$hasMalformedTimestamp = ($Branch -match '^[0-9]{7}-[0-9]{6}-') -or ($Branch -match '^(?:\d{7}|\d{8})-\d{6}$')
|
||||
$isSequential = ($Branch -match '^[0-9]{3,}-') -and (-not $hasMalformedTimestamp)
|
||||
if (-not $isSequential -and $Branch -notmatch '^\d{8}-\d{6}-') {
|
||||
[Console]::Error.WriteLine("ERROR: Not on a feature branch. Current branch: $raw")
|
||||
[Console]::Error.WriteLine("Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name")
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Git extension: initialize-repo.ps1
|
||||
# Initialize a Git repository with an initial commit.
|
||||
# Customizable — replace this script to add .gitignore templates,
|
||||
# default branch config, git-flow, LFS, signing, etc.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Find project root
|
||||
function Find-ProjectRoot {
|
||||
param([string]$StartDir)
|
||||
$current = Resolve-Path $StartDir
|
||||
while ($true) {
|
||||
foreach ($marker in @('.specify', '.git')) {
|
||||
if (Test-Path (Join-Path $current $marker)) {
|
||||
return $current
|
||||
}
|
||||
}
|
||||
$parent = Split-Path $current -Parent
|
||||
if ($parent -eq $current) { return $null }
|
||||
$current = $parent
|
||||
}
|
||||
}
|
||||
|
||||
$repoRoot = Find-ProjectRoot -StartDir $PSScriptRoot
|
||||
if (-not $repoRoot) { $repoRoot = Get-Location }
|
||||
Set-Location $repoRoot
|
||||
|
||||
# Read commit message from extension config, fall back to default
|
||||
$commitMsg = "[Spec Kit] Initial commit"
|
||||
$configFile = Join-Path $repoRoot ".specify/extensions/git/git-config.yml"
|
||||
if (Test-Path $configFile) {
|
||||
foreach ($line in Get-Content $configFile) {
|
||||
if ($line -match '^init_commit_message:\s*(.+)$') {
|
||||
$val = $matches[1].Trim() -replace '^["'']' -replace '["'']$'
|
||||
if ($val) { $commitMsg = $val }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check if git is available
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
Write-Warning "[specify] Warning: Git not found; skipped repository initialization"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if already a git repo
|
||||
try {
|
||||
git rev-parse --is-inside-work-tree 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Warning "[specify] Git repository already initialized; skipping"
|
||||
exit 0
|
||||
}
|
||||
} catch { }
|
||||
|
||||
# Initialize
|
||||
try {
|
||||
$out = git init -q 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw "git init failed: $out" }
|
||||
$out = git add . 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
|
||||
$out = git commit --allow-empty -q -m $commitMsg 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw "git commit failed: $out" }
|
||||
} catch {
|
||||
Write-Warning "[specify] Error: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✓ Git repository initialized"
|
||||
10
.specify/init-options.json
Normal file
10
.specify/init-options.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"ai": "copilot",
|
||||
"branch_numbering": "sequential",
|
||||
"context_file": ".github/copilot-instructions.md",
|
||||
"here": true,
|
||||
"integration": "copilot",
|
||||
"preset": null,
|
||||
"script": "sh",
|
||||
"speckit_version": "0.7.4"
|
||||
}
|
||||
4
.specify/integration.json
Normal file
4
.specify/integration.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"integration": "copilot",
|
||||
"version": "0.7.4"
|
||||
}
|
||||
25
.specify/integrations/copilot.manifest.json
Normal file
25
.specify/integrations/copilot.manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"integration": "copilot",
|
||||
"version": "0.7.4",
|
||||
"installed_at": "2026-04-22T21:58:02.962169+00:00",
|
||||
"files": {
|
||||
".github/agents/speckit.analyze.agent.md": "699032fdd49afe31d23c7191f3fe7bcb1d14b081fbc94c2287e6ba3a57574fda",
|
||||
".github/agents/speckit.checklist.agent.md": "d7d691689fe45427c868dcf18ade4df500f0c742a6c91923fefba405d6466dde",
|
||||
".github/agents/speckit.clarify.agent.md": "0cc766dcc5cab233ccdf3bc4cfb5759a6d7d1e13e29f611083046f818f5812bb",
|
||||
".github/agents/speckit.constitution.agent.md": "58d35eb026f56bb7364d91b8b0382d5dd1249ded6c1449a2b69546693afb85f7",
|
||||
".github/agents/speckit.implement.agent.md": "83628415c86ba487b3a083c7a2c0f016c9073abd02c1c7f4a30cff949b6602c0",
|
||||
".github/agents/speckit.plan.agent.md": "2ad128b81ccd8f5bfa78b3b43101f377dfddd8f800fa0856f85bf53b1489b783",
|
||||
".github/agents/speckit.specify.agent.md": "5bbb5270836cc9a3286ce3ed96a500f3d383a54abb06aa11b01a2d2f76dbf39b",
|
||||
".github/agents/speckit.tasks.agent.md": "a58886f29f75e1a14840007772ddd954742aafb3e03d9d1231bee033e6c1626b",
|
||||
".github/agents/speckit.taskstoissues.agent.md": "e84794f7a839126defb364ca815352c5c2b2d20db2d6da399fa53e4ddbb7b3ee",
|
||||
".github/prompts/speckit.analyze.prompt.md": "bb93dbbafa96d07b7cd07fc7061d8adb0c6b26cb772a52d0dce263b1ca2b9b77",
|
||||
".github/prompts/speckit.checklist.prompt.md": "c3aea7526c5cbfd8665acc9508ad5a9a3f71e91a63c36be7bed13a834c3a683c",
|
||||
".github/prompts/speckit.clarify.prompt.md": "ce79b3437ca918d46ac858eb4b8b44d3b0a02c563660c60d94c922a7b5d8d4f4",
|
||||
".github/prompts/speckit.constitution.prompt.md": "38f937279de14387601422ddfda48365debdbaf47b2d513527b8f6d8a27d499d",
|
||||
".github/prompts/speckit.implement.prompt.md": "5053a17fb9238338c63b898ee9c80b2cb4ad1a90c6071fe3748de76864ac6a80",
|
||||
".github/prompts/speckit.plan.prompt.md": "2098dae6bd9277335f31cb150b78bfb1de539c0491798e5cfe382c89ab0bcd0e",
|
||||
".github/prompts/speckit.specify.prompt.md": "7b2cc4dc6462da1c96df46bac4f60e53baba3097f4b24ac3f9b684194458aa98",
|
||||
".github/prompts/speckit.tasks.prompt.md": "88fc57c289f99d5e9d35c255f3e2683f73ecb0a5155dcb4d886f82f52b11841f",
|
||||
".github/prompts/speckit.taskstoissues.prompt.md": "2f9636d4f312a1470f000747cb62677fec0655d8b4e2357fa4fbf238965fa66d"
|
||||
}
|
||||
}
|
||||
8
.specify/integrations/speckit.manifest.json
Normal file
8
.specify/integrations/speckit.manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"integration": "speckit",
|
||||
"version": "0.7.4",
|
||||
"installed_at": "2026-04-22T21:58:02.965809+00:00",
|
||||
"files": {
|
||||
".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3"
|
||||
}
|
||||
}
|
||||
50
.specify/templates/constitution-template.md
Normal file
50
.specify/templates/constitution-template.md
Normal file
@ -0,0 +1,50 @@
|
||||
# [PROJECT_NAME] Constitution
|
||||
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
|
||||
|
||||
## Core Principles
|
||||
|
||||
### [PRINCIPLE_1_NAME]
|
||||
<!-- Example: I. Library-First -->
|
||||
[PRINCIPLE_1_DESCRIPTION]
|
||||
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
|
||||
|
||||
### [PRINCIPLE_2_NAME]
|
||||
<!-- Example: II. CLI Interface -->
|
||||
[PRINCIPLE_2_DESCRIPTION]
|
||||
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
|
||||
|
||||
### [PRINCIPLE_3_NAME]
|
||||
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
|
||||
[PRINCIPLE_3_DESCRIPTION]
|
||||
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
|
||||
|
||||
### [PRINCIPLE_4_NAME]
|
||||
<!-- Example: IV. Integration Testing -->
|
||||
[PRINCIPLE_4_DESCRIPTION]
|
||||
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
|
||||
|
||||
### [PRINCIPLE_5_NAME]
|
||||
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
|
||||
[PRINCIPLE_5_DESCRIPTION]
|
||||
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
|
||||
|
||||
## [SECTION_2_NAME]
|
||||
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
|
||||
|
||||
[SECTION_2_CONTENT]
|
||||
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
|
||||
|
||||
## [SECTION_3_NAME]
|
||||
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
|
||||
|
||||
[SECTION_3_CONTENT]
|
||||
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
|
||||
|
||||
## Governance
|
||||
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
||||
|
||||
[GOVERNANCE_RULES]
|
||||
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
|
||||
|
||||
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
||||
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
|
||||
63
.specify/workflows/speckit/workflow.yml
Normal file
63
.specify/workflows/speckit/workflow.yml
Normal file
@ -0,0 +1,63 @@
|
||||
schema_version: "1.0"
|
||||
workflow:
|
||||
id: "speckit"
|
||||
name: "Full SDD Cycle"
|
||||
version: "1.0.0"
|
||||
author: "GitHub"
|
||||
description: "Runs specify → plan → tasks → implement with review gates"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.7.2"
|
||||
integrations:
|
||||
any: ["copilot", "claude", "gemini"]
|
||||
|
||||
inputs:
|
||||
spec:
|
||||
type: string
|
||||
required: true
|
||||
prompt: "Describe what you want to build"
|
||||
integration:
|
||||
type: string
|
||||
default: "copilot"
|
||||
prompt: "Integration to use (e.g. claude, copilot, gemini)"
|
||||
scope:
|
||||
type: string
|
||||
default: "full"
|
||||
enum: ["full", "backend-only", "frontend-only"]
|
||||
|
||||
steps:
|
||||
- id: specify
|
||||
command: speckit.specify
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
|
||||
- id: review-spec
|
||||
type: gate
|
||||
message: "Review the generated spec before planning."
|
||||
options: [approve, reject]
|
||||
on_reject: abort
|
||||
|
||||
- id: plan
|
||||
command: speckit.plan
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
|
||||
- id: review-plan
|
||||
type: gate
|
||||
message: "Review the plan before generating tasks."
|
||||
options: [approve, reject]
|
||||
on_reject: abort
|
||||
|
||||
- id: tasks
|
||||
command: speckit.tasks
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
|
||||
- id: implement
|
||||
command: speckit.implement
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
13
.specify/workflows/workflow-registry.json
Normal file
13
.specify/workflows/workflow-registry.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"workflows": {
|
||||
"speckit": {
|
||||
"name": "Full SDD Cycle",
|
||||
"version": "1.0.0",
|
||||
"description": "Runs specify \u2192 plan \u2192 tasks \u2192 implement with review gates",
|
||||
"source": "bundled",
|
||||
"installed_at": "2026-04-22T21:58:03.039039+00:00",
|
||||
"updated_at": "2026-04-22T21:58:03.039046+00:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user