chore: add agent guardrails (CLAUDE.md, stop hook, permissions, git hooks)
CLAUDE.md documents commands, the spec-first workflow, the functional-core rule and a do-not list. A Claude Code Stop hook blocks finishing while typecheck or unit tests fail. Permissions deny reading env files, hook bypass flags, hard resets and recursive deletes, and ask before CI edits or any push. Husky pre-commit now also typechecks and runs unit tests; pre-push runs coverage and the mutation gate when src/domain changed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop hook: the agent may only finish when typecheck and unit tests pass on the current code.
|
||||
set -uo pipefail
|
||||
cd "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel)}"
|
||||
|
||||
payload="$(cat)"
|
||||
# Avoid a loop: if we already blocked once in this turn, let it stop and show the failure to the user.
|
||||
if printf '%s' "$payload" | grep -q '"stop_hook_active"[[:space:]]*:[[:space:]]*true'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Nothing changed in the app, nothing to verify.
|
||||
if [ -z "$(git status --porcelain -- apps/web package.json pnpm-lock.yaml)" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
run() {
|
||||
local label="$1"; shift
|
||||
local out
|
||||
if ! out="$("$@" 2>&1)"; then
|
||||
{
|
||||
echo "VERIFY FAILED: $label"
|
||||
echo "$out" | tail -n 40
|
||||
echo "Fix the cause (do not weaken tests), then finish again."
|
||||
} >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
run "typecheck" pnpm --filter @mintel/web typecheck
|
||||
run "unit tests" pnpm --filter @mintel/web test
|
||||
exit 0
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Read(./.env)",
|
||||
"Read(./.env.*)",
|
||||
"Read(./apps/web/.env)",
|
||||
"Read(./apps/web/.env.*)",
|
||||
"Edit(./.env)",
|
||||
"Edit(./.env.*)",
|
||||
"Bash(git push --force*)",
|
||||
"Bash(git push -f*)",
|
||||
"Bash(git push * main*)",
|
||||
"Bash(git push * master*)",
|
||||
"Bash(git commit *--no-verify*)",
|
||||
"Bash(git commit * -n *)",
|
||||
"Bash(git reset --hard*)",
|
||||
"Bash(git clean*)",
|
||||
"Bash(rm -rf*)"
|
||||
],
|
||||
"ask": [
|
||||
"Edit(./.gitea/workflows/**)",
|
||||
"Write(./.gitea/workflows/**)",
|
||||
"Edit(./Dockerfile*)",
|
||||
"Edit(./docker-compose*)",
|
||||
"Bash(git push*)"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify.sh",
|
||||
"timeout": 180
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pnpm exec lint-staged
|
||||
pnpm --filter @mintel/web typecheck
|
||||
pnpm --filter @mintel/web test
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
# Pushing is gated harder than committing: full coverage, and mutation when the domain core changed.
|
||||
pnpm --filter @mintel/web test:coverage || exit 1
|
||||
|
||||
base="$(git merge-base HEAD origin/main 2>/dev/null || git rev-list --max-parents=0 HEAD | tail -n 1)"
|
||||
if git diff --name-only "$base"...HEAD | grep -q '^apps/web/src/domain/'; then
|
||||
echo "src/domain changed: running mutation gate (about 2 minutes)"
|
||||
pnpm --filter @mintel/web test:mutation || exit 1
|
||||
fi
|
||||
@@ -0,0 +1,46 @@
|
||||
# mintel.me
|
||||
|
||||
Personal website of a senior freelance engineer. Next.js 16 (App Router) + Payload, pnpm monorepo.
|
||||
App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.md` (read it first).
|
||||
|
||||
## Commands (run from repo root)
|
||||
|
||||
- `pnpm --filter @mintel/web typecheck` — must pass with zero errors
|
||||
- `pnpm --filter @mintel/web test` — unit specs (Vitest); 0 tests run counts as a failure
|
||||
- `pnpm --filter @mintel/web test:coverage` — `src/domain` must stay at 100%
|
||||
- `pnpm --filter @mintel/web test:mutation` — Stryker on `src/domain`, ~2 min, must score 100
|
||||
- `pnpm --filter @mintel/web lint`
|
||||
|
||||
## How to work here (spec first)
|
||||
|
||||
1. Write the behavior as concrete cases (inputs, outputs, edge cases, errors) as a `*.spec.ts`.
|
||||
2. Run it and confirm it fails for the right reason (missing behavior, not an import error).
|
||||
3. Implement the simplest clean solution, run again.
|
||||
4. Run typecheck, coverage and mutation. Survivors mean weak specs or dead code: fix one or the other.
|
||||
5. Report the exact commands and results. "Done" means they were run on the current code.
|
||||
Never weaken, skip or delete a test to make it pass.
|
||||
|
||||
## Architecture rules
|
||||
|
||||
- Functional core, imperative shell. Pure logic goes in `apps/web/src/domain/` (no I/O, no clock, no
|
||||
randomness, no framework imports; pass `today` etc. in as arguments). Everything there is covered 100%
|
||||
and mutation-tested. Pages, routes and adapters stay thin and call into the core.
|
||||
- Parse at the boundary into precise types (`Result`, discriminated unions); no nullable state bags.
|
||||
- No `any`, `@ts-ignore`, `!` non-null assertions or eslint-disable to silence errors. Fix the cause.
|
||||
|
||||
## Do not
|
||||
|
||||
- Read or edit `.env*` (only `.env.example`). Never print or log secrets.
|
||||
- Commit scratch files (`tmp*`, `*.log`, `*.tmp`, debug HTML, build output). They are gitignored; keep it so.
|
||||
- Use `--no-verify`, force-push, push to `main`, or run `reset --hard`, `clean`, `rm -rf` without asking.
|
||||
- Edit `.gitea/workflows/*` or deploy config without asking.
|
||||
- Upgrade `vitest` past 4.1.x: `@stryker-mutator/vitest-runner` 10.0.0 runs zero tests per mutant under
|
||||
vitest 5 and reports false survivors. Re-verify mutation results after any tooling bump.
|
||||
- Add dependencies for trivial things, or touch unrelated code (no drive-by refactors).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Stryker needs `plugins` listed explicitly in `stryker.config.json` (pnpm layout) and ignores the large
|
||||
asset folders via `ignorePatterns`; keep them in sync when adding top-level folders.
|
||||
- `@mintel/*` packages are `link:` dependencies on a sibling `at-mintel` checkout.
|
||||
- Git branches: work on `feat/*`, never directly on `main`.
|
||||
Reference in New Issue
Block a user