AgentSkillsCN

Pre Commit Hooks Implementation

Pre Commit 钩子实现

SKILL.md

Skill: Implement Pre-Commit Hooks Integration

Overview

The pre-commit hook system automatically renders OpenSpec artifact files (spec.md and tasks.md) from td state before every git commit. This ensures documents are always synchronized with the source of truth (td) without agents having to manually run commands.

Architecture

Hook Location

code
.git/hooks/pre-commit  (installed during project setup)

Invocation

Pre-commit hooks are automatically invoked by git before every commit:

bash
git commit [files] →  .git/hooks/pre-commit runs  →  commit proceeds or aborts

Pipeline

code
1. Pre-Commit Hook Triggered
   └── git commit is invoked

2. Hook Detects Changed Files
   ├── Read staging area: git diff --cached --name-only
   ├── Filter for files in openspec/changes/
   └── Extract change name from path

3. Render if Needed
   ├── If proposal.md or design.md changed:
   │   └── Run: opsx-render <change-name>
   ├── If spec.md or tasks.md changed but no td changes:
   │   └── Skip (already current)
   └── Otherwise:
       └── Skip (nothing to render)

4. Stage Rendered Files
   ├── git add openspec/changes/<change>/specs/**/*.md
   ├── git add openspec/changes/<change>/tasks.md
   └── Files now included in commit

5. Commit Proceeds (or fails if render error)
   ├── If render succeeded:
   │   └── git commit continues
   └── If render failed:
       └── git commit aborts, agent must fix

6. Cleanup
   └── Hook exit code (0 = success, 1 = failure)

Implementation Requirements

Requirement 1: Enforce document rendering (td-f56bc8)

Acceptance Criteria:

  • ✅ Hook runs on every git commit in openspec/changes/*/
  • ✅ Hook executes opsx-render before commit proceeds
  • ✅ Commit fails if render fails
  • ✅ Agent can see render error and fix

Implementation Details:

  1. Hook is installed at .git/hooks/pre-commit during setup
  2. Hook checks: are there staged files in openspec/changes/*/?
  3. If yes: extract change name and run opsx-render <change-name>
  4. If render fails: hook exits with code 1 (commit aborted)
  5. If render succeeds: hook exits with code 0 (commit proceeds)
  6. Commit only includes files that were staged BEFORE hook ran + rendered files

Hook pseudocode:

bash
#!/bin/bash
set -e  # Exit on error

CHANGED_FILES=$(git diff --cached --name-only)

# Check if any files in openspec/changes/ are staged
if echo "$CHANGED_FILES" | grep -q "openspec/changes/"; then
  # Extract change name from first matching path
  CHANGE_PATH=$(echo "$CHANGED_FILES" | grep "openspec/changes/" | head -1)
  CHANGE_NAME=$(echo "$CHANGE_PATH" | sed 's|openspec/changes/\([^/]*\)/.*|\1|')
  
  # Run render command
  if ! opsx-render "$CHANGE_NAME"; then
    echo "ERROR: opsx-render failed for change: $CHANGE_NAME"
    echo "Fix the issue in td and try again."
    exit 1
  fi
  
  # Stage rendered files
  git add "openspec/changes/$CHANGE_NAME/specs/**/*.md"
  git add "openspec/changes/$CHANGE_NAME/tasks.md"
fi

exit 0

Scenarios:

  • Agent commits changes to proposal.md: hook runs render, stages spec.md/tasks.md
  • Render succeeds: commit includes rendered files automatically
  • Render fails (missing td ids): hook aborts, shows error message

Requirement 2: Detect change directory (td-831182)

Acceptance Criteria:

  • ✅ Hook automatically detects which change directory is modified
  • ✅ Hook extracts change name from file path
  • ✅ Hook handles single-change commits correctly
  • ✅ Hook fails with clear message if multiple changes detected

