Files
crewli/dev-docs/CLAUDE_CODE_TOOLING.md
bert.hausmans c9e417690c chore: add multi-agent build pipeline (.claude/ agents, orchestrator, gates)
Adds crewli-architect, backend/frontend-implementer, test-writer subagents,
the /build-module orchestrator command, the PR merge-gate template, and a
permissions allow-list in settings.json. Documents the layer as
CLAUDE_CODE_TOOLING.md section 10. Implementer Edit/Write is allow-listed;
git push deliberately omitted so merge/push stay human.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-03 01:30:19 +02:00

274 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Claude Code tooling — `.claude/` reference
## 1. Purpose
`/CLAUDE.md` is *advisory*: it tells Claude what to do. Claude is good
about following it, but advisory rules fail open — when Claude misses
a line, nothing catches it. Crewli runs a parallel *deterministic*
layer in `.claude/` that fires whether or not Claude reads it: hooks
on tool calls, a code-review subagent, and slash commands for the
procedural shortcuts that come up every sprint.
The two layers are not redundant. They have different jobs.
> **Binding principle:** Would Claude make a mistake without this
> rule? If no, delete it. If yes, hook it. CLAUDE.md is for
> understanding; `.claude/` is for guarantees.
A rule belongs in `settings.json` / hooks if and only if Claude would
make a mistake without it. Otherwise the rule belongs nowhere — not
in CLAUDE.md, not in a hook. Don't double up: a hook rule that's also
documented in CLAUDE.md just rots when you change one and forget the
other.
## 2. Layout
```
.claude/
├── settings.json # Hook registry (committed)
├── settings.local.json # Per-user overrides (gitignored)
├── agents/
│ └── crewli-reviewer.md # Code-review subagent
├── commands/
│ ├── sprint-status.md # /sprint-status
│ ├── review-multitenancy.md # /review-multitenancy <Model>
│ └── sync-docs.md # /sync-docs
└── hooks/
├── protect-files.sh # PreToolUse Edit/Write
├── block-dangerous-bash.sh # PreToolUse Bash
├── post-edit-pint.sh # PostToolUse Edit/Write — PHP
├── post-edit-eslint.sh # PostToolUse Edit/Write — JS/TS/Vue
└── inject-sprint-context.sh # SessionStart compact
```
Everything except `settings.local.json` is checked in.
## 3. Hooks reference
| Event | Matcher | Script | Behaviour | Fail mode |
|---|---|---|---|---|
| PreToolUse | `Edit\|Write\|MultiEdit` | `protect-files.sh` | Blocks edits to secrets, lock files, default migrations, the deleted `apps/admin/`, `.claude/` itself, and `dev-docs/SCHEMA.md`. | Exit 2 with reason on stderr. |
| PreToolUse | `Bash` | `block-dangerous-bash.sh` | Blocks `git reset --hard`, force pushes, blanket dependency updates, database wipes that aren't scoped to `--env=testing`, and `rm -rf` on absolute paths outside `/tmp`, `/var/folders`, and `$HOME`. | Exit 2 with reason on stderr. |
| PostToolUse | `Edit\|Write\|MultiEdit` | `post-edit-pint.sh` | Runs `vendor/bin/pint --dirty` from `api/` after any `.php` edit. | Exit 0 silently — formatting failures never block. |
| PostToolUse | `Edit\|Write\|MultiEdit` | `post-edit-eslint.sh` | Runs `pnpm eslint --fix` inside `apps/app/` for `.vue/.ts/.tsx/.js` files. | Exit 0 silently. |
| SessionStart | `compact` | `inject-sprint-context.sh` | Prints branch, last 10 commits, and the top of `BACKLOG.md` so Claude resumes with sprint context after auto-compaction. | Exit 0; output is appended to context. |
Every script:
- Sets `set -euo pipefail`.
- Reads JSON from stdin via `jq`.
- Resolves paths through `$CLAUDE_PROJECT_DIR`.
- Completes in well under 500 ms for typical inputs.
PreToolUse hooks signal *block* by exiting `2` (never `1``1` is a
hook misconfiguration, not a policy denial). PostToolUse hooks always
exit `0`; a formatter that fails should not stop the agent.
## 4. Subagent: `crewli-reviewer`
Lives at `.claude/agents/crewli-reviewer.md`. Isolated context,
review-only — it has Read / Grep / Glob / Bash but no Edit. Designed
for a single pass against the zero-compromise principles in CLAUDE.md
and the schema rules in `dev-docs/SCHEMA.md`.
### When to invoke
After any non-trivial implementation, before committing:
> Use the crewli-reviewer subagent on the changes since HEAD~1.
### Report shape
Three sections, every finding cited as `path/to/file.php:LINE`:
- **MUST FIX** — blocking, violates a zero-compromise principle.
- **SHOULD FIX** — non-blocking but clearly correct improvement.
- **CONSIDER** — judgment call surfaced for Bert.
If the diff is clean, the agent outputs `No issues found against the
zero-compromise principles.` and stops.
### Extending the system prompt
Edit `crewli-reviewer.md`. The frontmatter (`name`, `description`,
`tools`, `model`) is the discovery contract — keep `tools` minimal so
the reviewer can't accidentally patch code. Add new checklist items
under the existing checklist sections; if a recurring pattern of
review findings emerges, promote it to the *six most-missed gaps*
list at the bottom of the prompt.
Don't move review checks into `CLAUDE.md` thinking it's redundant —
the subagent runs in an isolated context that may not have read
`CLAUDE.md` yet, so the prompt has to be self-contained.
## 5. Slash commands
| Command | Description | Example |
|---|---|---|
| `/sprint-status` | 510 line summary: current branch, last completed work package, uncommitted work, next BACKLOG item. | `/sprint-status` |
| `/review-multitenancy <Model>` | Reads model + migration + policy + tests for `<Model>` and reports PASS/FAIL/N/A on the multi-tenancy checklist. Ends with `READY` or `NEEDS WORK`. | `/review-multitenancy Shift` |
| `/sync-docs` | Runs `npm run sync:docs`, prints the manifest's Git SHA + generation timestamp, and reminds you to upload `.claude-sync/` to Project Knowledge. | `/sync-docs` |
Slash commands live in `.claude/commands/<name>.md`. Frontmatter
declares `description`, optional `argument-hint`, and an
`allowed-tools` allowlist (least-privilege — `/sync-docs` only allows
`Bash(npm:*)` and `Read`, not arbitrary Bash).
## 6. Testing a hook
Each hook reads a single JSON object on stdin. Test by piping a
fixture and inspecting the exit code and stderr:
```bash
# protect-files — should BLOCK
echo '{"tool_input":{"file_path":".env"}}' | bash .claude/hooks/protect-files.sh; echo "exit=$?"
# protect-files — should ALLOW
echo '{"tool_input":{"file_path":"app/Models/Event.php"}}' | bash .claude/hooks/protect-files.sh; echo "exit=$?"
# block-dangerous-bash — should BLOCK
echo '{"tool_input":{"command":"git push --force origin main"}}' | bash .claude/hooks/block-dangerous-bash.sh; echo "exit=$?"
# block-dangerous-bash — should ALLOW (testing scope)
echo '{"tool_input":{"command":"php artisan migrate:fresh --env=testing --seed"}}' | bash .claude/hooks/block-dangerous-bash.sh; echo "exit=$?"
# post-edit-pint — no-op on non-PHP
echo '{"tool_input":{"file_path":"README.md"}}' | bash .claude/hooks/post-edit-pint.sh; echo "exit=$?"
# post-edit-eslint — no-op on PHP
echo '{"tool_input":{"file_path":"api/app/Models/Event.php"}}' | bash .claude/hooks/post-edit-eslint.sh; echo "exit=$?"
# inject-sprint-context — full output
bash .claude/hooks/inject-sprint-context.sh
```
Block scenarios should print a reason on stderr and exit `2`; allow
scenarios should be silent and exit `0`.
## 7. Disabling temporarily
Set `"disableAllHooks": true` at the top level of `.claude/settings.json`:
```json
{
"disableAllHooks": true,
"hooks": { ... }
}
```
Document *why* in the commit message and revert before pushing the
branch you're working on. The hooks exist because they catch real
mistakes; turning them off is a knowingly-temporary state, not a
preference.
For per-user overrides without touching the committed file, use
`.claude/settings.local.json` (gitignored).
## 8. Adding a new hook
Checklist:
- [ ] Script completes in well under 500 ms (a slow hook makes every
tool call slower).
- [ ] PreToolUse: exit `2` to block (NEVER `1``1` reads as
misconfiguration). PostToolUse: always exit `0`.
- [ ] `set -euo pipefail` at the top.
- [ ] JSON parsed via `jq` from stdin (no shelling out to grep over
the whole payload).
- [ ] Path resolution via `$CLAUDE_PROJECT_DIR`, never `$PWD`.
- [ ] Test fixture in this document's section 6 covers the new
pattern.
- [ ] Entry added to the hooks reference table in section 3.
- [ ] `chmod +x` set, committed.
## 9. Design principle (verbatim)
Settings and hooks are deterministic; CLAUDE.md is advisory. They
serve different jobs and should never duplicate. If a rule is
hookable, hook it and *delete* it from CLAUDE.md — keeping both is
the worst of both worlds (the agent reads the rule, the hook fires
the rule, and when they diverge nobody knows which is right).
CLAUDE.md is for understanding the project; `.claude/` is for
catching the mistakes that happen anyway.
## 10. Multi-agent build pipeline
The `.claude/` layer now includes an orchestrated build pipeline on top
of the deterministic hooks and the review subagent. It automates the
work Bert previously did by hand (prompt authoring, dispatch, gate
assembly) while keeping the two irreversible decisions human.
### 10.1 Validated against
- Claude Code version: **<fill in `claude --version` at setup>**
- Re-verify after every `claude update`: the Agent-tool name, the
`isolation: worktree` field, and the subagent permission model have
all shifted in point releases. After an update, re-run the §6 hook
smoke-tests and one architect dry-run before trusting the chain.
### 10.2 The agents
| Agent | Model | Tools | Role |
|---|---|---|---|
| `crewli-architect` | opus | Read, Grep, Glob, Bash | Drift-check, audit, decompose, emit DECISION BRIEF. Plans only — never writes code. |
| `backend-implementer` | sonnet | + Edit, Write (worktree) | One bounded backend subtask per the approved plan. |
| `frontend-implementer` | sonnet | + Edit, Write (worktree) | One bounded `apps/app/` subtask. |
| `test-writer` | sonnet | + Edit, Write (worktree) | PHPUnit + Vitest tests; never weakens a test to pass. |
| `crewli-reviewer` | opus | Read, Grep, Glob (read-only) | Zero-compromise review; emits `REVIEW VERDICT: PASS\|BLOCK`. |
Implementer prompts are deliberately thin: they encode only what an
agent would get WRONG without the instruction, and lean on CLAUDE.md +
SCHEMA.md for the rest. They do NOT duplicate hookable rules (pint,
eslint, protect-files, block-dangerous-bash already fire on every tool
call). This is the §1 binding principle applied to agents.
### 10.3 The orchestrator
`/build-module <task>` runs in the MAIN session (subagents can't spawn
subagents). Five phases:
0. Branch off main (always Phase 0).
1. Architect -> DECISION BRIEF -> **HUMAN GATE 1** (approve/adjust/reject).
2. Dispatch implementers + test-writer in dependency order; parallel-
safe subtasks run as background subagents with worktree isolation.
3. Reviewer gate; BLOCK loops back to the implementer without bothering
the human; PASS proceeds.
4. Assemble `pr-merge-gate.md` with real signals -> **HUMAN GATE 2**
(reply `merge`). A red signal never reaches the human.
5. Post-merge: sync-docs reminder; branch cleanup ONLY after merge
verification (the D1 near-miss rule).
### 10.4 The two human gates
Both gates are designed to reduce to a single glance + one word:
- **Gate 1** (decomposition): the architect surfaces its own risk flags
and open questions at the top of the brief, so Bert weighs only the
flagged points, not the whole plan. Reply `approve`.
- **Gate 2** (merge): every signal is pre-verified green before the gate
is shown; a red signal returns the PR to the implementer instead.
Bert performs the `--no-ff` merge + push manually. Reply `merge`.
`git push` is intentionally OFF the settings.json allow-list, so the
"merge & push stay human" rule is enforced at the permission layer.
### 10.5 Permissions interaction
Subagents can't answer "ask" prompts (an asked tool is auto-denied), so
implementer Edit/Write/Bash are allow-listed in settings.json §permissions.
The PreToolUse hooks still fire and block the dangerous subset. Allow
broadly; block narrowly via hooks. Never add `git push` to the allow-list.
### 10.6 Files
```
.claude/agents/crewli-architect.md
.claude/agents/backend-implementer.md
.claude/agents/frontend-implementer.md
.claude/agents/test-writer.md
.claude/agents/crewli-reviewer.md (existing + verdict-line block)
.claude/commands/build-module.md
.claude/templates/pr-merge-gate.md
.claude/settings.json (existing hooks + new permissions)
```