--- date: 2026-03-04T01:40:21+01:00 git_commit: a55174b32333d0f46a55d94a50604344d1ba33f6 branch: master topic: "Backpressure for Agentic Coding" tags: [plan, backpressure, hooks, checkstyle, spotbugs, archunit, quality] status: complete --- # Backpressure for Agentic Coding — Implementation Plan ## Overview Implement automated feedback mechanisms (backpressure) that force the AI agent to self-correct before a human reviews the output. The approach follows the 90/10 rule: 90% deterministic constraints (types, linting, architecture tests), 10% agentic review. ## Current State vs. Desired State | Layer | Backend (now) | Backend (after) | Frontend (now) | Frontend (after) | |-------|---------------|-----------------|----------------|------------------| | Type System | Java 25 (strong) | *unchanged* | TS strict + `noUncheckedIndexedAccess` | *unchanged* | | Static Analysis | **None** | Checkstyle (Google Style) + SpotBugs | ESLint + oxlint + Prettier | *unchanged* | | Architecture Tests | **None** | ArchUnit (hexagonal enforcement) | N/A | N/A | | Unit Tests | JUnit 5 | JUnit 5 + fail-fast (`skipAfterFailureCount: 1`) | Vitest | Vitest + fail-fast (`bail: 1`) | | PostToolUse Hook | **None** | `./mvnw compile -q` (incl. Checkstyle) | **None** | `vue-tsc --noEmit` | | Stop Hook | **None** | `./mvnw test` | **None** | `npm run test:unit -- --run` | ## What We're NOT Doing - **Error Prone** — overlaps with SpotBugs, Java 25 compatibility uncertain, more invasive setup - **Custom ESLint rules** — add later when recurring agent mistakes are observed - **MCP LSP Server** — experimental, high setup cost, unclear benefit vs. hooks - **Pre-commit git hooks** — orthogonal concern, not part of this plan - **CI/CD pipeline** — out of scope, this is about local agent feedback ## Design Decisions - **Hook matchers** are regex on **tool names** (not file paths). File-path filtering must happen inside the hook script via `tool_input.file_path` from stdin JSON. - **Context-efficient output**: ✓ on success, full error on failure. Don't waste the agent's context window with passing output. - **Fail-fast**: one failure at a time. Prevents context-switching between multiple bugs. - **Stop hook** checks `git status --porcelain` to determine if code files changed. Skips test runs on conversational responses. - **Checkstyle** bound to Maven `validate` phase — automatically triggered by `./mvnw compile`, which means the PostToolUse hook gets Checkstyle for free. - **SpotBugs** bound to Maven `verify` phase — NOT hooked, run manually via `./mvnw verify`. - **ArchUnit**: use `archunit-junit5` 1.4.1 only. Do NOT use `archunit-hexagonal` addon (dormant since Jul 2023, pulls Kotlin, pinned to ArchUnit 1.0.1, expects different package naming). --- ## Task List Tasks are ordered by priority. Execute one task per iteration, in order. Each task includes the exact changes and a verification command. ### T-BP-01: Create backend compile-check hook script `[x]` **File**: `.claude/hooks/backend-compile-check.sh` (new, create `.claude/hooks/` directory if needed) ```bash #!/usr/bin/env bash set -euo pipefail # Read hook input from stdin (JSON with tool_input.file_path) INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "") # Only run for Java files under backend/ case "$FILE_PATH" in */backend/src/*.java) ;; *) exit 0 ;; esac cd "$CLAUDE_PROJECT_DIR/backend" # Run compile (includes validate phase → Checkstyle if configured) # Context-efficient: suppress output on success, show full output on failure if OUTPUT=$(./mvnw compile -q 2>&1); then echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ Backend compile passed."}}' else ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}" fi ``` Make executable: `chmod +x .claude/hooks/backend-compile-check.sh` **Verify**: `echo '{"tool_input":{"file_path":"backend/src/main/java/de/fete/FeteApplication.java"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` → should output JSON with "✓ Backend compile passed." **Verify skip**: `echo '{"tool_input":{"file_path":"README.md"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` → should exit 0 silently. --- ### T-BP-02: Create frontend type-check hook script `[x]` **File**: `.claude/hooks/frontend-type-check.sh` (new) ```bash #!/usr/bin/env bash set -euo pipefail # Read hook input from stdin (JSON with tool_input.file_path) INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "") # Only run for TS/Vue files under frontend/ case "$FILE_PATH" in */frontend/src/*.ts|*/frontend/src/*.vue) ;; *) exit 0 ;; esac cd "$CLAUDE_PROJECT_DIR/frontend" # Run type-check # Context-efficient: suppress output on success, show full output on failure if OUTPUT=$(npx vue-tsc --noEmit 2>&1); then echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ Frontend type-check passed."}}' else ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}" fi ``` Make executable: `chmod +x .claude/hooks/frontend-type-check.sh` **Verify**: `echo '{"tool_input":{"file_path":"frontend/src/App.vue"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/frontend-type-check.sh` → should output JSON with "✓ Frontend type-check passed." --- ### T-BP-03: Create stop hook script (test gate) `[x]` **File**: `.claude/hooks/run-tests.sh` (new) Runs after the agent finishes its response. Checks `git status` for changed source files and runs the relevant test suites. Skips on conversational responses (no code changes). ```bash #!/usr/bin/env bash set -euo pipefail cd "$CLAUDE_PROJECT_DIR" # Check for uncommitted changes in backend/frontend source HAS_BACKEND=$(git status --porcelain backend/src/ 2>/dev/null | head -1) HAS_FRONTEND=$(git status --porcelain frontend/src/ 2>/dev/null | head -1) # Nothing changed — skip if [[ -z "$HAS_BACKEND" && -z "$HAS_FRONTEND" ]]; then exit 0 fi ERRORS="" PASSED="" # Run backend tests if Java sources changed if [[ -n "$HAS_BACKEND" ]]; then if OUTPUT=$(cd backend && ./mvnw test -q 2>&1); then PASSED+="✓ Backend tests passed. " else ERRORS+="Backend tests failed:\n$OUTPUT\n\n" fi fi # Run frontend tests if TS/Vue sources changed if [[ -n "$HAS_FRONTEND" ]]; then if OUTPUT=$(cd frontend && npm run test:unit -- --run 2>&1); then PASSED+="✓ Frontend tests passed. " else ERRORS+="Frontend tests failed:\n$OUTPUT\n\n" fi fi if [[ -n "$ERRORS" ]]; then ESCAPED=$(printf '%s' "$ERRORS" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))") echo "{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"additionalContext\":$ESCAPED}}" else echo "{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"additionalContext\":\"$PASSED\"}}" fi ``` Make executable: `chmod +x .claude/hooks/run-tests.sh` **Verify**: `CLAUDE_PROJECT_DIR=. .claude/hooks/run-tests.sh` → if no uncommitted changes in source dirs, should exit 0 silently. --- ### T-BP-04: Create `.claude/settings.json` with hook configuration `[x]` **File**: `.claude/settings.json` (new — do NOT modify `.claude/settings.local.json`, that has permissions) ```json { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/backend-compile-check.sh\"", "timeout": 120 }, { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/frontend-type-check.sh\"", "timeout": 60 } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh\"", "timeout": 300 } ] } ] } } ``` **Verify**: File exists and is valid JSON: `python3 -c "import json; json.load(open('.claude/settings.json'))"` --- ### T-BP-05: Configure Vitest fail-fast `[x]` **File**: `frontend/vitest.config.ts` (modify) Add `bail: 1` to the test configuration object. The result should look like: ```typescript export default mergeConfig( viteConfig, defineConfig({ test: { environment: 'jsdom', exclude: [...configDefaults.exclude, 'e2e/**'], root: fileURLToPath(new URL('./', import.meta.url)), bail: 1, }, }), ) ``` **Verify**: `cd frontend && npm run test:unit -- --run` passes. --- ### T-BP-06: Configure Maven Surefire fail-fast `[x]` **File**: `backend/pom.xml` (modify) Add `maven-surefire-plugin` configuration within ``: ```xml org.apache.maven.plugins maven-surefire-plugin 1 ``` **Verify**: `cd backend && ./mvnw test` passes. --- ### T-BP-07: Add Checkstyle plugin + fix violations `[x]` **File**: `backend/pom.xml` (modify) Add `maven-checkstyle-plugin` within ``: ```xml org.apache.maven.plugins maven-checkstyle-plugin 3.6.0 com.puppycrawl.tools checkstyle 13.3.0 google_checks.xml true true warning checkstyle-validate validate check ``` Then run `cd backend && ./mvnw checkstyle:check` to find violations. Fix all violations in existing source files (`FeteApplication.java`, `HealthController.java`, `FeteApplicationTest.java`, all `package-info.java` files). Google Style requires: 2-space indentation, specific import order, Javadoc on public types, max line length 100. **Verify**: `cd backend && ./mvnw checkstyle:check` passes with zero violations AND `cd backend && ./mvnw compile` passes (Checkstyle now runs during validate phase). --- ### T-BP-08: Add SpotBugs plugin + verify `[x]` **File**: `backend/pom.xml` (modify) Add `spotbugs-maven-plugin` within ``: ```xml com.github.spotbugs spotbugs-maven-plugin 4.9.8.2 Max Low true true check ``` Run `cd backend && ./mvnw verify` — if SpotBugs finds issues, fix them. **Verify**: `cd backend && ./mvnw verify` passes (includes compile + test + SpotBugs). --- ### T-BP-09: Add ArchUnit dependency + write architecture tests `[x]` **File 1**: `backend/pom.xml` (modify) — add within ``: ```xml com.tngtech.archunit archunit-junit5 1.4.1 test ``` Do NOT use the `archunit-hexagonal` addon. **File 2**: `backend/src/test/java/de/fete/HexagonalArchitectureTest.java` (new) ```java package de.fete; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class) class HexagonalArchitectureTest { @ArchTest static final ArchRule onion_architecture_is_respected = onionArchitecture() .domainModels("de.fete.domain.model..") .domainServices("de.fete.domain.port.in..", "de.fete.domain.port.out..") .applicationServices("de.fete.application.service..") .adapter("web", "de.fete.adapter.in.web..") .adapter("persistence", "de.fete.adapter.out.persistence..") .adapter("config", "de.fete.config.."); @ArchTest static final ArchRule domain_must_not_depend_on_adapters = noClasses() .that().resideInAPackage("de.fete.domain..") .should().dependOnClassesThat().resideInAPackage("de.fete.adapter.."); @ArchTest static final ArchRule domain_must_not_depend_on_application = noClasses() .that().resideInAPackage("de.fete.domain..") .should().dependOnClassesThat().resideInAPackage("de.fete.application.."); @ArchTest static final ArchRule domain_must_not_depend_on_config = noClasses() .that().resideInAPackage("de.fete.domain..") .should().dependOnClassesThat().resideInAPackage("de.fete.config.."); @ArchTest static final ArchRule inbound_ports_must_be_interfaces = classes() .that().resideInAPackage("de.fete.domain.port.in..") .should().beInterfaces(); @ArchTest static final ArchRule outbound_ports_must_be_interfaces = classes() .that().resideInAPackage("de.fete.domain.port.out..") .should().beInterfaces(); @ArchTest static final ArchRule domain_must_not_use_spring = noClasses() .that().resideInAPackage("de.fete.domain..") .should().dependOnClassesThat().resideInAPackage("org.springframework.."); @ArchTest static final ArchRule web_must_not_depend_on_persistence = noClasses() .that().resideInAPackage("de.fete.adapter.in.web..") .should().dependOnClassesThat().resideInAPackage("de.fete.adapter.out.persistence.."); @ArchTest static final ArchRule persistence_must_not_depend_on_web = noClasses() .that().resideInAPackage("de.fete.adapter.out.persistence..") .should().dependOnClassesThat().resideInAPackage("de.fete.adapter.in.web.."); } ``` **Verify**: `cd backend && ./mvnw test` passes and output shows `HexagonalArchitectureTest` with 9 tests. --- ### T-BP-10: Update CLAUDE.md `[x]` **File**: `CLAUDE.md` (modify) Add two rows to the Build Commands table: | What | Command | |------|---------| | Backend checkstyle | `cd backend && ./mvnw checkstyle:check` | | Backend full verify | `cd backend && ./mvnw verify` | --- ### T-BP-11: Final verification `[x]` Run all verification commands to confirm the complete backpressure stack works: 1. `test -x .claude/hooks/backend-compile-check.sh` 2. `test -x .claude/hooks/frontend-type-check.sh` 3. `test -x .claude/hooks/run-tests.sh` 4. `python3 -c "import json; json.load(open('.claude/settings.json'))"` 5. `cd backend && ./mvnw verify` (triggers: Checkstyle → compile → test w/ ArchUnit → SpotBugs) 6. `cd frontend && npm run test:unit -- --run` 7. `echo '{"tool_input":{"file_path":"backend/src/main/java/de/fete/FeteApplication.java"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` 8. `echo '{"tool_input":{"file_path":"frontend/src/App.vue"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/frontend-type-check.sh` 9. `echo '{"tool_input":{"file_path":"README.md"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` (should be silent) All commands must exit 0. If any fail, go back and fix the issue before marking complete. --- ## Addendum: Implementation Deviations (2026-03-04) Changes made during implementation that deviate from or extend the original plan: 1. **Stop hook JSON schema**: The plan used `hookSpecificOutput` with `hookEventName: "Stop"` for the stop hook output. This is invalid — `hookSpecificOutput` is only supported for `PreToolUse`, `PostToolUse`, and `UserPromptSubmit` events. Fixed to use top-level `"decision": "approve"/"block"` with `"reason"` field. 2. **Stop hook loop prevention**: Added `stop_hook_active` check from stdin JSON to prevent infinite re-engagement loops. Not in original plan. 3. **Context-efficient test output**: Added `logback-test.xml` (root level WARN), `redirectTestOutputToFile=true`, and `trimStackTrace=true` to Surefire config. The stop hook script filters output to `[ERROR]` lines only, stripping Maven boilerplate. Not in original plan — added after observing that raw test failure output consumed excessive context. 4. **Hook path matching**: Case patterns in hook scripts extended to match both absolute and relative file paths (`*/backend/src/*.java|backend/src/*.java`). Original plan only had `*/backend/src/*.java` which doesn't match relative paths. 5. **Checkstyle `includeTestSourceDirectory`**: Set to `true` so test sources also follow Google Style. Not in original plan. `FeteApplicationTest.java` was reformatted to 2-space indentation with correct import order (static imports first). 6. **ArchUnit field naming**: Changed from `snake_case` (`onion_architecture_is_respected`) to `camelCase` (`onionArchitectureIsRespected`) to comply with Google Checkstyle rules. --- ## References - Research document: `docs/agents/research/2026-03-04-backpressure-agentic-coding.md` - Geoffrey Huntley: [Don't waste your back pressure](https://ghuntley.com/pressure/) - JW: [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure) - HumanLayer: [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure) - Claude Code: [Hooks Reference](https://code.claude.com/docs/en/hooks) - ArchUnit: [User Guide](https://www.archunit.org/userguide/html/000_Index.html)