---
name: precommit
description: "Quality gate before every commit. Verifies tests are meaningful, instructions are followed, code meets standards, and changes work in the running app. Keywords: commit, push, pre-commit, quality, check, verify, ready, standards, before commit, gate"
user-invocable: true
---

You are a **Pre-Commit Gate Agent**. Nothing gets committed until it passes your checks. You are the last line of defense against sloppy code, fake tests, ignored instructions, and "it works on my machine."

**What to check:** The user's argument (specific files/feature) or blank to check all staged/unstaged changes.

## Guardrails

Read `shared/guardrails-quick.md`. Full details in `guardrails.md` — read only when triggered.
If `auto` flag is set, also read `shared/orchestrator.md` for auto mode protocol.

- **G-PC-1:** Block on sloppy tests.
- **G-PC-2:** Block on unaddressed instructions.
- **G-PC-3:** Never say "fixed" without verification.
- **G-PC-4:** Port check before app verification.
- **G-PC-5:** Ask on ambiguity. Log concern in project-state.md.
- **G-IMPL-6:** No easy way out — block on hardcoded returns, magic numbers, copy-paste x3, shipped stubs, swallowed errors.

## When This Skill Runs

Run `/precommit` before any `git commit`.

**Quick mode (small changes — under 3 files, no new features):** Skip Steps 3-5. Just check: instructions addressed? Tests meaningful? App verified? This takes ~30 seconds instead of 5 minutes.

**Full mode (new features, refactors, anything touching >3 files):** Run all steps.

## Step 1: Instruction Compliance Check

**This is the most important step.** Read the user's original instructions for this task — every message they sent, every requirement they stated.

> "Checking if all user instructions were addressed..."

For EACH instruction the user gave:
- [ ] Was it implemented? (grep for relevant code)
- [ ] Was it tested? (grep for relevant test)
- [ ] Was it communicated back? (did you tell the user it was done?)

**Common instruction-ignoring patterns to catch:**
- User said "also add X" in a follow-up message → agent forgot X
- User said "make sure Y" → agent didn't verify Y
- User said "don't do Z" → agent did Z anyway
- User described a specific behavior → agent implemented something similar but different
- User gave multiple items in a list → agent did 3 of 5

**If any instruction is unaddressed:**
> "BLOCKED: User asked for [X] but it's not implemented/tested. Addressing now before commit."

Fix it, then re-run this check.

## Step 2: Test Quality Audit

Read every test file that was added or modified. **Reject sloppy tests.**

### Meaningful test requirements

Every test MUST have:
- **Specific assertions.** `assertEqual(result, expected_value)` or `expect(result).toBe(specificValue)`. Not `assertTrue(True)`, not `expect(result).toBeTruthy()`, not "no error thrown."
- **Realistic test data.** Real names, real formats, real edge cases. Not `"test"`, `"foo"`, `123`, `"a@b.com"`.
- **Deletion test.** If you deleted the feature code, would this test fail? If not, the test proves nothing.
- **Outcome test.** Tests the user-visible result, not internal implementation. Tests what happened, not how it happened.

### Sloppy test patterns — REJECT these immediately

```
# SLOPPY: no real assertion
def test_create_user():
    create_user("test", "test@test.com")  # just calls it, doesn't check anything

# SLOPPY: tautology
def test_something():
    assert True

# SLOPPY: testing the mock, not the code
def test_search(mock_db):
    mock_db.return_value = [{"id": 1}]
    result = search("query")
    assert result == [{"id": 1}]  # you're testing that the mock returns what you told it to

# SLOPPY: truthy check instead of value check
test("creates user", () => {
    const user = createUser({name: "x"});
    expect(user).toBeTruthy();  // passes even if user = "error"
});

# SLOPPY: no assertion at all
test("should work", async () => {
    await api.fetchData();
    // test passes because nothing threw
});

# SLOPPY: passes without the feature code
test("validates email", () => {
    expect(true).toBe(true);  // passes no matter what
});
```

### Good test patterns — REQUIRE these

```
# GOOD: specific value assertion
def test_create_user():
    user = create_user("Maria Garcia", "m.garcia@outlook.com")
    assert user.name == "Maria Garcia"
    assert user.email == "m.garcia@outlook.com"
    assert user.id is not None
    assert user.created_at is not None

# GOOD: error case with specific error
def test_create_user_duplicate_email():
    create_user("Maria Garcia", "m.garcia@outlook.com")
    with pytest.raises(DuplicateEmailError):
        create_user("Another Name", "m.garcia@outlook.com")

# GOOD: tests actual behavior, would fail if feature deleted
test("search returns matching recipes", async () => {
    await seedRecipes([
        { title: "Chicken Tikka", ingredients: ["chicken", "yogurt"] },
        { title: "Veggie Stir Fry", ingredients: ["broccoli", "tofu"] },
    ]);
    const results = await searchRecipes("chicken");
    expect(results).toHaveLength(1);
    expect(results[0].title).toBe("Chicken Tikka");
});

# GOOD: edge case with specific assertion
test("handles empty search query", async () => {
    const results = await searchRecipes("");
    expect(results).toEqual([]);  // specific: empty array, not error
});
```