Implementation Details:

  1. Read staged files: git diff --cached --name-only
  2. Filter lines matching pattern: openspec/changes/*/
  3. Extract change name using regex: openspec/changes/([^/]+)/
  4. Validate: all staged files in openspec/changes/ belong to SAME change
  5. If multiple changes: error message, exit 1
  6. If no changes: skip rendering, exit 0

Change name extraction:

code
Input:  openspec/changes/agent-task-management/specs/foo/spec.md
Output: agent-task-management

Input:  openspec/changes/my-feature/proposal.md
Output: my-feature

Multiple changes detection:

code
Staged:
  openspec/changes/change-1/proposal.md
  openspec/changes/change-2/proposal.md

Error: "Cannot commit multiple changes at once. 
Detected changes: change-1, change-2
Please commit one change at a time."

Scenarios:

  • Single change: hook detects it, renders correctly
  • Multiple changes: hook fails with clear message
  • Files outside openspec/changes/: hook skips, commit proceeds
  • No openspec/changes/ files: hook skips, commit proceeds

Requirement 3: Handle edge cases (td-bc7615)

Acceptance Criteria:

  • ✅ First commit doesn't block (creates files if missing)
  • ✅ Only docs changed (no td changes): skip render (idempotent)
  • ✅ No td changes: skip render but commit proceeds
  • ✅ Partial state handled gracefully

Implementation Details:

  1. First proposal.md creation:

    • Check: does proposal.md have proposal_feature_id?
    • If missing: error message with instructions
    • If present: proceed with rendering
  2. Doc-only changes:

    • Check: did proposal.md or design.md change?
    • If no: skip render, exit 0
    • If yes: run render (it's idempotent)
  3. No spec changes:

    • Check: does specs/ directory exist with files?
    • If no: create empty tasks.md showing "No tasks", exit 0
    • If yes: proceed with rendering
  4. Idempotency:

    • Render should produce identical output if td unchanged
    • Files unchanged? Still stage them (no harm)
    • Git will see no actual changes, commit proceeds

Scenarios:

  • First proposal commit (no feature_id): error guiding setup
  • Only design.md changed: render runs, files update, commit proceeds
  • No spec changes yet: render succeeds, empty tasks.md created
  • Multiple renders without td changes: identical output each time

Requirement 4: Simple and reproducible configuration (td-502042)

Acceptance Criteria:

  • ✅ Hook provided as template in repo
  • ✅ Simple shell script (~50 lines max)
  • ✅ Easy installation via make/script
  • ✅ Reproducible across all agents
  • ✅ Clear comments and minimal logic

Implementation Details:

  1. Hook template location: .husky/pre-commit or .git-hooks/pre-commit

  2. Installation method:

    bash
    make setup-hooks
    # or
    ./scripts/install-hooks.sh
    
  3. What installation does:

    • Copies template to .git/hooks/pre-commit
    • Makes it executable: chmod +x .git/hooks/pre-commit
    • Verifies opsx-render command is available
    • Tests hook with dummy run
  4. Hook characteristics:

    • Pure shell script (no Python/Node dependencies)
    • Uses only git and grep (basic tools)
    • Single call to opsx-render with error handling
    • Clear exit codes (0 = success, 1 = failure)

Sample hook (~40 lines):

bash
#!/bin/bash
# OpenSpec Pre-Commit Hook
# Automatically renders spec.md and tasks.md from td state

set -e

CHANGED_FILES=$(git diff --cached --name-only)

# Check for openspec/changes/* files
if ! echo "$CHANGED_FILES" | grep -q "^openspec/changes/"; then
  exit 0
fi

# Extract change name
CHANGE_PATH=$(echo "$CHANGED_FILES" | grep "^openspec/changes/" | head -1)
CHANGE_NAME=$(basename $(dirname "$CHANGE_PATH" | head -1))

echo "OpenSpec: Rendering $CHANGE_NAME..."

# Run opsx-render
if ! opsx-render "$CHANGE_NAME"; then
  echo ""
  echo "ERROR: Failed to render $CHANGE_NAME"
  echo "Fix issues in td and try committing again."
  exit 1
fi

# Stage rendered files
git add "openspec/changes/$CHANGE_NAME/specs/**/*.md" 2>/dev/null || true
git add "openspec/changes/$CHANGE_NAME/tasks.md" 2>/dev/null || true

