PostToolUse hooks run after every file edit: - Backend: ./mvnw compile (Checkstyle Google Style + javac) - Frontend: vue-tsc --noEmit + oxlint + ESLint Stop hook runs test suites when source files changed, blocks the agent on failure and re-engages it to fix the issue. Output is filtered to [ERROR] lines only for context efficiency. Static analysis: Checkstyle (validate phase), SpotBugs (verify phase), ArchUnit (9 hexagonal architecture rules as JUnit tests). Fail-fast: Surefire skipAfterFailureCount=1, Vitest bail=1. Test log noise suppressed via logback-test.xml (WARN level), redirectTestOutputToFile, and trimStackTrace. Existing Java sources reformatted to Google Style (2-space indent, import order, Javadoc on public types). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
18 KiB
date, git_commit, branch, topic, tags, status
| date | git_commit | branch | topic | tags | status | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-03-04T01:40:21+01:00 | a55174b323 |
master | Backpressure for Agentic Coding |
|
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_pathfrom 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 --porcelainto determine if code files changed. Skips test runs on conversational responses. - Checkstyle bound to Maven
validatephase — automatically triggered by./mvnw compile, which means the PostToolUse hook gets Checkstyle for free. - SpotBugs bound to Maven
verifyphase — NOT hooked, run manually via./mvnw verify. - ArchUnit: use
archunit-junit51.4.1 only. Do NOT usearchunit-hexagonaladdon (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)
#!/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)
#!/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).
#!/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)
{
"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:
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 <build><plugins>:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Fail-fast: stop on first test failure -->
<skipAfterFailureCount>1</skipAfterFailureCount>
</configuration>
</plugin>
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 <build><plugins>:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>13.3.0</version>
</dependency>
</dependencies>
<configuration>
<configLocation>google_checks.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
</configuration>
<executions>
<execution>
<id>checkstyle-validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
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 <build><plugins>:
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.9.8.2</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<xmlOutput>true</xmlOutput>
<failOnError>true</failOnError>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
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 <dependencies>:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.1</version>
<scope>test</scope>
</dependency>
Do NOT use the archunit-hexagonal addon.
File 2: backend/src/test/java/de/fete/HexagonalArchitectureTest.java (new)
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:
test -x .claude/hooks/backend-compile-check.shtest -x .claude/hooks/frontend-type-check.shtest -x .claude/hooks/run-tests.shpython3 -c "import json; json.load(open('.claude/settings.json'))"cd backend && ./mvnw verify(triggers: Checkstyle → compile → test w/ ArchUnit → SpotBugs)cd frontend && npm run test:unit -- --runecho '{"tool_input":{"file_path":"backend/src/main/java/de/fete/FeteApplication.java"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.shecho '{"tool_input":{"file_path":"frontend/src/App.vue"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/frontend-type-check.shecho '{"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:
-
Stop hook JSON schema: The plan used
hookSpecificOutputwithhookEventName: "Stop"for the stop hook output. This is invalid —hookSpecificOutputis only supported forPreToolUse,PostToolUse, andUserPromptSubmitevents. Fixed to use top-level"decision": "approve"/"block"with"reason"field. -
Stop hook loop prevention: Added
stop_hook_activecheck from stdin JSON to prevent infinite re-engagement loops. Not in original plan. -
Context-efficient test output: Added
logback-test.xml(root level WARN),redirectTestOutputToFile=true, andtrimStackTrace=trueto 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. -
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/*.javawhich doesn't match relative paths. -
Checkstyle
includeTestSourceDirectory: Set totrueso test sources also follow Google Style. Not in original plan.FeteApplicationTest.javawas reformatted to 2-space indentation with correct import order (static imports first). -
ArchUnit field naming: Changed from
snake_case(onion_architecture_is_respected) tocamelCase(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
- JW: If you don't engineer backpressure, you'll get slopped
- HumanLayer: Context-Efficient Backpressure for Coding Agents
- Claude Code: Hooks Reference
- ArchUnit: User Guide