From 236135d0b92b9290bb87a65c20f7f1180bfd907e Mon Sep 17 00:00:00 2001 From: aligneddev Date: Tue, 14 Apr 2026 16:08:10 +0000 Subject: [PATCH] upgrade speckit --- .github/agents/speckit.analyze.agent.md | 71 ++- .github/agents/speckit.checklist.agent.md | 66 +++ .github/agents/speckit.clarify.agent.md | 68 ++- .github/agents/speckit.constitution.agent.md | 66 +++ .github/agents/speckit.git.commit.agent.md | 51 ++ .github/agents/speckit.git.feature.agent.md | 70 +++ .../agents/speckit.git.initialize.agent.md | 52 ++ .github/agents/speckit.git.remote.agent.md | 48 ++ .github/agents/speckit.git.validate.agent.md | 52 ++ .github/agents/speckit.implement.agent.md | 65 ++- .github/agents/speckit.plan.agent.md | 63 +++ .github/agents/speckit.specify.agent.md | 145 ++++-- .github/agents/speckit.tasks.agent.md | 63 +++ .github/agents/speckit.taskstoissues.agent.md | 66 +++ .github/prompts/speckit.git.commit.prompt.md | 3 + .github/prompts/speckit.git.feature.prompt.md | 3 + .../prompts/speckit.git.initialize.prompt.md | 3 + .github/prompts/speckit.git.remote.prompt.md | 3 + .../prompts/speckit.git.validate.prompt.md | 3 + .specify/extensions.yml | 148 ++++++ .specify/extensions/.registry | 23 + .specify/extensions/git/README.md | 100 ++++ .../git/commands/speckit.git.commit.md | 48 ++ .../git/commands/speckit.git.feature.md | 67 +++ .../git/commands/speckit.git.initialize.md | 49 ++ .../git/commands/speckit.git.remote.md | 45 ++ .../git/commands/speckit.git.validate.md | 49 ++ .specify/extensions/git/config-template.yml | 62 +++ .specify/extensions/git/extension.yml | 140 ++++++ .specify/extensions/git/git-config.yml | 62 +++ .../git/scripts/bash/auto-commit.sh | 140 ++++++ .../git/scripts/bash/create-new-feature.sh | 453 ++++++++++++++++++ .../extensions/git/scripts/bash/git-common.sh | 41 ++ .../git/scripts/bash/initialize-repo.sh | 54 +++ .../git/scripts/powershell/auto-commit.ps1 | 149 ++++++ .../scripts/powershell/create-new-feature.ps1 | 403 ++++++++++++++++ .../git/scripts/powershell/git-common.ps1 | 50 ++ .../scripts/powershell/initialize-repo.ps1 | 69 +++ .specify/init-options.json | 9 + .specify/integration.json | 7 + .specify/integrations/copilot.manifest.json | 27 ++ .../copilot/scripts/update-context.ps1 | 32 ++ .../copilot/scripts/update-context.sh | 37 ++ .specify/integrations/speckit.manifest.json | 6 + 44 files changed, 3188 insertions(+), 43 deletions(-) create mode 100644 .github/agents/speckit.git.commit.agent.md create mode 100644 .github/agents/speckit.git.feature.agent.md create mode 100644 .github/agents/speckit.git.initialize.agent.md create mode 100644 .github/agents/speckit.git.remote.agent.md create mode 100644 .github/agents/speckit.git.validate.agent.md create mode 100644 .github/prompts/speckit.git.commit.prompt.md create mode 100644 .github/prompts/speckit.git.feature.prompt.md create mode 100644 .github/prompts/speckit.git.initialize.prompt.md create mode 100644 .github/prompts/speckit.git.remote.prompt.md create mode 100644 .github/prompts/speckit.git.validate.prompt.md create mode 100644 .specify/extensions.yml create mode 100644 .specify/extensions/.registry create mode 100644 .specify/extensions/git/README.md create mode 100644 .specify/extensions/git/commands/speckit.git.commit.md create mode 100644 .specify/extensions/git/commands/speckit.git.feature.md create mode 100644 .specify/extensions/git/commands/speckit.git.initialize.md create mode 100644 .specify/extensions/git/commands/speckit.git.remote.md create mode 100644 .specify/extensions/git/commands/speckit.git.validate.md create mode 100644 .specify/extensions/git/config-template.yml create mode 100644 .specify/extensions/git/extension.yml create mode 100644 .specify/extensions/git/git-config.yml create mode 100644 .specify/extensions/git/scripts/bash/auto-commit.sh create mode 100644 .specify/extensions/git/scripts/bash/create-new-feature.sh create mode 100644 .specify/extensions/git/scripts/bash/git-common.sh create mode 100644 .specify/extensions/git/scripts/bash/initialize-repo.sh create mode 100644 .specify/extensions/git/scripts/powershell/auto-commit.ps1 create mode 100644 .specify/extensions/git/scripts/powershell/create-new-feature.ps1 create mode 100644 .specify/extensions/git/scripts/powershell/git-common.ps1 create mode 100644 .specify/extensions/git/scripts/powershell/initialize-repo.ps1 create mode 100644 .specify/init-options.json create mode 100644 .specify/integration.json create mode 100644 .specify/integrations/copilot.manifest.json create mode 100644 .specify/integrations/copilot/scripts/update-context.ps1 create mode 100644 .specify/integrations/copilot/scripts/update-context.sh create mode 100644 .specify/integrations/speckit.manifest.json diff --git a/.github/agents/speckit.analyze.agent.md b/.github/agents/speckit.analyze.agent.md index 542a3de..45b84bf 100644 --- a/.github/agents/speckit.analyze.agent.md +++ b/.github/agents/speckit.analyze.agent.md @@ -10,6 +10,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before analysis)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_analyze` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Goal. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Goal Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`. @@ -41,7 +75,7 @@ Load only the minimal necessary context from each artifact: - Overview/Context - Functional Requirements -- Non-Functional Requirements +- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) - User Stories - Edge Cases (if present) @@ -68,7 +102,7 @@ Load only the minimal necessary context from each artifact: Create internal representations (do not include raw artifacts in output): -- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`) +- **Requirements inventory**: For each Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%"). - **User story/action inventory**: Discrete user actions with acceptance criteria - **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) - **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements @@ -102,7 +136,7 @@ Focus on high-signal findings. Limit to 50 findings total; aggregate remainder i - Requirements with zero associated tasks - Tasks with no mapped requirement/story -- Non-functional requirements not reflected in tasks (e.g., performance, security) +- Success Criteria requiring buildable work (performance, security, availability) not reflected in tasks #### F. Inconsistency @@ -162,6 +196,37 @@ At end of report, output a concise Next Actions block: Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.) +### 9. Check for extension hooks + +After reporting, check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_analyze` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Operating Principles ### Context Efficiency diff --git a/.github/agents/speckit.checklist.agent.md b/.github/agents/speckit.checklist.agent.md index 004de01..6115a49 100644 --- a/.github/agents/speckit.checklist.agent.md +++ b/.github/agents/speckit.checklist.agent.md @@ -31,6 +31,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before checklist generation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_checklist` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Execution Steps. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Execution Steps 1. **Setup**: Run `.specify/scripts/powershell/check-prerequisites.ps1 -Json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list. @@ -293,3 +327,35 @@ Sample items: - Correct: Validation of requirement quality - Wrong: "Does it do X?" - Correct: "Is X clearly specified?" + +## Post-Execution Checks + +**Check for extension hooks (after checklist generation)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_checklist` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.github/agents/speckit.clarify.agent.md b/.github/agents/speckit.clarify.agent.md index 328e964..63c0708 100644 --- a/.github/agents/speckit.clarify.agent.md +++ b/.github/agents/speckit.clarify.agent.md @@ -14,6 +14,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before clarification)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_clarify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. @@ -142,7 +176,7 @@ Execution steps: - Functional ambiguity → Update or add a bullet in Functional Requirements. - User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario. - Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly. - - Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target). + - Non-functional constraint → Add/modify measurable criteria in Success Criteria > Measurable Outcomes (convert vague adjective to metric or explicit target). - Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it). - Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once. - If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text. @@ -179,3 +213,35 @@ Behavior rules: - If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale. Context for prioritization: $ARGUMENTS + +## Post-Execution Checks + +**Check for extension hooks (after clarification)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_clarify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.github/agents/speckit.constitution.agent.md b/.github/agents/speckit.constitution.agent.md index 63d4f66..29ae9a0 100644 --- a/.github/agents/speckit.constitution.agent.md +++ b/.github/agents/speckit.constitution.agent.md @@ -14,6 +14,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before constitution update)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_constitution` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts. @@ -82,3 +116,35 @@ If the user supplies partial updates (e.g., only one principle revision), still If critical info missing (e.g., ratification date truly unknown), insert `TODO(): explanation` and include in the Sync Impact Report under deferred items. Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file. + +## Post-Execution Checks + +**Check for extension hooks (after constitution update)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_constitution` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.github/agents/speckit.git.commit.agent.md b/.github/agents/speckit.git.commit.agent.md new file mode 100644 index 0000000..9ece588 --- /dev/null +++ b/.github/agents/speckit.git.commit.agent.md @@ -0,0 +1,51 @@ +--- +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 ` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 ` + +Replace `` 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 \ No newline at end of file diff --git a/.github/agents/speckit.git.feature.agent.md b/.github/agents/speckit.git.feature.agent.md new file mode 100644 index 0000000..ab5be09 --- /dev/null +++ b/.github/agents/speckit.git.feature.agent.md @@ -0,0 +1,70 @@ +--- +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 "" ""` +- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "" ""` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "" ""` +- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "" ""` + +**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 \ No newline at end of file diff --git a/.github/agents/speckit.git.initialize.agent.md b/.github/agents/speckit.git.initialize.agent.md new file mode 100644 index 0000000..597e6f7 --- /dev/null +++ b/.github/agents/speckit.git.initialize.agent.md @@ -0,0 +1,52 @@ +--- +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 \ No newline at end of file diff --git a/.github/agents/speckit.git.remote.agent.md b/.github/agents/speckit.git.remote.agent.md new file mode 100644 index 0000000..5601e64 --- /dev/null +++ b/.github/agents/speckit.git.remote.agent.md @@ -0,0 +1,48 @@ +--- +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//.git` +- SSH: `git@github.com:/.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 \ No newline at end of file diff --git a/.github/agents/speckit.git.validate.agent.md b/.github/agents/speckit.git.validate.agent.md new file mode 100644 index 0000000..4359735 --- /dev/null +++ b/.github/agents/speckit.git.validate.agent.md @@ -0,0 +1,52 @@ +--- +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: ` +- Check if the corresponding spec directory exists under `specs/`: + - For sequential branches, look for `specs/-*` where prefix matches the numeric portion + - For timestamp branches, look for `specs/-*` where prefix matches the `YYYYMMDD-HHMMSS` portion +- If spec directory exists: `✓ Spec directory found: ` +- If spec directory missing: `⚠ No spec directory found for prefix ` + +If NOT on a feature branch: +- Output: `✗ Not on a feature branch. Current branch: ` +- 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 \ No newline at end of file diff --git a/.github/agents/speckit.implement.agent.md b/.github/agents/speckit.implement.agent.md index a8cc290..d518c3d 100644 --- a/.github/agents/speckit.implement.agent.md +++ b/.github/agents/speckit.implement.agent.md @@ -10,6 +10,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before implementation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_implement` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. Run `.specify/scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). @@ -85,7 +119,7 @@ You **MUST** consider the user input before proceeding (if not empty). - **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*` - **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*` - **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*` - - **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*` + - **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `*.dll`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*` - **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/` - **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/` - **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/` @@ -133,3 +167,32 @@ You **MUST** consider the user input before proceeding (if not empty). - Report final status with summary of completed work Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list. + +10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_implement` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.github/agents/speckit.plan.agent.md b/.github/agents/speckit.plan.agent.md index d6b7d0b..3669df5 100644 --- a/.github/agents/speckit.plan.agent.md +++ b/.github/agents/speckit.plan.agent.md @@ -18,6 +18,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before planning)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_plan` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. **Setup**: Run `.specify/scripts/powershell/setup-plan.ps1 -Json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). @@ -35,6 +69,35 @@ You **MUST** consider the user input before proceeding (if not empty). 4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts. +5. **Check for extension hooks**: After reporting, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_plan` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Phases ### Phase 0: Outline & Research diff --git a/.github/agents/speckit.specify.agent.md b/.github/agents/speckit.specify.agent.md index 727c128..8d6bda6 100644 --- a/.github/agents/speckit.specify.agent.md +++ b/.github/agents/speckit.specify.agent.md @@ -18,13 +18,47 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before specification)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_specify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command. Given that feature description, do this: -1. **Generate a concise short name** (2-4 words) for the branch: +1. **Generate a concise short name** (2-4 words) for the feature: - Analyze the feature description and extract the most meaningful keywords - Create a 2-4 word short name that captures the essence of the feature - Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug") @@ -36,43 +70,47 @@ Given that feature description, do this: - "Create a dashboard for analytics" → "analytics-dashboard" - "Fix payment processing timeout bug" → "fix-payment-timeout" -2. **Check for existing branches before creating new one**: +2. **Branch creation** (optional, via hook): - a. First, fetch all remote branches to ensure we have the latest information: + If a `before_specify` hook ran successfully in the Pre-Execution Checks above, it will have created/switched to a git branch and output JSON containing `BRANCH_NAME` and `FEATURE_NUM`. Note these values for reference, but the branch name does **not** dictate the spec directory name. - ```bash - git fetch --all --prune - ``` + If the user explicitly provided `GIT_BRANCH_NAME`, pass it through to the hook so the branch script uses the exact value as the branch name (bypassing all prefix/suffix generation). - b. Find the highest feature number across all sources for the short-name: - - Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-$'` - - Local branches: `git branch | grep -E '^[* ]*[0-9]+-$'` - - Specs directories: Check for directories matching `specs/[0-9]+-` +3. **Create the spec feature directory**: - c. Determine the next available number: - - Extract all numbers from all three sources - - Find the highest number N - - Use N+1 for the new branch number + Specs live under the default `specs/` directory unless the user explicitly provides `SPECIFY_FEATURE_DIRECTORY`. - d. Run the script `.specify/scripts/powershell/create-new-feature.ps1 -Json "$ARGUMENTS"` with the calculated number and short-name: - - Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description - - Bash example: `.specify/scripts/powershell/create-new-feature.ps1 -Json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"` - - PowerShell example: `.specify/scripts/powershell/create-new-feature.ps1 -Json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"` + **Resolution order for `SPECIFY_FEATURE_DIRECTORY`**: + 1. If the user explicitly provided `SPECIFY_FEATURE_DIRECTORY` (e.g., via environment variable, argument, or configuration), use it as-is + 2. Otherwise, auto-generate it under `specs/`: + - Check `.specify/init-options.json` for `branch_numbering` + - If `"timestamp"`: prefix is `YYYYMMDD-HHMMSS` (current timestamp) + - If `"sequential"` or absent: prefix is `NNN` (next available 3-digit number after scanning existing directories in `specs/`) + - Construct the directory name: `-` (e.g., `003-user-auth` or `20260319-143022-user-auth`) + - Set `SPECIFY_FEATURE_DIRECTORY` to `specs/` - **IMPORTANT**: - - Check all three sources (remote branches, local branches, specs directories) to find the highest number - - Only match branches/directories with the exact short-name pattern - - If no existing branches/directories found with this short-name, start with number 1 - - You must only ever run this script once per feature - - The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for - - The JSON output will contain BRANCH_NAME and SPEC_FILE paths - - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot") + **Create the directory and spec file**: + - `mkdir -p SPECIFY_FEATURE_DIRECTORY` + - Copy `.specify/templates/spec-template.md` to `SPECIFY_FEATURE_DIRECTORY/spec.md` as the starting point + - Set `SPEC_FILE` to `SPECIFY_FEATURE_DIRECTORY/spec.md` + - Persist the resolved path to `.specify/feature.json`: + ```json + { + "feature_directory": "" + } + ``` + Write the actual resolved directory path value (for example, `specs/003-user-auth`), not the literal string `SPECIFY_FEATURE_DIRECTORY`. + This allows downstream commands (`/speckit.plan`, `/speckit.tasks`, etc.) to locate the feature directory without relying on git branch name conventions. -3. Load `.specify/templates/spec-template.md` to understand required sections. + **IMPORTANT**: + - You must only create one feature per `/speckit.specify` invocation + - The spec directory name and the git branch name are independent — they may be the same but that is the user's choice + - The spec directory and file are always created by this command, never by the hook -4. Follow this execution flow: +4. Load `.specify/templates/spec-template.md` to understand required sections. - 1. Parse user description from Input +5. Follow this execution flow: + 1. Parse user description from arguments If empty: ERROR "No feature description provided" 2. Extract key concepts from description Identify: actors, actions, data, constraints @@ -96,11 +134,11 @@ Given that feature description, do this: 7. Identify Key Entities (if data involved) 8. Return: SUCCESS (spec ready for planning) -5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. +6. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. -6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: +7. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: - a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items: + a. **Create Spec Quality Checklist**: Generate a checklist file at `SPECIFY_FEATURE_DIRECTORY/checklists/requirements.md` using the checklist template structure with these validation items: ```markdown # Specification Quality Checklist: [FEATURE NAME] @@ -145,7 +183,7 @@ Given that feature description, do this: c. **Handle Validation Results**: - - **If all items pass**: Mark checklist complete and proceed to step 6 + - **If all items pass**: Mark checklist complete and proceed to step 7 - **If items fail (excluding [NEEDS CLARIFICATION])**: 1. List the failing items and specific issues @@ -190,11 +228,42 @@ Given that feature description, do this: d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status -7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). - -**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. - -## General Guidelines +8. **Report completion** to the user with: + - `SPECIFY_FEATURE_DIRECTORY` — the feature directory path + - `SPEC_FILE` — the spec file path + - Checklist results summary + - Readiness for the next phase (`/speckit.clarify` or `/speckit.plan`) + +9. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_specify` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + +**NOTE:** Branch creation is handled by the `before_specify` hook (git extension). Spec directory and file creation are always handled by this core command. ## Quick Guidelines diff --git a/.github/agents/speckit.tasks.agent.md b/.github/agents/speckit.tasks.agent.md index 0e932da..d94a03b 100644 --- a/.github/agents/speckit.tasks.agent.md +++ b/.github/agents/speckit.tasks.agent.md @@ -19,6 +19,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before tasks generation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_tasks` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. **Setup**: Run `.specify/scripts/powershell/check-prerequisites.ps1 -Json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). @@ -60,6 +94,35 @@ You **MUST** consider the user input before proceeding (if not empty). - Suggested MVP scope (typically just User Story 1) - Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths) +6. **Check for extension hooks**: After tasks.md is generated, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_tasks` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + Context for task generation: $ARGUMENTS The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context. diff --git a/.github/agents/speckit.taskstoissues.agent.md b/.github/agents/speckit.taskstoissues.agent.md index 9ca5b17..aa5a7d9 100644 --- a/.github/agents/speckit.taskstoissues.agent.md +++ b/.github/agents/speckit.taskstoissues.agent.md @@ -11,6 +11,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before tasks-to-issues conversion)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_taskstoissues` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. Run `.specify/scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). @@ -28,3 +62,35 @@ git config --get remote.origin.url > [!CAUTION] > UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL + +## Post-Execution Checks + +**Check for extension hooks (after tasks-to-issues conversion)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_taskstoissues` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.github/prompts/speckit.git.commit.prompt.md b/.github/prompts/speckit.git.commit.prompt.md new file mode 100644 index 0000000..ababc2b --- /dev/null +++ b/.github/prompts/speckit.git.commit.prompt.md @@ -0,0 +1,3 @@ +--- +agent: speckit.git.commit +--- diff --git a/.github/prompts/speckit.git.feature.prompt.md b/.github/prompts/speckit.git.feature.prompt.md new file mode 100644 index 0000000..9d9c1b0 --- /dev/null +++ b/.github/prompts/speckit.git.feature.prompt.md @@ -0,0 +1,3 @@ +--- +agent: speckit.git.feature +--- diff --git a/.github/prompts/speckit.git.initialize.prompt.md b/.github/prompts/speckit.git.initialize.prompt.md new file mode 100644 index 0000000..cf62da8 --- /dev/null +++ b/.github/prompts/speckit.git.initialize.prompt.md @@ -0,0 +1,3 @@ +--- +agent: speckit.git.initialize +--- diff --git a/.github/prompts/speckit.git.remote.prompt.md b/.github/prompts/speckit.git.remote.prompt.md new file mode 100644 index 0000000..fc187d0 --- /dev/null +++ b/.github/prompts/speckit.git.remote.prompt.md @@ -0,0 +1,3 @@ +--- +agent: speckit.git.remote +--- diff --git a/.github/prompts/speckit.git.validate.prompt.md b/.github/prompts/speckit.git.validate.prompt.md new file mode 100644 index 0000000..e6fcb72 --- /dev/null +++ b/.github/prompts/speckit.git.validate.prompt.md @@ -0,0 +1,3 @@ +--- +agent: speckit.git.validate +--- diff --git a/.specify/extensions.yml b/.specify/extensions.yml new file mode 100644 index 0000000..dff278f --- /dev/null +++ b/.specify/extensions.yml @@ -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 diff --git a/.specify/extensions/.registry b/.specify/extensions/.registry new file mode 100644 index 0000000..6464d01 --- /dev/null +++ b/.specify/extensions/.registry @@ -0,0 +1,23 @@ +{ + "schema_version": "1.0", + "extensions": { + "git": { + "version": "1.0.0", + "source": "local", + "manifest_hash": "sha256:9731aa8143a72fbebfdb440f155038ab42642517c2b2bdbbf67c8fdbe076ed79", + "enabled": true, + "priority": 10, + "registered_commands": { + "copilot": [ + "speckit.git.feature", + "speckit.git.validate", + "speckit.git.remote", + "speckit.git.initialize", + "speckit.git.commit" + ] + }, + "registered_skills": [], + "installed_at": "2026-04-14T16:06:35.812985+00:00" + } + } +} \ No newline at end of file diff --git a/.specify/extensions/git/README.md b/.specify/extensions/git/README.md new file mode 100644 index 0000000..31ba75c --- /dev/null +++ b/.specify/extensions/git/README.md @@ -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) diff --git a/.specify/extensions/git/commands/speckit.git.commit.md b/.specify/extensions/git/commands/speckit.git.commit.md new file mode 100644 index 0000000..e606f91 --- /dev/null +++ b/.specify/extensions/git/commands/speckit.git.commit.md @@ -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 ` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 ` + +Replace `` 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 diff --git a/.specify/extensions/git/commands/speckit.git.feature.md b/.specify/extensions/git/commands/speckit.git.feature.md new file mode 100644 index 0000000..1a9c5e3 --- /dev/null +++ b/.specify/extensions/git/commands/speckit.git.feature.md @@ -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 "" ""` +- **Bash (timestamp)**: `.specify/extensions/git/scripts/bash/create-new-feature.sh --json --timestamp --short-name "" ""` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -ShortName "" ""` +- **PowerShell (timestamp)**: `.specify/extensions/git/scripts/powershell/create-new-feature.ps1 -Json -Timestamp -ShortName "" ""` + +**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 diff --git a/.specify/extensions/git/commands/speckit.git.initialize.md b/.specify/extensions/git/commands/speckit.git.initialize.md new file mode 100644 index 0000000..4451ee6 --- /dev/null +++ b/.specify/extensions/git/commands/speckit.git.initialize.md @@ -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 diff --git a/.specify/extensions/git/commands/speckit.git.remote.md b/.specify/extensions/git/commands/speckit.git.remote.md new file mode 100644 index 0000000..712a3e8 --- /dev/null +++ b/.specify/extensions/git/commands/speckit.git.remote.md @@ -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//.git` +- SSH: `git@github.com:/.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 diff --git a/.specify/extensions/git/commands/speckit.git.validate.md b/.specify/extensions/git/commands/speckit.git.validate.md new file mode 100644 index 0000000..dd84618 --- /dev/null +++ b/.specify/extensions/git/commands/speckit.git.validate.md @@ -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: ` +- Check if the corresponding spec directory exists under `specs/`: + - For sequential branches, look for `specs/-*` where prefix matches the numeric portion + - For timestamp branches, look for `specs/-*` where prefix matches the `YYYYMMDD-HHMMSS` portion +- If spec directory exists: `✓ Spec directory found: ` +- If spec directory missing: `⚠ No spec directory found for prefix ` + +If NOT on a feature branch: +- Output: `✗ Not on a feature branch. Current branch: ` +- 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 diff --git a/.specify/extensions/git/config-template.yml b/.specify/extensions/git/config-template.yml new file mode 100644 index 0000000..8c414ba --- /dev/null +++ b/.specify/extensions/git/config-template.yml @@ -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" diff --git a/.specify/extensions/git/extension.yml b/.specify/extensions/git/extension.yml new file mode 100644 index 0000000..13c1977 --- /dev/null +++ b/.specify/extensions/git/extension.yml @@ -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" diff --git a/.specify/extensions/git/git-config.yml b/.specify/extensions/git/git-config.yml new file mode 100644 index 0000000..8c414ba --- /dev/null +++ b/.specify/extensions/git/git-config.yml @@ -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" diff --git a/.specify/extensions/git/scripts/bash/auto-commit.sh b/.specify/extensions/git/scripts/bash/auto-commit.sh new file mode 100644 index 0000000..49c32fe --- /dev/null +++ b/.specify/extensions/git/scripts/bash/auto-commit.sh @@ -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 +# e.g.: auto-commit.sh after_specify + +set -e + +EVENT_NAME="${1:-}" +if [ -z "$EVENT_NAME" ]; then + echo "Usage: $0 " >&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..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 "✓ Changes committed ${_phase} ${_command_name}" >&2 diff --git a/.specify/extensions/git/scripts/bash/create-new-feature.sh b/.specify/extensions/git/scripts/bash/create-new-feature.sh new file mode 100644 index 0000000..286aaf7 --- /dev/null +++ b/.specify/extensions/git/scripts/bash/create-new-feature.sh @@ -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 ] [--number N] [--timestamp] " + 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 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 ] [--number N] [--timestamp] " >&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 diff --git a/.specify/extensions/git/scripts/bash/git-common.sh b/.specify/extensions/git/scripts/bash/git-common.sh new file mode 100644 index 0000000..882a385 --- /dev/null +++ b/.specify/extensions/git/scripts/bash/git-common.sh @@ -0,0 +1,41 @@ +#!/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 +} + +# Validate that a branch name matches the expected feature branch pattern. +# Accepts sequential (###-* with >=3 digits) or timestamp (YYYYMMDD-HHMMSS-*) formats. +check_feature_branch() { + local branch="$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 + + # Reject malformed timestamps (7-digit date, 8-digit date without trailing slug, or 7-digit with slug) + if [[ "$branch" =~ ^[0-9]{7}-[0-9]{6} ]] || [[ "$branch" =~ ^[0-9]{8}-[0-9]{6}$ ]]; then + echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 + echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2 + return 1 + fi + + # Accept sequential (>=3 digits followed by hyphen) or timestamp (YYYYMMDD-HHMMSS-*) + if [[ "$branch" =~ ^[0-9]{3,}- ]] || [[ "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then + return 0 + fi + + echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 + echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2 + return 1 +} diff --git a/.specify/extensions/git/scripts/bash/initialize-repo.sh b/.specify/extensions/git/scripts/bash/initialize-repo.sh new file mode 100644 index 0000000..296e363 --- /dev/null +++ b/.specify/extensions/git/scripts/bash/initialize-repo.sh @@ -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 diff --git a/.specify/extensions/git/scripts/powershell/auto-commit.ps1 b/.specify/extensions/git/scripts/powershell/auto-commit.ps1 new file mode 100644 index 0000000..e9777ff --- /dev/null +++ b/.specify/extensions/git/scripts/powershell/auto-commit.ps1 @@ -0,0 +1,149 @@ +#!/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 +# 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 +} + +try { + git rev-parse --is-inside-work-tree 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { throw "not a repo" } +} catch { + 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 +$diffHead = git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE +$diffCached = git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE +$untracked = git ls-files --others --exclude-standard 2>$null + +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 +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 +} + +Write-Host "✓ Changes committed $phase $commandName" diff --git a/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 b/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 new file mode 100644 index 0000000..b579f05 --- /dev/null +++ b/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 @@ -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 ] [-Number N] [-Timestamp] " + 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 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 ] [-Number N] [-Timestamp] " + 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" + } +} diff --git a/.specify/extensions/git/scripts/powershell/git-common.ps1 b/.specify/extensions/git/scripts/powershell/git-common.ps1 new file mode 100644 index 0000000..8a9c4fd --- /dev/null +++ b/.specify/extensions/git/scripts/powershell/git-common.ps1 @@ -0,0 +1,50 @@ +#!/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 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 + } + + # Reject malformed timestamps (7-digit date or no trailing slug) + $hasMalformedTimestamp = ($Branch -match '^[0-9]{7}-[0-9]{6}-') -or + ($Branch -match '^(?:\d{7}|\d{8})-\d{6}$') + if ($hasMalformedTimestamp) { + Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" + Write-Output "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" + return $false + } + + # Accept sequential (>=3 digits followed by hyphen) or timestamp (YYYYMMDD-HHMMSS-*) + $isSequential = ($Branch -match '^[0-9]{3,}-') -and (-not $hasMalformedTimestamp) + $isTimestamp = $Branch -match '^\d{8}-\d{6}-' + + if ($isSequential -or $isTimestamp) { + return $true + } + + Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" + Write-Output "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" + return $false +} diff --git a/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 b/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 new file mode 100644 index 0000000..324240a --- /dev/null +++ b/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 @@ -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" diff --git a/.specify/init-options.json b/.specify/init-options.json new file mode 100644 index 0000000..39aa824 --- /dev/null +++ b/.specify/init-options.json @@ -0,0 +1,9 @@ +{ + "ai": "copilot", + "branch_numbering": "sequential", + "here": true, + "integration": "copilot", + "preset": null, + "script": "ps", + "speckit_version": "0.6.2" +} \ No newline at end of file diff --git a/.specify/integration.json b/.specify/integration.json new file mode 100644 index 0000000..9a17284 --- /dev/null +++ b/.specify/integration.json @@ -0,0 +1,7 @@ +{ + "integration": "copilot", + "version": "0.6.2", + "scripts": { + "update-context": ".specify/integrations/copilot/scripts/update-context.ps1" + } +} diff --git a/.specify/integrations/copilot.manifest.json b/.specify/integrations/copilot.manifest.json new file mode 100644 index 0000000..4d4bea6 --- /dev/null +++ b/.specify/integrations/copilot.manifest.json @@ -0,0 +1,27 @@ +{ + "integration": "copilot", + "version": "0.6.2", + "installed_at": "2026-04-14T16:06:35.572321+00:00", + "files": { + ".github/agents/speckit.analyze.agent.md": "56285817b93f55ae926eade0528673d25ffcf441e8109492323bfbe8941be8f7", + ".github/agents/speckit.checklist.agent.md": "53ce35462c1df402780e730ea54150a30872e6e0d6d44a8bd38dd8b5d42900a9", + ".github/agents/speckit.clarify.agent.md": "97b14836d6174f0ef51d8bbda0b731c87ef0b011aa36ccd4171a2fc3d7e3280a", + ".github/agents/speckit.constitution.agent.md": "58d35eb026f56bb7364d91b8b0382d5dd1249ded6c1449a2b69546693afb85f7", + ".github/agents/speckit.implement.agent.md": "9dda40376414146a8992d09e6853976c75708ba4cad7d9df9fcd6e7faa8f0a4c", + ".github/agents/speckit.plan.agent.md": "82961180ad08d9c5f33003d673996dad0bf73d06d7e7b6f07e967cff0a103a0c", + ".github/agents/speckit.specify.agent.md": "5bbb5270836cc9a3286ce3ed96a500f3d383a54abb06aa11b01a2d2f76dbf39b", + ".github/agents/speckit.tasks.agent.md": "d505523168e1f1c758350da9d45c08ff990ed8686d6bc8451db8b89a145768a4", + ".github/agents/speckit.taskstoissues.agent.md": "12532e50c8f9aeb453a565aec6aa6f53abe53cf92926ca553b3d2c61b6b7b6e3", + ".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", + ".specify/integrations/copilot/scripts/update-context.ps1": "31b3cb0e13a4cbb93932ee0ac5de79436b30f58b1da63f73f8e82ca8050b9ea3", + ".specify/integrations/copilot/scripts/update-context.sh": "89abb5cf143d07d9bd4cb4e4fe2623ec850933674a5c3d2ce2236c65c99d4100" + } +} diff --git a/.specify/integrations/copilot/scripts/update-context.ps1 b/.specify/integrations/copilot/scripts/update-context.ps1 new file mode 100644 index 0000000..26e746a --- /dev/null +++ b/.specify/integrations/copilot/scripts/update-context.ps1 @@ -0,0 +1,32 @@ +# update-context.ps1 — Copilot integration: create/update .github/copilot-instructions.md +# +# This is the copilot-specific implementation that produces the GitHub +# Copilot instructions file. The shared dispatcher reads +# .specify/integration.json and calls this script. +# +# NOTE: This script is not yet active. It will be activated in Stage 7 +# when the shared update-agent-context.ps1 replaces its switch statement +# with integration.json-based dispatch. The shared script must also be +# refactored to support SPECKIT_SOURCE_ONLY (guard the Main call) before +# dot-sourcing will work. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +# Invoke shared update-agent-context script as a separate process. +# Dot-sourcing is unsafe until that script guards its Main call. +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType copilot diff --git a/.specify/integrations/copilot/scripts/update-context.sh b/.specify/integrations/copilot/scripts/update-context.sh new file mode 100644 index 0000000..c7f3bc6 --- /dev/null +++ b/.specify/integrations/copilot/scripts/update-context.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# update-context.sh — Copilot integration: create/update .github/copilot-instructions.md +# +# This is the copilot-specific implementation that produces the GitHub +# Copilot instructions file. The shared dispatcher reads +# .specify/integration.json and calls this script. +# +# NOTE: This script is not yet active. It will be activated in Stage 7 +# when the shared update-agent-context.sh replaces its case statement +# with integration.json-based dispatch. The shared script must also be +# refactored to support SPECKIT_SOURCE_ONLY (guard the main logic) +# before sourcing will work. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +# Invoke shared update-agent-context script as a separate process. +# Sourcing is unsafe until that script guards its main logic. +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" copilot diff --git a/.specify/integrations/speckit.manifest.json b/.specify/integrations/speckit.manifest.json new file mode 100644 index 0000000..3863a67 --- /dev/null +++ b/.specify/integrations/speckit.manifest.json @@ -0,0 +1,6 @@ +{ + "integration": "speckit", + "version": "0.6.2", + "installed_at": "2026-04-14T16:06:35.580029+00:00", + "files": {} +}