From fe613d1b2c066b1538ac121c507e85424c136b5a Mon Sep 17 00:00:00 2001 From: Jonathan Lameira Date: Sat, 28 Mar 2026 13:13:13 -0300 Subject: [PATCH 1/4] feat: add approval gates feature - Configuration loader for .speckit/approval-gates.yaml - CLI command: specify approval - Unit tests and YAML template - Enables approval enforcement between workflow phases --- docs/approval-gates-guide.md | 152 ++++++++++++++++++++++++++++ docs/examples/approval-gates.yaml | 35 +++++++ src/specify_cli/__init__.py | 12 +++ src/specify_cli/approval_command.py | 43 ++++++++ src/specify_cli/approval_gates.py | 45 ++++++++ tests/test_approval_command.py | 59 +++++++++++ tests/test_approval_gates.py | 69 +++++++++++++ 7 files changed, 415 insertions(+) create mode 100644 docs/approval-gates-guide.md create mode 100644 docs/examples/approval-gates.yaml create mode 100644 src/specify_cli/approval_command.py create mode 100644 src/specify_cli/approval_gates.py create mode 100644 tests/test_approval_command.py create mode 100644 tests/test_approval_gates.py diff --git a/docs/approval-gates-guide.md b/docs/approval-gates-guide.md new file mode 100644 index 0000000000..87967648f8 --- /dev/null +++ b/docs/approval-gates-guide.md @@ -0,0 +1,152 @@ +# Approval Gates + +Enforce approval requirements between workflow phases to prevent "spec-less coding". + +## Quick Start + +### 1. Create Configuration + +```bash +mkdir -p .speckit +cat > .speckit/approval-gates.yaml << 'EOF' +approval_gates: + specify: + enabled: true + requires: [product_lead, architect] + min_approvals: 1 + description: "Functional spec approval" + + plan: + enabled: true + requires: [architect, tech_lead] + min_approvals: 2 + description: "Technical spec approval" + + tasks: + enabled: true + requires: [tech_lead] + min_approvals: 1 + description: "Task breakdown approval" + + implement: + enabled: false +EOF +``` + +### 2. Check Status + +```bash +specify approval +``` + +Expected output: +``` +✅ Approval gates enabled + + specify + • Enabled: ✅ + • Min approvals: 1 + plan + • Enabled: ✅ + • Min approvals: 2 + tasks + • Enabled: ✅ + • Min approvals: 1 + implement: disabled +``` + +## Configuration + +Edit `.speckit/approval-gates.yaml` to: +- **enabled**: true/false - Enable/disable this gate +- **requires**: [role1, role2] - Who can approve +- **min_approvals**: number - How many approvals needed +- **description**: string - What this gate is for + +### Available Phases + +- `constitution` — Project fundamentals +- `specify` — Functional specifications +- `plan` — Technical specifications +- `tasks` — Task breakdown +- `implement` — Implementation (optional) + +## Why Use Approval Gates? + +✅ **Prevents spec-less coding** — Requires approval before moving phases +✅ **Ensures alignment** — Teams must agree before proceeding +✅ **Creates clarity** — Clear approval requirements for each phase + +## Commands + +```bash +# Check gate status +specify approval + +# Explicitly request status +specify approval --action status +specify approval -a status + +# Show help +specify approval --help +``` + +## Examples + +### Basic Setup (All Phases) +```yaml +approval_gates: + specify: + enabled: true + min_approvals: 1 + plan: + enabled: true + min_approvals: 2 + tasks: + enabled: true + min_approvals: 1 +``` + +### Minimal Setup (Only Specify) +```yaml +approval_gates: + specify: + enabled: true + min_approvals: 1 +``` + +### Strict Setup (High Approval Requirements) +```yaml +approval_gates: + constitution: + enabled: true + requires: [owner] + min_approvals: 1 + specify: + enabled: true + requires: [product_lead, architect] + min_approvals: 2 + plan: + enabled: true + requires: [architect, tech_lead, security_lead] + min_approvals: 3 +``` + +## Troubleshooting + +### Command not found +```bash +# Make sure you're in the spec-kit project +cd ~/Documents/Projects/spec-kit +uv run specify approval +``` + +### No approval gates configured +Create `.speckit/approval-gates.yaml` in your project root. + +### YAML errors +Check YAML indentation — spaces matter! Use a YAML validator if unsure. + +--- + +**Template:** See `docs/examples/approval-gates.yaml` for a full example. \ No newline at end of file diff --git a/docs/examples/approval-gates.yaml b/docs/examples/approval-gates.yaml new file mode 100644 index 0000000000..0ff8edf575 --- /dev/null +++ b/docs/examples/approval-gates.yaml @@ -0,0 +1,35 @@ +# .speckit/approval-gates.yaml +# Copy this file to your project root: .speckit/approval-gates.yaml +# Then customize for your team's needs + +approval_gates: + constitution: + enabled: false + requires: [owner] + min_approvals: 1 + + specify: + enabled: true + requires: [product_lead, architect] + min_approvals: 1 + description: "Functional spec approval" + + plan: + enabled: true + requires: [architect, tech_lead] + min_approvals: 2 + description: "Technical spec approval" + + tasks: + enabled: true + requires: [tech_lead] + min_approvals: 1 + description: "Task breakdown approval" + + implement: + enabled: false + description: "Implementation gate (optional)" + +github_actions: + # Future: GitHub Actions integration + enabled: false diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index f528535a61..64e6bca3ce 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -4571,6 +4571,18 @@ def extension_set_priority( console.print("\n[dim]Lower priority = higher precedence in template resolution[/dim]") +@app.command() +def approval( + action: str = typer.Option("status", "--action", "-a", help="Approval action"), +): + """Check approval gates status (if configured). + + If no .speckit/approval-gates.yaml exists, shows setup instructions. + """ + from .approval_command import approval_command + approval_command(action=action) + + def main(): app() diff --git a/src/specify_cli/approval_command.py b/src/specify_cli/approval_command.py new file mode 100644 index 0000000000..d7f99a46df --- /dev/null +++ b/src/specify_cli/approval_command.py @@ -0,0 +1,43 @@ +"""Approval gate command + +Provides 'specify approval' command using Typer framework. +""" + +import typer +from rich.console import Console +from specify_cli.approval_gates import ApprovalGatesConfig + +console = Console() + + +def approval_command( + action: str = typer.Option("status", "--action", "-a", help="Approval action"), +): + """Check approval gates status (if configured). + + If no .speckit/approval-gates.yaml exists, returns helpful message. + + Example: + specify approval + """ + + config = ApprovalGatesConfig.load() + + if config is None: + console.print("ℹ️ No approval gates configured") + console.print(" Create .speckit/approval-gates.yaml to enable") + console.print("") + console.print(" See: docs/approval-gates-guide.md for setup") + return + + if action == "status": + console.print("✅ Approval gates enabled") + console.print("") + for phase, gate in config.gates.items(): + if gate.get("enabled"): + min_approvals = gate.get("min_approvals", 1) + console.print(f" {phase}") + console.print(f" • Enabled: ✅") + console.print(f" • Min approvals: {min_approvals}") + else: + console.print(f" {phase}: disabled") \ No newline at end of file diff --git a/src/specify_cli/approval_gates.py b/src/specify_cli/approval_gates.py new file mode 100644 index 0000000000..b8281658fb --- /dev/null +++ b/src/specify_cli/approval_gates.py @@ -0,0 +1,45 @@ +"""Approval Gates Configuration Handler + +Loads and validates .speckit/approval-gates.yaml from user projects. +""" + +from pathlib import Path +from typing import Optional, Dict +import yaml + + +class ApprovalGatesConfig: + """Load and validate approval gates from .speckit/approval-gates.yaml""" + + CONFIG_FILE = Path(".speckit/approval-gates.yaml") + + @classmethod + def load(cls) -> Optional['ApprovalGatesConfig']: + """Load approval gates config if it exists in user's project + + Returns None if no approval gates configured. + """ + if not cls.CONFIG_FILE.exists(): + return None # No approval gates configured - this is OK + + with open(cls.CONFIG_FILE) as f: + data = yaml.safe_load(f) + + if data is None: + return None + + return cls(data) + + def __init__(self, config: Dict): + self.gates = config.get("approval_gates", {}) + + def is_phase_gated(self, phase: str) -> bool: + """Check if a phase requires approval""" + gate = self.gates.get(phase, {}) + return gate.get("enabled", False) + + def get_phase_gate(self, phase: str) -> Optional[Dict]: + """Get gate configuration for a specific phase""" + if self.is_phase_gated(phase): + return self.gates.get(phase) + return None \ No newline at end of file diff --git a/tests/test_approval_command.py b/tests/test_approval_command.py new file mode 100644 index 0000000000..0915fc0f66 --- /dev/null +++ b/tests/test_approval_command.py @@ -0,0 +1,59 @@ +"""Tests for approval CLI command""" + +import pytest +from typer.testing import CliRunner +from unittest.mock import patch, MagicMock +from specify_cli.approval_command import approval_command +import typer + + +def test_approval_status_no_config(): + """Test approval status when no config exists""" + with patch("specify_cli.approval_command.ApprovalGatesConfig.load", return_value=None): + # Create a simple typer app to test the command + app = typer.Typer() + app.command()(approval_command) + + runner = CliRunner() + result = runner.invoke(app, ["--action", "status"]) + assert result.exit_code == 0 + assert "No approval gates configured" in result.stdout + + +def test_approval_status_with_config(): + """Test approval status with gates configured""" + # Mock configuration + mock_config = MagicMock() + mock_config.gates = { + "specify": {"enabled": True, "min_approvals": 1}, + "plan": {"enabled": True, "min_approvals": 2}, + } + + with patch("specify_cli.approval_command.ApprovalGatesConfig.load", return_value=mock_config): + app = typer.Typer() + app.command()(approval_command) + + runner = CliRunner() + result = runner.invoke(app, ["--action", "status"]) + assert result.exit_code == 0 + assert "Approval gates enabled" in result.stdout + assert "specify" in result.stdout + assert "plan" in result.stdout + + +def test_approval_default_action(): + """Test approval command with default action (status)""" + mock_config = MagicMock() + mock_config.gates = { + "specify": {"enabled": True, "min_approvals": 1}, + } + + with patch("specify_cli.approval_command.ApprovalGatesConfig.load", return_value=mock_config): + app = typer.Typer() + app.command()(approval_command) + + runner = CliRunner() + # Invoke without --action (should default to status) + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "Approval gates enabled" in result.stdout diff --git a/tests/test_approval_gates.py b/tests/test_approval_gates.py new file mode 100644 index 0000000000..4e4af5eda1 --- /dev/null +++ b/tests/test_approval_gates.py @@ -0,0 +1,69 @@ +"""Tests for approval gates functionality""" + +import pytest +from pathlib import Path +from unittest.mock import patch, mock_open +from specify_cli.approval_gates import ApprovalGatesConfig + + +def test_approval_gates_load_exists(): + """Test loading approval gates when config exists""" + yaml_content = """ +approval_gates: + specify: + enabled: true + min_approvals: 1 + plan: + enabled: true + min_approvals: 2 +""" + with patch("builtins.open", mock_open(read_data=yaml_content)): + with patch("pathlib.Path.exists", return_value=True): + config = ApprovalGatesConfig.load() + assert config is not None + assert "specify" in config.gates + + +def test_approval_gates_load_not_exists(): + """Test loading when no approval gates configured""" + with patch("pathlib.Path.exists", return_value=False): + config = ApprovalGatesConfig.load() + assert config is None # This is expected behavior + + +def test_is_phase_gated_enabled(): + """Test checking if phase is gated (enabled)""" + config_data = { + "approval_gates": { + "specify": {"enabled": True, "min_approvals": 1}, + "plan": {"enabled": False}, + } + } + config = ApprovalGatesConfig(config_data) + assert config.is_phase_gated("specify") == True + assert config.is_phase_gated("plan") == False + + +def test_get_phase_gate(): + """Test retrieving gate configuration for phase""" + config_data = { + "approval_gates": { + "specify": {"enabled": True, "min_approvals": 1}, + } + } + config = ApprovalGatesConfig(config_data) + gate = config.get_phase_gate("specify") + assert gate is not None + assert gate.get("min_approvals") == 1 + + +def test_get_phase_gate_disabled(): + """Test getting gate for disabled phase""" + config_data = { + "approval_gates": { + "plan": {"enabled": False}, + } + } + config = ApprovalGatesConfig(config_data) + gate = config.get_phase_gate("plan") + assert gate is None # Returns None if not enabled From 866b3f13d9d38c404a87444fabaab22b8e8bb9b9 Mon Sep 17 00:00:00 2001 From: Jonathan Lameira Date: Sat, 28 Mar 2026 13:21:54 -0300 Subject: [PATCH 2/4] feat: change path on docs --- docs/approval-gates-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/approval-gates-guide.md b/docs/approval-gates-guide.md index 87967648f8..90d438af28 100644 --- a/docs/approval-gates-guide.md +++ b/docs/approval-gates-guide.md @@ -137,7 +137,7 @@ approval_gates: ### Command not found ```bash # Make sure you're in the spec-kit project -cd ~/Documents/Projects/spec-kit +cd ~/spec-kit uv run specify approval ``` From fc8cfa2b858369765d25553f75bdac0ba2ea0ff0 Mon Sep 17 00:00:00 2001 From: Jonathan Lameira Date: Sun, 5 Apr 2026 14:12:35 -0300 Subject: [PATCH 3/4] feat(extensions): add approval-gates extension Add Approval Gates extension to enforce approval requirements between spec-driven development phases. Features: - Command: /speckit.approval-gates.status - display gate configuration - Configuration: per-phase approval requirements (roles, min approvals) - Hook integration: after_tasks phase with optional prompt - Config template: approval-gates-config.template.yml for customization Extension includes: - extension.yml manifest with command and config definitions - commands/status.md with Markdown-based implementation - approval-gates-config.template.yml for project customization - README.md with installation and configuration guide - CHANGELOG.md with version history --- docs/approval-gates-guide.md | 152 ---------------- docs/examples/approval-gates.yaml | 35 ---- extensions/approval-gates/CHANGELOG.md | 23 +++ extensions/approval-gates/LICENSE | 21 +++ extensions/approval-gates/README.md | 159 +++++++++++++++++ .../approval-gates-config.template.yml | 42 +++++ extensions/approval-gates/commands/status.md | 164 ++++++++++++++++++ extensions/approval-gates/extension.yml | 40 +++++ src/specify_cli/__init__.py | 12 -- src/specify_cli/approval_command.py | 43 ----- src/specify_cli/approval_gates.py | 45 ----- tests/test_approval_command.py | 59 ------- tests/test_approval_gates.py | 69 -------- 13 files changed, 449 insertions(+), 415 deletions(-) delete mode 100644 docs/approval-gates-guide.md delete mode 100644 docs/examples/approval-gates.yaml create mode 100644 extensions/approval-gates/CHANGELOG.md create mode 100644 extensions/approval-gates/LICENSE create mode 100644 extensions/approval-gates/README.md create mode 100644 extensions/approval-gates/approval-gates-config.template.yml create mode 100644 extensions/approval-gates/commands/status.md create mode 100644 extensions/approval-gates/extension.yml delete mode 100644 src/specify_cli/approval_command.py delete mode 100644 src/specify_cli/approval_gates.py delete mode 100644 tests/test_approval_command.py delete mode 100644 tests/test_approval_gates.py diff --git a/docs/approval-gates-guide.md b/docs/approval-gates-guide.md deleted file mode 100644 index 90d438af28..0000000000 --- a/docs/approval-gates-guide.md +++ /dev/null @@ -1,152 +0,0 @@ -# Approval Gates - -Enforce approval requirements between workflow phases to prevent "spec-less coding". - -## Quick Start - -### 1. Create Configuration - -```bash -mkdir -p .speckit -cat > .speckit/approval-gates.yaml << 'EOF' -approval_gates: - specify: - enabled: true - requires: [product_lead, architect] - min_approvals: 1 - description: "Functional spec approval" - - plan: - enabled: true - requires: [architect, tech_lead] - min_approvals: 2 - description: "Technical spec approval" - - tasks: - enabled: true - requires: [tech_lead] - min_approvals: 1 - description: "Task breakdown approval" - - implement: - enabled: false -EOF -``` - -### 2. Check Status - -```bash -specify approval -``` - -Expected output: -``` -✅ Approval gates enabled - - specify - • Enabled: ✅ - • Min approvals: 1 - plan - • Enabled: ✅ - • Min approvals: 2 - tasks - • Enabled: ✅ - • Min approvals: 1 - implement: disabled -``` - -## Configuration - -Edit `.speckit/approval-gates.yaml` to: -- **enabled**: true/false - Enable/disable this gate -- **requires**: [role1, role2] - Who can approve -- **min_approvals**: number - How many approvals needed -- **description**: string - What this gate is for - -### Available Phases - -- `constitution` — Project fundamentals -- `specify` — Functional specifications -- `plan` — Technical specifications -- `tasks` — Task breakdown -- `implement` — Implementation (optional) - -## Why Use Approval Gates? - -✅ **Prevents spec-less coding** — Requires approval before moving phases -✅ **Ensures alignment** — Teams must agree before proceeding -✅ **Creates clarity** — Clear approval requirements for each phase - -## Commands - -```bash -# Check gate status -specify approval - -# Explicitly request status -specify approval --action status -specify approval -a status - -# Show help -specify approval --help -``` - -## Examples - -### Basic Setup (All Phases) -```yaml -approval_gates: - specify: - enabled: true - min_approvals: 1 - plan: - enabled: true - min_approvals: 2 - tasks: - enabled: true - min_approvals: 1 -``` - -### Minimal Setup (Only Specify) -```yaml -approval_gates: - specify: - enabled: true - min_approvals: 1 -``` - -### Strict Setup (High Approval Requirements) -```yaml -approval_gates: - constitution: - enabled: true - requires: [owner] - min_approvals: 1 - specify: - enabled: true - requires: [product_lead, architect] - min_approvals: 2 - plan: - enabled: true - requires: [architect, tech_lead, security_lead] - min_approvals: 3 -``` - -## Troubleshooting - -### Command not found -```bash -# Make sure you're in the spec-kit project -cd ~/spec-kit -uv run specify approval -``` - -### No approval gates configured -Create `.speckit/approval-gates.yaml` in your project root. - -### YAML errors -Check YAML indentation — spaces matter! Use a YAML validator if unsure. - ---- - -**Template:** See `docs/examples/approval-gates.yaml` for a full example. \ No newline at end of file diff --git a/docs/examples/approval-gates.yaml b/docs/examples/approval-gates.yaml deleted file mode 100644 index 0ff8edf575..0000000000 --- a/docs/examples/approval-gates.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# .speckit/approval-gates.yaml -# Copy this file to your project root: .speckit/approval-gates.yaml -# Then customize for your team's needs - -approval_gates: - constitution: - enabled: false - requires: [owner] - min_approvals: 1 - - specify: - enabled: true - requires: [product_lead, architect] - min_approvals: 1 - description: "Functional spec approval" - - plan: - enabled: true - requires: [architect, tech_lead] - min_approvals: 2 - description: "Technical spec approval" - - tasks: - enabled: true - requires: [tech_lead] - min_approvals: 1 - description: "Task breakdown approval" - - implement: - enabled: false - description: "Implementation gate (optional)" - -github_actions: - # Future: GitHub Actions integration - enabled: false diff --git a/extensions/approval-gates/CHANGELOG.md b/extensions/approval-gates/CHANGELOG.md new file mode 100644 index 0000000000..b053215fe3 --- /dev/null +++ b/extensions/approval-gates/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable changes to the Approval Gates extension are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-04-05 + +### Added + +- Initial release of Approval Gates extension +- `speckit.approval-gates.status` command to display approval gates configuration +- Support for per-phase approval requirements (specify, plan, tasks, implement, constitution) +- Configurable approval requirements: + - `enabled` — Toggle approval requirement for a phase + - `min_approvals` — Minimum number of approvals needed + - `requires` — List of roles who can approve + - `description` — Description of what the gate enforces +- `approval-gates-config.template.yml` template for team customization +- Hook integration: After `/speckit.tasks`, optional prompt to check approval gates +- Comprehensive documentation with configuration guide and examples +- Support for optional configuration (teams can use extension without configuring gates) diff --git a/extensions/approval-gates/LICENSE b/extensions/approval-gates/LICENSE new file mode 100644 index 0000000000..7d30d37a7e --- /dev/null +++ b/extensions/approval-gates/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/approval-gates/README.md b/extensions/approval-gates/README.md new file mode 100644 index 0000000000..7033403ac3 --- /dev/null +++ b/extensions/approval-gates/README.md @@ -0,0 +1,159 @@ +# Approval Gates Extension + +Extension to enforce approval requirements between spec-driven development phases. + +## Installation + +### 1. Install the Extension + +```bash +specify extension add --dev extensions/approval-gates +``` + +### 2. Create Configuration + +Copy the template to your project: + +```bash +mkdir -p .specify/extensions/approval-gates +cp extensions/approval-gates/approval-gates-config.template.yml \ + .specify/extensions/approval-gates/approval-gates-config.yml +``` + +### 3. Customize for Your Team + +Edit `.specify/extensions/approval-gates/approval-gates-config.yml` and set: + +- Which phases require approval +- How many approvals are needed +- Who can approve each phase +- Descriptions for each gate + +## Configuration + +### Schema + +```yaml +specify: + enabled: bool # Enable/disable this gate + requires: [role1, role2, ...] # Who can approve + min_approvals: int # How many approvals required + description: string # (optional) What this gate enforces +``` + +### Example + +```yaml +specify: + enabled: true + requires: [product_lead, architect] + min_approvals: 1 + description: "Functional spec approval" + +plan: + enabled: true + requires: [architect, tech_lead] + min_approvals: 2 + description: "Technical plan approval" + +tasks: + enabled: false +``` + +## Usage + +### Check Approval Gates Status + +```bash +> /speckit.approval-gates.status +``` + +Shows which phases are gated and their approval requirements: + +``` +✅ Approval gates enabled + + 📋 specify + • Status: ✅ ENFORCED + • Min approvals: 1 + • Description: Functional spec approval + + 📋 plan + • Status: ✅ ENFORCED + • Min approvals: 2 + • Description: Technical spec approval +``` + +### After Tasks Phase + +The extension integrates with the workflow: + +```bash +> /speckit.tasks +# ... task generation ... +# Prompt appears: +# ❓ Check approval gates for next phase? +> Y +``` + +## Phases + +Approval gates can be configured for the following phases: + +- `constitution` — Project setup and context +- `specify` — Functional specification +- `plan` — Technical specification and architecture +- `tasks` — Task breakdown and planning +- `implement` — Implementation phase (optional) + +## Troubleshooting + +### Command Not Found + +``` +❌ Command not found: speckit.approval-gates.status +``` + +**Solution**: Reinstall the extension: + +```bash +specify extension remove approval-gates +specify extension add --dev extensions/approval-gates +``` + +### Config Not Loading + +``` +ℹ️ No approval gates configured +``` + +**Solution**: Ensure the config file exists: + +```bash +ls .specify/extensions/approval-gates/approval-gates-config.yml +# If missing, create it from template +``` + +### YAML Parse Error + +``` +❌ Error parsing approval-gates-config.yml +``` + +**Solution**: Validate YAML syntax: + +```bash +yq eval '.' .specify/extensions/approval-gates/approval-gates-config.yml +``` + +Check for: +- Proper indentation (2 spaces) +- Quotes around strings +- No trailing colons + +## Related Commands + +- `/speckit.constitution` — Project setup +- `/speckit.specify` — Create specification +- `/speckit.plan` — Create plan +- `/speckit.tasks` — Generate tasks diff --git a/extensions/approval-gates/approval-gates-config.template.yml b/extensions/approval-gates/approval-gates-config.template.yml new file mode 100644 index 0000000000..874e524918 --- /dev/null +++ b/extensions/approval-gates/approval-gates-config.template.yml @@ -0,0 +1,42 @@ +# Approval Gates Configuration +# Copy this file to .specify/extensions/approval-gates/approval-gates-config.yml +# Then customize for your team's approval workflow + +# Define approval requirements for each phase +# Each phase can be enabled/disabled independently + +specify: + enabled: true + requires: [product_lead, architect] + min_approvals: 1 + description: "Functional specification approval" + +plan: + enabled: true + requires: [architect, tech_lead] + min_approvals: 2 + description: "Technical specification and architecture approval" + +tasks: + enabled: true + requires: [tech_lead] + min_approvals: 1 + description: "Task breakdown and planning approval" + +implement: + enabled: false + requires: [tech_lead] + min_approvals: 1 + description: "Implementation gate (optional)" + +constitution: + enabled: false + requires: [owner] + min_approvals: 1 + description: "Project constitution approval" + +# Future: GitHub Actions integration for automated checks +github_actions: + enabled: false + # repository: "your-org/your-repo" + # branch: "main" diff --git a/extensions/approval-gates/commands/status.md b/extensions/approval-gates/commands/status.md new file mode 100644 index 0000000000..5a1173f8e4 --- /dev/null +++ b/extensions/approval-gates/commands/status.md @@ -0,0 +1,164 @@ +--- +description: "Show approval gates configuration and enforcement status" +--- + +# Approval Gates Status + +Display the current approval gates configuration. + +## Steps + +### Step 1: Check if Approval Gates are Configured + +```bash +config_file=".specify/extensions/approval-gates/approval-gates-config.yml" + +if [ ! -f "$config_file" ]; then + echo "ℹ️ No approval gates configured" + echo "" + echo "To enable approval gates:" + echo " 1. Create .specify/extensions/approval-gates/ directory" + echo " 2. Copy the template: approval-gates-config.template.yml" + echo " 3. Customize for your team's workflow" + echo "" + echo "See: extensions/approval-gates/README.md for setup instructions" + exit 0 +fi +``` + +### Step 2: Display Approval Gates Status + +```bash +echo "✅ Approval gates enabled" +echo "" + +# Parse YAML and display each phase +phases=$(yq eval 'keys[]' "$config_file" 2>/dev/null || echo "") + +if [ -z "$phases" ]; then + echo "⚠️ No phases configured in approval-gates-config.yml" + exit 0 +fi + +for phase in $phases; do + enabled=$(yq eval ".${phase}.enabled" "$config_file" 2>/dev/null) + + if [ "$enabled" = "true" ]; then + min_approvals=$(yq eval ".${phase}.min_approvals // 1" "$config_file" 2>/dev/null) + requires=$(yq eval ".${phase}.requires // []" "$config_file" 2>/dev/null) + description=$(yq eval ".${phase}.description" "$config_file" 2>/dev/null) + + echo " 📋 $phase" + echo " • Status: ✅ ENFORCED" + echo " • Min approvals: $min_approvals" + + if [ -n "$description" ] && [ "$description" != "null" ]; then + echo " • Description: $description" + fi + + echo "" + else + echo " ⊘ $phase: disabled" + echo "" + fi +done +``` + +## Configuration + +This command reads from `.specify/extensions/approval-gates/approval-gates-config.yml`. + +### Example Configuration + +```yaml +specify: + enabled: true + requires: [product_lead, architect] + min_approvals: 1 + description: "Functional spec approval" + +plan: + enabled: true + requires: [architect, tech_lead] + min_approvals: 2 + description: "Technical spec approval" + +tasks: + enabled: true + requires: [tech_lead] + min_approvals: 1 + description: "Task breakdown approval" + +implement: + enabled: false +``` + +### Configuration Fields + +- **enabled** (boolean): Whether this phase requires approval + - `true`: Approval is required + - `false`: Phase can proceed without approval + +- **min_approvals** (number): Minimum approvals needed from the required roles + - Example: `1` (at least one person from `requires` must approve) + +- **requires** (array): List of roles who can approve + - Example: `[product_lead, architect, tech_lead]` + +- **description** (string, optional): What this gate enforces + - Example: "Technical spec approval" + +## Examples + +### Check Current Gates + +```bash +> /speckit.approval-gates.status +``` + +Output: +``` +✅ Approval gates enabled + + 📋 specify + • Status: ✅ ENFORCED + • Min approvals: 1 + • Description: Functional spec approval + + 📋 plan + • Status: ✅ ENFORCED + • Min approvals: 2 + • Description: Technical spec approval + + 📋 tasks + • Status: ✅ ENFORCED + • Min approvals: 1 + • Description: Task breakdown approval + + ⊘ implement: disabled +``` + +### No Configuration + +```bash +> /speckit.approval-gates.status +``` + +Output: +``` +ℹ️ No approval gates configured + +To enable approval gates: + 1. Create .specify/extensions/approval-gates/ directory + 2. Copy the template: approval-gates-config.template.yml + 3. Customize for your team's workflow + +See: extensions/approval-gates/README.md for setup instructions +``` + +## Related Commands + +- `/speckit.specify` — Create functional specification +- `/speckit.plan` — Create technical specification and task breakdown +- `/speckit.tasks` — Generate implementation tasks + diff --git a/extensions/approval-gates/extension.yml b/extensions/approval-gates/extension.yml new file mode 100644 index 0000000000..812400ce74 --- /dev/null +++ b/extensions/approval-gates/extension.yml @@ -0,0 +1,40 @@ +schema_version: "1.0" + +extension: + id: "approval-gates" + name: "Approval Gates" + version: "1.0.0" + description: "Enforce approval requirements between spec-driven development phases" + author: "Spec Kit Team" + repository: "https://github.com/github/spec-kit/tree/main/extensions/approval-gates" + license: "MIT" + homepage: "https://github.com/github/spec-kit/tree/main/extensions/approval-gates" + +requires: + speckit_version: ">=0.1.0" + +provides: + commands: + - name: "speckit.approval-gates.status" + file: "commands/status.md" + description: "Show approval gates configuration and enforcement status" + aliases: ["speckit.approval-gates.check"] + + config: + - name: "approval-gates-config.yml" + template: "approval-gates-config.template.yml" + description: "Approval gates configuration for this project" + required: false + +hooks: + after_tasks: + command: "speckit.approval-gates.status" + optional: true + prompt: "Check approval gates for next phase?" + description: "Show approval requirements after task breakdown" + +tags: + - "workflow" + - "governance" + - "approval" + - "quality-gates" diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 64e6bca3ce..f528535a61 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -4571,18 +4571,6 @@ def extension_set_priority( console.print("\n[dim]Lower priority = higher precedence in template resolution[/dim]") -@app.command() -def approval( - action: str = typer.Option("status", "--action", "-a", help="Approval action"), -): - """Check approval gates status (if configured). - - If no .speckit/approval-gates.yaml exists, shows setup instructions. - """ - from .approval_command import approval_command - approval_command(action=action) - - def main(): app() diff --git a/src/specify_cli/approval_command.py b/src/specify_cli/approval_command.py deleted file mode 100644 index d7f99a46df..0000000000 --- a/src/specify_cli/approval_command.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Approval gate command - -Provides 'specify approval' command using Typer framework. -""" - -import typer -from rich.console import Console -from specify_cli.approval_gates import ApprovalGatesConfig - -console = Console() - - -def approval_command( - action: str = typer.Option("status", "--action", "-a", help="Approval action"), -): - """Check approval gates status (if configured). - - If no .speckit/approval-gates.yaml exists, returns helpful message. - - Example: - specify approval - """ - - config = ApprovalGatesConfig.load() - - if config is None: - console.print("ℹ️ No approval gates configured") - console.print(" Create .speckit/approval-gates.yaml to enable") - console.print("") - console.print(" See: docs/approval-gates-guide.md for setup") - return - - if action == "status": - console.print("✅ Approval gates enabled") - console.print("") - for phase, gate in config.gates.items(): - if gate.get("enabled"): - min_approvals = gate.get("min_approvals", 1) - console.print(f" {phase}") - console.print(f" • Enabled: ✅") - console.print(f" • Min approvals: {min_approvals}") - else: - console.print(f" {phase}: disabled") \ No newline at end of file diff --git a/src/specify_cli/approval_gates.py b/src/specify_cli/approval_gates.py deleted file mode 100644 index b8281658fb..0000000000 --- a/src/specify_cli/approval_gates.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Approval Gates Configuration Handler - -Loads and validates .speckit/approval-gates.yaml from user projects. -""" - -from pathlib import Path -from typing import Optional, Dict -import yaml - - -class ApprovalGatesConfig: - """Load and validate approval gates from .speckit/approval-gates.yaml""" - - CONFIG_FILE = Path(".speckit/approval-gates.yaml") - - @classmethod - def load(cls) -> Optional['ApprovalGatesConfig']: - """Load approval gates config if it exists in user's project - - Returns None if no approval gates configured. - """ - if not cls.CONFIG_FILE.exists(): - return None # No approval gates configured - this is OK - - with open(cls.CONFIG_FILE) as f: - data = yaml.safe_load(f) - - if data is None: - return None - - return cls(data) - - def __init__(self, config: Dict): - self.gates = config.get("approval_gates", {}) - - def is_phase_gated(self, phase: str) -> bool: - """Check if a phase requires approval""" - gate = self.gates.get(phase, {}) - return gate.get("enabled", False) - - def get_phase_gate(self, phase: str) -> Optional[Dict]: - """Get gate configuration for a specific phase""" - if self.is_phase_gated(phase): - return self.gates.get(phase) - return None \ No newline at end of file diff --git a/tests/test_approval_command.py b/tests/test_approval_command.py deleted file mode 100644 index 0915fc0f66..0000000000 --- a/tests/test_approval_command.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Tests for approval CLI command""" - -import pytest -from typer.testing import CliRunner -from unittest.mock import patch, MagicMock -from specify_cli.approval_command import approval_command -import typer - - -def test_approval_status_no_config(): - """Test approval status when no config exists""" - with patch("specify_cli.approval_command.ApprovalGatesConfig.load", return_value=None): - # Create a simple typer app to test the command - app = typer.Typer() - app.command()(approval_command) - - runner = CliRunner() - result = runner.invoke(app, ["--action", "status"]) - assert result.exit_code == 0 - assert "No approval gates configured" in result.stdout - - -def test_approval_status_with_config(): - """Test approval status with gates configured""" - # Mock configuration - mock_config = MagicMock() - mock_config.gates = { - "specify": {"enabled": True, "min_approvals": 1}, - "plan": {"enabled": True, "min_approvals": 2}, - } - - with patch("specify_cli.approval_command.ApprovalGatesConfig.load", return_value=mock_config): - app = typer.Typer() - app.command()(approval_command) - - runner = CliRunner() - result = runner.invoke(app, ["--action", "status"]) - assert result.exit_code == 0 - assert "Approval gates enabled" in result.stdout - assert "specify" in result.stdout - assert "plan" in result.stdout - - -def test_approval_default_action(): - """Test approval command with default action (status)""" - mock_config = MagicMock() - mock_config.gates = { - "specify": {"enabled": True, "min_approvals": 1}, - } - - with patch("specify_cli.approval_command.ApprovalGatesConfig.load", return_value=mock_config): - app = typer.Typer() - app.command()(approval_command) - - runner = CliRunner() - # Invoke without --action (should default to status) - result = runner.invoke(app, []) - assert result.exit_code == 0 - assert "Approval gates enabled" in result.stdout diff --git a/tests/test_approval_gates.py b/tests/test_approval_gates.py deleted file mode 100644 index 4e4af5eda1..0000000000 --- a/tests/test_approval_gates.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Tests for approval gates functionality""" - -import pytest -from pathlib import Path -from unittest.mock import patch, mock_open -from specify_cli.approval_gates import ApprovalGatesConfig - - -def test_approval_gates_load_exists(): - """Test loading approval gates when config exists""" - yaml_content = """ -approval_gates: - specify: - enabled: true - min_approvals: 1 - plan: - enabled: true - min_approvals: 2 -""" - with patch("builtins.open", mock_open(read_data=yaml_content)): - with patch("pathlib.Path.exists", return_value=True): - config = ApprovalGatesConfig.load() - assert config is not None - assert "specify" in config.gates - - -def test_approval_gates_load_not_exists(): - """Test loading when no approval gates configured""" - with patch("pathlib.Path.exists", return_value=False): - config = ApprovalGatesConfig.load() - assert config is None # This is expected behavior - - -def test_is_phase_gated_enabled(): - """Test checking if phase is gated (enabled)""" - config_data = { - "approval_gates": { - "specify": {"enabled": True, "min_approvals": 1}, - "plan": {"enabled": False}, - } - } - config = ApprovalGatesConfig(config_data) - assert config.is_phase_gated("specify") == True - assert config.is_phase_gated("plan") == False - - -def test_get_phase_gate(): - """Test retrieving gate configuration for phase""" - config_data = { - "approval_gates": { - "specify": {"enabled": True, "min_approvals": 1}, - } - } - config = ApprovalGatesConfig(config_data) - gate = config.get_phase_gate("specify") - assert gate is not None - assert gate.get("min_approvals") == 1 - - -def test_get_phase_gate_disabled(): - """Test getting gate for disabled phase""" - config_data = { - "approval_gates": { - "plan": {"enabled": False}, - } - } - config = ApprovalGatesConfig(config_data) - gate = config.get_phase_gate("plan") - assert gate is None # Returns None if not enabled From 83d706f36b7557ee8cc9440aaa0addff411904a1 Mon Sep 17 00:00:00 2001 From: Jonathan Lameira Date: Sun, 5 Apr 2026 14:36:24 -0300 Subject: [PATCH 4/4] feat(extensions): add approval-gates extension defaults Add default configuration values to extension.yml to align with extension development guidelines. --- extensions/approval-gates/extension.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/extensions/approval-gates/extension.yml b/extensions/approval-gates/extension.yml index 812400ce74..4692150aac 100644 --- a/extensions/approval-gates/extension.yml +++ b/extensions/approval-gates/extension.yml @@ -38,3 +38,15 @@ tags: - "governance" - "approval" - "quality-gates" + +defaults: + specify: + enabled: false + plan: + enabled: false + tasks: + enabled: false + implement: + enabled: false + constitution: + enabled: false