Files
fete/docs/agents/plan/2026-03-04-backpressure-agentic-coding.md
nitrix a9802c2881 Add backpressure stack for agentic coding quality gates
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>
2026-03-04 02:44:15 +01:00

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
plan
backpressure
hooks
checkstyle
spotbugs
archunit
quality
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)

#!/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:

  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