Implement the 006-pre-commit-gate feature that enforces a pre-commit quality gate using Lefthook to run pnpm check before every commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-05 11:44:44 +01:00
parent fea2bfe39d
commit 0bbd6f27f9
11 changed files with 483 additions and 0 deletions

View File

@@ -53,6 +53,7 @@ The constitution (`.specify/memory/constitution.md`) governs all feature work:
## Active Technologies ## Active Technologies
- TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite (003-remove-combatant) - TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite (003-remove-combatant)
- In-memory React state (local-first, single-user MVP) (003-remove-combatant) - In-memory React state (local-first, single-user MVP) (003-remove-combatant)
- TypeScript 5.x (project), Go binary via npm (Lefthook) + `lefthook` (npm devDependency) (006-pre-commit-gate)
## Recent Changes ## Recent Changes
- 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite - 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite

4
lefthook.yml Normal file
View File

@@ -0,0 +1,4 @@
pre-commit:
jobs:
- name: check
run: pnpm check

View File

@@ -3,10 +3,12 @@
"packageManager": "pnpm@10.6.0", "packageManager": "pnpm@10.6.0",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.0.0", "@biomejs/biome": "2.0.0",
"lefthook": "^1.11.0",
"typescript": "^5.8.0", "typescript": "^5.8.0",
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"scripts": { "scripts": {
"prepare": "lefthook install",
"format": "biome format --write .", "format": "biome format --write .",
"format:check": "biome format .", "format:check": "biome format .",
"lint": "biome lint .", "lint": "biome lint .",

100
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@biomejs/biome': '@biomejs/biome':
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0
lefthook:
specifier: ^1.11.0
version: 1.13.6
typescript: typescript:
specifier: ^5.8.0 specifier: ^5.8.0
version: 5.9.3 version: 5.9.3
@@ -660,6 +663,60 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
lefthook-darwin-arm64@1.13.6:
resolution: {integrity: sha512-m6Lb77VGc84/Qo21Lhq576pEvcgFCnvloEiP02HbAHcIXD0RTLy9u2yAInrixqZeaz13HYtdDaI7OBYAAdVt8A==}
cpu: [arm64]
os: [darwin]
lefthook-darwin-x64@1.13.6:
resolution: {integrity: sha512-CoRpdzanu9RK3oXR1vbEJA5LN7iB+c7hP+sONeQJzoOXuq4PNKVtEaN84Gl1BrVtCNLHWFAvCQaZPPiiXSy8qg==}
cpu: [x64]
os: [darwin]
lefthook-freebsd-arm64@1.13.6:
resolution: {integrity: sha512-X4A7yfvAJ68CoHTqP+XvQzdKbyd935sYy0bQT6Ajz7FL1g7hFiro8dqHSdPdkwei9hs8hXeV7feyTXbYmfjKQQ==}
cpu: [arm64]
os: [freebsd]
lefthook-freebsd-x64@1.13.6:
resolution: {integrity: sha512-ai2m+Sj2kGdY46USfBrCqLKe9GYhzeq01nuyDYCrdGISePeZ6udOlD1k3lQKJGQCHb0bRz4St0r5nKDSh1x/2A==}
cpu: [x64]
os: [freebsd]
lefthook-linux-arm64@1.13.6:
resolution: {integrity: sha512-cbo4Wtdq81GTABvikLORJsAWPKAJXE8Q5RXsICFUVznh5PHigS9dFW/4NXywo0+jfFPCT6SYds2zz4tCx6DA0Q==}
cpu: [arm64]
os: [linux]
lefthook-linux-x64@1.13.6:
resolution: {integrity: sha512-uJl9vjCIIBTBvMZkemxCE+3zrZHlRO7Oc+nZJ+o9Oea3fu+W82jwX7a7clw8jqNfaeBS+8+ZEQgiMHWCloTsGw==}
cpu: [x64]
os: [linux]
lefthook-openbsd-arm64@1.13.6:
resolution: {integrity: sha512-7r153dxrNRQ9ytRs2PmGKKkYdvZYFPre7My7XToSTiRu5jNCq++++eAKVkoyWPduk97dGIA+YWiEr5Noe0TK2A==}
cpu: [arm64]
os: [openbsd]
lefthook-openbsd-x64@1.13.6:
resolution: {integrity: sha512-Z+UhLlcg1xrXOidK3aLLpgH7KrwNyWYE3yb7ITYnzJSEV8qXnePtVu8lvMBHs/myzemjBzeIr/U/+ipjclR06g==}
cpu: [x64]
os: [openbsd]
lefthook-windows-arm64@1.13.6:
resolution: {integrity: sha512-Uxef6qoDxCmUNQwk8eBvddYJKSBFglfwAY9Y9+NnnmiHpWTjjYiObE9gT2mvGVpEgZRJVAatBXc+Ha5oDD/OgQ==}
cpu: [arm64]
os: [win32]
lefthook-windows-x64@1.13.6:
resolution: {integrity: sha512-mOZoM3FQh3o08M8PQ/b3IYuL5oo36D9ehczIw1dAgp1Ly+Tr4fJ96A+4SEJrQuYeRD4mex9bR7Ps56I73sBSZA==}
cpu: [x64]
os: [win32]
lefthook@1.13.6:
resolution: {integrity: sha512-ojj4/4IJ29Xn4drd5emqVgilegAPN3Kf0FQM2p/9+lwSTpU+SZ1v4Ig++NF+9MOa99UKY8bElmVrLhnUUNFh5g==}
hasBin: true
loupe@3.2.1: loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@@ -1362,6 +1419,49 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
lefthook-darwin-arm64@1.13.6:
optional: true
lefthook-darwin-x64@1.13.6:
optional: true
lefthook-freebsd-arm64@1.13.6:
optional: true
lefthook-freebsd-x64@1.13.6:
optional: true
lefthook-linux-arm64@1.13.6:
optional: true
lefthook-linux-x64@1.13.6:
optional: true
lefthook-openbsd-arm64@1.13.6:
optional: true
lefthook-openbsd-x64@1.13.6:
optional: true
lefthook-windows-arm64@1.13.6:
optional: true
lefthook-windows-x64@1.13.6:
optional: true
lefthook@1.13.6:
optionalDependencies:
lefthook-darwin-arm64: 1.13.6
lefthook-darwin-x64: 1.13.6
lefthook-freebsd-arm64: 1.13.6
lefthook-freebsd-x64: 1.13.6
lefthook-linux-arm64: 1.13.6
lefthook-linux-x64: 1.13.6
lefthook-openbsd-arm64: 1.13.6
lefthook-openbsd-x64: 1.13.6
lefthook-windows-arm64: 1.13.6
lefthook-windows-x64: 1.13.6
loupe@3.2.1: {} loupe@3.2.1: {}
lru-cache@5.1.1: lru-cache@5.1.1:

View File

@@ -1,3 +1,6 @@
packages: packages:
- "packages/*" - "packages/*"
- "apps/*" - "apps/*"
onlyBuiltDependencies:
- lefthook

View File

@@ -0,0 +1,34 @@
# Specification Quality Checklist: Pre-Commit Gate
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-05
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.

View File

@@ -0,0 +1,67 @@
# Implementation Plan: Pre-Commit Gate
**Branch**: `006-pre-commit-gate` | **Date**: 2026-03-05 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/006-pre-commit-gate/spec.md`
## Summary
Enforce a pre-commit quality gate by adding Lefthook as a Git hooks manager. A `lefthook.yml` configuration defines a pre-commit hook that runs `pnpm check`. The hook auto-installs via a `prepare` script in `package.json`, ensuring zero manual setup for developers after `pnpm install`.
## Technical Context
**Language/Version**: TypeScript 5.x (project), Go binary via npm (Lefthook)
**Primary Dependencies**: `lefthook` (npm devDependency)
**Storage**: N/A
**Testing**: Manual verification (commit with passing/failing checks)
**Target Platform**: macOS, Linux (developer workstations)
**Project Type**: Monorepo (pnpm workspaces) -- web application with domain/application/web layers
**Performance Goals**: No additional overhead beyond `pnpm check` execution time
**Constraints**: Must auto-install on `pnpm install`; must not interfere with CI
**Scale/Scope**: 2 files changed (lefthook.yml created, package.json modified)
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Deterministic Domain Core | N/A | No domain logic changes |
| II. Layered Architecture | PASS | Tooling-only change, no layer imports affected |
| III. Agent Boundary | N/A | No agent layer changes |
| IV. Clarification-First | PASS | User explicitly specified Lefthook; no ambiguity |
| V. Escalation Gates | PASS | Feature is within spec scope |
| VI. MVP Baseline Language | PASS | No permanent bans introduced |
| VII. No Gameplay Rules | N/A | Not a gameplay feature |
| Development Workflow (merge gate) | PASS | Directly implements the "automated checks must pass" rule |
| Layer boundary compliance | N/A | No source code layer changes |
**Post-design re-check**: All gates still pass. No design decisions introduced new violations.
## Project Structure
### Documentation (this feature)
```text
specs/006-pre-commit-gate/
├── plan.md # This file
├── research.md # Phase 0 output
├── quickstart.md # Phase 1 output
├── spec.md # Feature specification
└── checklists/
└── requirements.md # Spec quality checklist
```
### Source Code (repository root)
```text
./
├── lefthook.yml # NEW — Lefthook configuration (pre-commit hook)
├── package.json # MODIFIED — add lefthook devDep + prepare script
└── (all existing files unchanged)
```
**Structure Decision**: This feature only adds tooling configuration at the repository root. No source code directories are created or modified. The existing monorepo structure (`packages/*`, `apps/*`) is unchanged.
## Complexity Tracking
No constitution violations. Table not applicable.

View File

@@ -0,0 +1,34 @@
# Quickstart: Pre-Commit Gate
## What This Feature Does
Adds a Git pre-commit hook (managed by Lefthook) that runs `pnpm check` before every commit. If any check fails, the commit is blocked.
## Files to Create/Modify
| File | Action | Purpose |
|------|--------|---------|
| `lefthook.yml` | Create | Lefthook configuration with pre-commit hook |
| `package.json` | Modify | Add `lefthook` devDependency + `prepare` script |
## Setup After Implementation
After the feature is implemented, hooks activate automatically:
```bash
pnpm install # installs lefthook + runs `prepare` which calls `lefthook install`
```
## How It Works
1. Developer runs `git commit`
2. Lefthook intercepts via the Git pre-commit hook
3. Lefthook runs `pnpm check` (format + lint + typecheck + test)
4. If `pnpm check` exits 0 → commit proceeds
5. If `pnpm check` exits non-zero → commit is blocked, output shown
## Bypass
```bash
git commit --no-verify # skips the pre-commit hook
```

View File

@@ -0,0 +1,45 @@
# Research: Pre-Commit Gate
## R1: Hook Management Tool
**Decision**: Use [Lefthook](https://github.com/evilmartians/lefthook) (npm package) as the Git hooks manager.
**Rationale**:
- User explicitly requested Lefthook.
- Lightweight, standalone Go binary distributed via npm -- no runtime dependencies.
- Simple YAML configuration (`lefthook.yml`).
- Auto-installs hooks via npm `postinstall` lifecycle script -- satisfies FR-005 (no manual setup).
- Well-maintained, widely adopted (used by n8n, Shopify, and others).
- Respects `--no-verify` by default (standard Git behavior) -- satisfies FR-006.
**Alternatives considered**:
- Husky: Popular but heavier configuration, requires `.husky/` directory with shell scripts.
- `core.hooksPath`: Native Git, but requires manual setup or custom scripts for auto-install.
- Simple `prepare` script copying a shell script: Works but no parallel jobs, no structured config.
## R2: Auto-Install Mechanism
**Decision**: Use a `prepare` script in root `package.json` that runs `lefthook install`.
**Rationale**:
- The `prepare` lifecycle hook runs automatically after `pnpm install`.
- This ensures every developer gets hooks installed without extra steps after cloning.
- Lefthook's npm package includes a `postinstall` script that can auto-install, but an explicit `prepare` script is more transparent and reliable across package managers.
- In CI environments, `CI=true` prevents the `prepare` script from running (standard npm/pnpm behavior), avoiding unnecessary hook installation in CI.
**Alternatives considered**:
- Relying solely on lefthook's built-in `postinstall`: Less transparent; behavior varies with `CI` env var.
- Manual `lefthook install` step in README: Violates FR-005.
## R3: Hook Command Strategy
**Decision**: The pre-commit hook runs `pnpm check` as a single command.
**Rationale**:
- `pnpm check` already orchestrates format, lint, typecheck, and test in sequence.
- Running it as one command keeps the hook configuration minimal and consistent with the existing merge-gate workflow.
- Output from `pnpm check` already identifies which specific check failed (FR-004, SC-004).
**Alternatives considered**:
- Running each check as a separate Lefthook job with `parallel: true`: Could be faster but adds configuration complexity and the existing `pnpm check` script already handles sequencing. MVP baseline does not include parallel hook jobs.
- Using `{staged_files}` for file-scoped checks: MVP baseline does not include staged-only checking per spec assumptions.

View File

@@ -0,0 +1,88 @@
# Feature Specification: Pre-Commit Gate
**Feature Branch**: `006-pre-commit-gate`
**Created**: 2026-03-05
**Status**: Draft
**Input**: User description: "Enforce a pre-commit gate: block commits unless `pnpm check` passes."
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Blocked Commit on Failing Checks (Priority: P1)
A developer attempts to commit code that does not pass `pnpm check` (format, lint, typecheck, or test failures). The commit is automatically rejected with a clear message indicating what failed, preventing broken code from entering the repository.
**Why this priority**: This is the core purpose of the feature -- preventing commits that violate project quality standards.
**Independent Test**: Can be tested by introducing a deliberate lint or type error, attempting to commit, and verifying the commit is blocked with an informative error message.
**Acceptance Scenarios**:
1. **Given** a developer has staged changes that fail formatting, **When** they run `git commit`, **Then** the commit is rejected and the output shows the formatting errors.
2. **Given** a developer has staged changes that fail linting, **When** they run `git commit`, **Then** the commit is rejected and the output shows the lint errors.
3. **Given** a developer has staged changes that fail typechecking, **When** they run `git commit`, **Then** the commit is rejected and the output shows the typecheck errors.
4. **Given** a developer has staged changes that fail tests, **When** they run `git commit`, **Then** the commit is rejected and the output shows the test failures.
---
### User Story 2 - Successful Commit on Passing Checks (Priority: P1)
A developer commits code that passes all checks. The pre-commit gate runs `pnpm check`, all checks pass, and the commit proceeds normally without extra friction.
**Why this priority**: Equally critical -- the gate must not block valid commits. A gate that only blocks but never allows is useless.
**Independent Test**: Can be tested by making a valid code change, committing, and verifying the commit succeeds after checks pass.
**Acceptance Scenarios**:
1. **Given** a developer has staged changes that pass all checks, **When** they run `git commit`, **Then** `pnpm check` runs and the commit completes successfully.
---
### User Story 3 - Bypass Gate in Emergencies (Priority: P2)
A developer needs to bypass the pre-commit gate in an emergency situation (e.g., a hotfix where the existing codebase already has a known issue). They can use the standard Git `--no-verify` flag to skip the hook.
**Why this priority**: Important escape hatch, but not the primary use case. Standard Git behavior should be preserved.
**Independent Test**: Can be tested by attempting `git commit --no-verify` with failing checks and verifying the commit succeeds.
**Acceptance Scenarios**:
1. **Given** a developer has staged changes that fail checks, **When** they run `git commit --no-verify`, **Then** the commit proceeds without running the pre-commit gate.
---
### Edge Cases
- What happens when `pnpm` is not installed or not in PATH? The hook should fail with a clear error message.
- What happens when `node_modules` are not installed? The hook should fail with a clear error message suggesting `pnpm install`.
- What happens when the hook is run outside the project root? The hook should resolve the project root correctly.
- What happens on a fresh clone? The hook must be automatically available after `pnpm install` without additional manual steps.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The repository MUST include a Git pre-commit hook that runs `pnpm check` before every commit.
- **FR-002**: The hook MUST block the commit (exit non-zero) if `pnpm check` fails.
- **FR-003**: The hook MUST allow the commit (exit zero) if `pnpm check` succeeds.
- **FR-004**: The hook MUST display the output from `pnpm check` so the developer can see what failed.
- **FR-005**: The hook MUST be automatically available to all developers after cloning and running `pnpm install` (no manual hook installation steps).
- **FR-006**: The hook MUST be bypassable using the standard `git commit --no-verify` flag.
- **FR-007**: The hook MUST provide a clear error message if `pnpm` is not available.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: 100% of commits made without `--no-verify` are validated by `pnpm check` before being accepted.
- **SC-002**: Developers see check results within the normal `pnpm check` execution time -- the hook adds no meaningful overhead beyond running the checks themselves.
- **SC-003**: New contributors can clone the repository, run `pnpm install`, and have the pre-commit gate active without any additional setup steps.
- **SC-004**: Developers can identify the specific failing check (format, lint, typecheck, or test) from the hook output alone.
## Assumptions
- The project already has a working `pnpm check` command that runs format, lint, typecheck, and test checks.
- All developers use Git for version control.
- The hook management approach uses Lefthook, a lightweight Git hooks manager distributed as an npm package, with a `prepare` script for auto-installation.
- MVP baseline does not include partial/staged-only checking (e.g., lint-staged). The full `pnpm check` runs on the entire project.

View File

@@ -0,0 +1,105 @@
# Tasks: Pre-Commit Gate
**Input**: Design documents from `/specs/006-pre-commit-gate/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, quickstart.md
**Tests**: No test tasks included (not requested in feature specification). Verification is manual.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Install Lefthook and configure auto-install mechanism
- [x] T001 Add `lefthook` as a devDependency and add a `prepare` script that runs `lefthook install` in `package.json`
- [x] T002 Run `pnpm install` to install lefthook and activate the prepare script
**Checkpoint**: `lefthook` is installed and `pnpm install` triggers `lefthook install` automatically
---
## Phase 2: User Story 1 & 2 - Block Failing / Allow Passing Commits (Priority: P1) MVP
**Goal**: Create the Lefthook pre-commit hook configuration that runs `pnpm check`, blocking commits on failure and allowing commits on success.
**Independent Test (US1)**: Introduce a deliberate lint error, run `git commit`, and verify the commit is blocked with visible error output.
**Independent Test (US2)**: Make a valid change, run `git commit`, and verify the commit succeeds after `pnpm check` passes.
### Implementation
- [x] T003 [US1] [US2] Create `lefthook.yml` at repository root with a `pre-commit` hook that runs `pnpm check`
**Checkpoint**: Commits are blocked when `pnpm check` fails (US1) and allowed when it passes (US2). Output from `pnpm check` is visible to the developer.
---
## Phase 3: User Story 3 - Bypass Gate in Emergencies (Priority: P2)
**Goal**: Ensure the standard `git commit --no-verify` flag bypasses the pre-commit hook.
**Independent Test**: Stage a change that would fail checks, run `git commit --no-verify`, and verify the commit succeeds without running checks.
### Implementation
No implementation task needed -- Lefthook respects `--no-verify` by default (standard Git behavior). This phase exists for verification only.
**Checkpoint**: `git commit --no-verify` bypasses the pre-commit gate.
---
## Phase 4: Polish & Cross-Cutting Concerns
**Purpose**: Edge case handling and validation
- [x] T004 Verify the hook provides a clear error when `pnpm` is not in PATH (FR-007) and when `node_modules` are missing (edge case)
- [x] T005 Run quickstart.md validation: clone-install-commit workflow works end-to-end (SC-003)
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies -- start immediately
- **US1 & US2 (Phase 2)**: Depends on Phase 1 (lefthook must be installed)
- **US3 (Phase 3)**: No implementation needed -- verify after Phase 2
- **Polish (Phase 4)**: Depends on Phase 2 completion
### Parallel Opportunities
- T001 and T003 touch different files (`package.json` vs `lefthook.yml`) but T003 depends on lefthook being installed, so they must be sequential.
- T004 and T005 can run in parallel after Phase 2.
---
## Implementation Strategy
### MVP First (User Stories 1 & 2)
1. Complete Phase 1: Install lefthook + prepare script
2. Complete Phase 2: Create `lefthook.yml` with pre-commit hook
3. **STOP and VALIDATE**: Test both blocking and allowing commits
4. Verify US3 bypass works (no implementation needed)
### Execution Summary
Total: **5 tasks** across 4 phases
- Phase 1 (Setup): 2 tasks
- Phase 2 (US1 + US2): 1 task
- Phase 3 (US3): 0 tasks (verification only)
- Phase 4 (Polish): 2 tasks
---
## Notes
- US1 and US2 are implemented by the same single task (T003) because they are two sides of the same coin: the hook either blocks or allows based on `pnpm check` exit code.
- US3 requires no implementation -- `--no-verify` is standard Git behavior that Lefthook respects.
- This is a minimal-footprint feature: 1 new file (`lefthook.yml`), 1 modified file (`package.json`).