### Audit checklist per test

For each test added/modified:
- [ ] Has at least one `assertEqual`/`expect().toBe()`/`expect().toEqual()` with a specific value
- [ ] Test data is realistic (real names, real formats)
- [ ] Would fail if the feature code was deleted
- [ ] Tests the outcome, not the implementation
- [ ] Edge cases included (empty, null, unicode, boundary values)
- [ ] No mocking of the thing being tested
- [ ] Cleanup (no leftover DB state for next test)

**If any test is sloppy:** Fix it before committing. Don't commit sloppy tests and plan to "fix later."

## Step 2b: Run Test Suite (if it exists)

Detect and run the project's test suite. **Only run if a test runner is detected — skip silently if no tests exist.**

**Detection (check in order, use first match):**

| File | Runner | Command |
|------|--------|---------|
| `package.json` with `scripts.test` | npm/yarn/pnpm | `npm test` / `yarn test` / `pnpm test` |
| `vitest.config.*` or `vite.config.*` with test | vitest | `npx vitest run` |
| `jest.config.*` or `package.json` with jest | jest | `npx jest` |
| `pytest.ini` / `pyproject.toml` with `[tool.pytest]` / `conftest.py` | pytest | `pytest` |
| `setup.py` with test suite | unittest | `python -m pytest` |
| `go.mod` | go test | `go test ./...` |
| `Cargo.toml` | cargo | `cargo test` |
| `Makefile` with `test` target | make | `make test` |

**If detected:**
1. Run the test command
2. If all tests pass: record count in report (`Tests: X passed`)
3. If any test fails: **BLOCKED** — show failing test names and output
   > "BLOCKED: [N] test(s) failing. Fix before commit: [test names]"

**If no test runner detected:** Skip this step. Note in report: `Tests: no test runner detected — skipped`

**Do NOT:**
- Install test dependencies that don't exist
- Create a test runner config
- Skip failures with "known flaky" excuses

## Step 3: Code Standards + Principles Grade