echo "✓ Rendered files staged"
exit 0

Installation script:

bash
#!/bin/bash
# scripts/install-hooks.sh

HOOK_DIR=.git/hooks
HOOK_NAME=pre-commit
HOOK_SOURCE=.husky/$HOOK_NAME

# Create directory if needed
mkdir -p "$HOOK_DIR"

# Copy hook
cp "$HOOK_SOURCE" "$HOOK_DIR/$HOOK_NAME"
chmod +x "$HOOK_DIR/$HOOK_NAME"

echo "✓ Pre-commit hook installed"

Scenarios:

  • First clone: run make setup-hooks → hook installed and working
  • Manual setup: copy .husky/pre-commit to .git/hooks/pre-commit → works
  • Verify: cat .git/hooks/pre-commit → readable shell script, clear comments

Requirement 5: Automatically stage rendered files (td-c8139f)

Acceptance Criteria:

  • ✅ Generated files automatically staged after render
  • ✅ Staged files visible in git status
  • ✅ Agent can review before final commit
  • ✅ Agent can unstage if needed

Implementation Details:

  1. After successful render:

    bash
    git add openspec/changes/<change>/specs/**/*.md
    git add openspec/changes/<change>/tasks.md
    
  2. Visible in git status:

    code
    $ git status
    
    Changes to be committed:
      modified:   openspec/changes/agent-task-management/specs/agent-workflow-integration/spec.md
      modified:   openspec/changes/agent-task-management/specs/td-to-openspec-rendering/spec.md
      ...
      modified:   openspec/changes/agent-task-management/tasks.md
    
  3. Agent review before commit:

    bash
    git diff --staged openspec/changes/agent-task-management/specs/
    # Agent can see what will be rendered
    
  4. Optional unstage:

    bash
    git reset HEAD openspec/changes/agent-task-management/specs/
    # If agent wants to amend and re-render
    

Scenarios:

  • Render succeeds: files staged automatically, visible in git status
  • Agent reviews changes: git diff --staged shows rendered diff
  • Agent wants to revise: git reset HEAD → unstage → amend changes → commit again

Integration with opsx-render

The pre-commit hook depends on opsx-render being implemented and available:

code
opsx-render command
      ↑
      │ (invoked by)
      │
pre-commit hook
      ↑
      │ (runs before)
      │
git commit

Implementation order:

  1. First: Implement opsx-render command (Phase 2.1-2.5)
  2. Then: Implement pre-commit hook (Phase 2.6-2.10)
  3. Finally: Test integration of both together

Testing Strategy

Hook Installation Tests

  • Verify hook can be installed cleanly
  • Verify hook is executable
  • Verify hook runs without errors on non-openspec commits

Hook Functionality Tests

  • Commit to non-openspec files: hook skips, commit proceeds
  • Commit to openspec/changes/foo/proposal.md: hook renders, stages files
  • Commit to multiple changes: hook fails with clear message
  • Render failure: hook aborts commit, shows error

Integration Tests

  • Create change, add proposal, add specs, add requirements
  • Commit proposal: hook renders specs/tasks, stages them
  • Modify td state (mark requirement done)
  • Commit empty file: hook renders tasks.md with new status
  • Verify files are correct after each commit

Related Commands

  • opsx-render - Render documents manually (called by hook automatically)
  • /opsx-new - Create new change (will use hook on first commit)
  • /opsx-continue - Create specs/requirements (will use hook on commit)
  • /opsx-apply - Work on requirements (will use hook on commit)

Next Steps

  1. Implement opsx-render command (Phase 2 Reqs 1-5)
  2. Implement pre-commit hook (Phase 2 Reqs 6-10)
  3. Test integration of hook + render
  4. Document setup process for agents
  5. Move to opsx-* commands enhancement (Phase 3)