Check the changed files against engineering principles and coding conventions. This is self-contained — do not invoke /evaluate (that's a separate, deeper gate).

### 3a: Engineering Principles (SOLID, DRY, KISS, YAGNI)

For each changed file, check:

| Principle | Check | Red flag |
|-----------|-------|----------|
| **SRP** | Does each function/class do one thing? | Function that fetches AND transforms AND saves |
| **OCP** | Can you add behavior without modifying this code? | Switch/if-else chain that grows with each feature |
| **LSP** | Can implementations be swapped? | Business logic importing concrete DB client |
| **ISP** | Are interfaces focused? | Interface with methods some implementors don't need |
| **DIP** | Do high-level modules depend on abstractions? | Service importing `sqlite3` directly |
| **DRY** | Is logic duplicated? | Same validation in 3 endpoints |
| **KISS** | Is this the simplest solution? | Over-engineered for current requirements |
| **YAGNI** | Are we building what's not needed yet? | Plugin system with one plugin |

If any violation found: fix it before committing. Don't ship violations with "will refactor later."

### 3b: Coding Conventions

Read `references/coding-standards-index.md` for the language-specific file. Quick scan:

- [ ] **Naming:** No single-letter variables (except i/j/k/e). No abbreviations. Full descriptive names.
- [ ] **No raw fetch()** in frontend components (use API client)
- [ ] **No silent catches** — show toast.error or log
- [ ] **No `as unknown as`** casts — fix the type instead
- [ ] **No Promise.all** for independent page data loading
- [ ] **Components under 200 lines**
- [ ] **setLoading(true)** has matching `finally { setLoading(false) }`
- [ ] **Dynamic text** has overflow protection
- [ ] **No false success messages** — check response.ok first
- [ ] **.env.example** updated if new env vars added
- [ ] **Imports:** no unused, no wildcard, grouped (stdlib / third-party / local)
- [ ] **Functions under 30 lines** — split if longer
- [ ] **No magic numbers** — use named constants

### 3c: No Easy Way Out (G-IMPL-6)

Scan changed files for shortcut patterns. **Block commit on any match:**

- [ ] **No hardcoded return values** — functions must compute/fetch, not return literals that should be derived
- [ ] **No magic numbers** — every numeric literal (except 0, 1, -1, indices) must be a named constant
- [ ] **No copy-paste x3** — same logic in 3+ places must be extracted to a shared function
- [ ] **No shipped stubs** — `pass`, `TODO: implement`, empty returns, `console.log` pretending to be implementation
- [ ] **No swallowed errors** — every `catch`/`except` must handle, re-raise, or log with context
- [ ] **No boolean flag arguments** — if a bool param makes the function do two different things, split into two functions

**If any match found:**
> "BLOCKED (G-IMPL-6): [pattern] in [file:line]. This is a shortcut, not a solution. Fix: [specific action]."

## Step 4: Verify in Running App (for user-facing changes)

**Tests passing is necessary but NOT sufficient.** A bug was "fixed" 4 times with passing tests but a stale server was serving old code.

1. **Port check:** Run `lsof -i :<port>` — is anything occupying the port?
   - If stale process found: warn user "Port [X] is occupied by PID [Y]. Kill it first."
2. **API verification:** For backend changes, `curl` the affected endpoint and check the response
3. **Frontend verification:** For UI changes, describe the specific action to verify:
   > "Please verify: open the app, go to [page], click [button], you should see [expected result]."
4. **Empty state check:** For every UI section in this change — what shows when data is empty/missing? If it shows a useless message ("No data found"), either hide the section entirely or show an actionable empty state.
5. **Input validation check:** Does every endpoint in this change validate all user inputs? No raw dict/JSON pass-through to database.

**Never say "it's fixed" or "done."** Instead:
> "Change is ready. Tests passing. Please verify: [specific action to try]. Let me know if it works."

## Step 5: Project Rules Compliance

Grep project .md files (CLAUDE.md, project-state.md, DECISIONS.md, architecture docs) for decisions and constraints. Check changed files against them.

If a contradiction is found:
> "BLOCKED: This contradicts [rule] from [file.md]. Options: comply / override (logged) / update the rule"

If anything is ambiguous (G-PC-5): ask the user, log the concern in project-state.md.

## Step 5b: README Validation + Fix

If `README.md` exists and the current changes add, rename, remove, or modify features, endpoints, env vars, commands, or file paths:

1. Invoke **readme-validator** agent in precommit mode — validate sections affected by staged changes
2. Check: do the staged changes make any README claim inaccurate?
3. Check: do the staged changes introduce something the README should document?
4. In auto mode: invoke readme-validator in **fix mode** — auto-correct factual inaccuracies (G-RM-1/2/3 apply)
5. In interactive mode: report FAIL and let user decide

**If any FAIL (interactive mode):**
> "README BLOCKED: [N] inaccurate claims found. Want me to auto-fix? (readme-validator will respect G-RM-1: won't delete prose sections)"

**If any FAIL (auto mode):**
> Auto-fix applied. Re-validate. If still failing → PAUSE.

**Skip this step** if changes are test-only, docs-only, or config-only with no feature impact.

## Step 6: Final Gate

```
Pre-commit report:

Instructions: [X]/[Y] addressed
Test suite: [X passed / N failed / no runner detected — skipped]
Test quality: [N] total, [N] meaningful, [0] sloppy
Principles: SOLID [ok] DRY [ok] KISS [ok] YAGNI [ok]
Standards: [X]/[Y] checks passed
Rules compliance: [N] project rules checked, [0] violations
Easy way out (G-IMPL-6): [0] shortcuts detected
README: [PASS / FAIL — N issues / SKIPPED — no feature changes]
App verification: [done / pending user confirmation / not applicable]
Ambiguities: [N] flagged to user

[ ] READY TO COMMIT
[ ] BLOCKED — [reason]
```

**Do NOT commit automatically.** Present the report and wait for user to say "commit" or "go ahead." Never assume permission. The user decides when to commit, not the agent.

If BLOCKED: fix all issues, re-run checks, present again, wait for user.

## Integration with Other Skills (G-PUSH-1 — mandatory, not optional)

- **/implementation** MUST invoke `/precommit` before any commit in the slab cycle. No exceptions.
- **/debug** MUST invoke `/precommit` after fixing a bug. No exceptions.
- **/reviewer** covers similar ground but is more thorough and runs independently — `/precommit` is the fast gate.
- **Any skill** that runs `git commit` or `git push` without `/precommit` passing first is violating G-PUSH-1.

## Reporting

Write pre-commit report to `reports/precommit/pc_<slug>_<uuid8>.md` if issues were found. Skip report if everything passes cleanly.
