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>
This commit is contained in:
23
.claude/hooks/backend-compile-check.sh
Executable file
23
.claude/hooks/backend-compile-check.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/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|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
|
||||||
37
.claude/hooks/frontend-check.sh
Executable file
37
.claude/hooks/frontend-check.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/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|frontend/src/*.ts|frontend/src/*.vue) ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cd "$CLAUDE_PROJECT_DIR/frontend"
|
||||||
|
|
||||||
|
ERRORS=""
|
||||||
|
|
||||||
|
# Type-check
|
||||||
|
if OUTPUT=$(npx vue-tsc --noEmit 2>&1); then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
ERRORS+="Type-check failed:\n$OUTPUT\n\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lint (without --fix — agent must self-correct)
|
||||||
|
if OUTPUT=$(npx oxlint . 2>&1 && npx eslint . --cache 2>&1); then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
ERRORS+="Lint failed:\n$OUTPUT\n\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$ERRORS" ]]; then
|
||||||
|
ESCAPED=$(printf '%s' "$ERRORS" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||||
|
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
|
||||||
|
else
|
||||||
|
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ Frontend type-check + lint passed."}}'
|
||||||
|
fi
|
||||||
54
.claude/hooks/run-tests.sh
Executable file
54
.claude/hooks/run-tests.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$CLAUDE_PROJECT_DIR"
|
||||||
|
|
||||||
|
# Read hook input from stdin
|
||||||
|
INPUT=$(cat)
|
||||||
|
|
||||||
|
# Prevent infinite loops: if already re-engaged by a previous Stop hook, let it stop
|
||||||
|
STOP_HOOK_ACTIVE=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('stop_hook_active', False))" 2>/dev/null || echo "False")
|
||||||
|
if [[ "$STOP_HOOK_ACTIVE" == "True" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# Filter: only [ERROR] lines, skip Maven boilerplate
|
||||||
|
FILTERED=$(echo "$OUTPUT" | grep -E "^\[ERROR\]" | grep -v -E "Re-run Maven|See |Help 1|full stack trace|Failed to execute goal|For more information|^\[ERROR\] *$" || true)
|
||||||
|
ERRORS+="Backend tests failed:\n$FILTERED\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
|
||||||
|
# Block stopping — re-engage the agent to fix failures
|
||||||
|
ESCAPED=$(printf '%s' "$ERRORS" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||||
|
echo "{\"decision\":\"block\",\"reason\":$ESCAPED}"
|
||||||
|
else
|
||||||
|
# Success — allow stopping, report via stopReason
|
||||||
|
echo "{\"decision\":\"approve\",\"reason\":\"$PASSED\"}"
|
||||||
|
fi
|
||||||
32
.claude/settings.json
Normal file
32
.claude/settings.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"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-check.sh\"",
|
||||||
|
"timeout": 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh\"",
|
||||||
|
"timeout": 300
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
29
CLAUDE.md
29
CLAUDE.md
@@ -66,14 +66,16 @@ These are the non-negotiable principles of this project. Every decision — arch
|
|||||||
|
|
||||||
### Build Commands
|
### Build Commands
|
||||||
|
|
||||||
| What | Command |
|
| What | Command |
|
||||||
|------|---------|
|
|------------------|----------------------------------------|
|
||||||
| Backend test | `cd backend && ./mvnw test` |
|
| Backend test | `cd backend && ./mvnw test` |
|
||||||
| Backend run | `cd backend && ./mvnw spring-boot:run` |
|
| Backend run | `cd backend && ./mvnw spring-boot:run` |
|
||||||
| Frontend install | `cd frontend && npm install` |
|
| Frontend install | `cd frontend && npm install` |
|
||||||
| Frontend build | `cd frontend && npm run build` |
|
| Frontend build | `cd frontend && npm run build` |
|
||||||
| Frontend test | `cd frontend && npm run test:unit` |
|
| Frontend test | `cd frontend && npm run test:unit` |
|
||||||
| Frontend dev | `cd frontend && npm run dev` |
|
| Frontend dev | `cd frontend && npm run dev` |
|
||||||
|
| Backend checkstyle | `cd backend && ./mvnw checkstyle:check` |
|
||||||
|
| Backend full verify | `cd backend && ./mvnw verify` |
|
||||||
|
|
||||||
### Agent Documentation
|
### Agent Documentation
|
||||||
|
|
||||||
@@ -82,6 +84,17 @@ These are the non-negotiable principles of this project. Every decision — arch
|
|||||||
- Agent test reports (browser verification): `.agent-tests/` (gitignored)
|
- Agent test reports (browser verification): `.agent-tests/` (gitignored)
|
||||||
- Use the `browser-interactive-testing` skill (rodney/showboat) for visual verification — this is an agent tool, not manual work.
|
- Use the `browser-interactive-testing` skill (rodney/showboat) for visual verification — this is an agent tool, not manual work.
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
|
||||||
|
The following skills are available and should be used for their respective purposes:
|
||||||
|
|
||||||
|
| Skill | Trigger | Purpose |
|
||||||
|
|-------------------------------|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `rpi-research` | Deep codebase investigation needed | Conducts thorough research and produces a written report. Use for understanding how something works before changing it. |
|
||||||
|
| `rpi-plan` | Feature, refactor, or bug fix needs planning | Creates detailed, phased implementation plans through interactive research and iteration. |
|
||||||
|
| `rpi-implement` | Approved plan ready for execution | Executes approved implementation plans phase by phase with automated and manual verification. |
|
||||||
|
| `browser-interactive-testing` | Visual verification of web pages | Headless Chrome testing via rodney/showboat. Use for screenshots, browser automation, and visual test reports. |
|
||||||
|
|
||||||
### Ralph Loops
|
### Ralph Loops
|
||||||
|
|
||||||
- Autonomous work is done via Ralph Loops. See [.claude/rules/ralph-loops.md](.claude/rules/ralph-loops.md) for documentation.
|
- Autonomous work is done via Ralph Loops. See [.claude/rules/ralph-loops.md](.claude/rules/ralph-loops.md) for documentation.
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -33,13 +33,13 @@ A privacy-focused, self-hostable web app for event announcements and RSVPs. An a
|
|||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|--------------|------------------------------------------|
|
|--------------|--------------------------------------------|
|
||||||
| Backend | Java (latest LTS), Spring Boot, Maven |
|
| Backend | Java (latest LTS), Spring Boot, Maven |
|
||||||
| Frontend | Vue 3, Vite, TypeScript |
|
| Frontend | Vue 3, Vite, TypeScript |
|
||||||
| Database | PostgreSQL (external, not bundled) |
|
| Database | PostgreSQL (external, not bundled) |
|
||||||
| Architecture | SPA + RESTful API, hexagonal/onion backend |
|
| Architecture | SPA + RESTful API, hexagonal/onion backend |
|
||||||
| Deployment | Single Docker container |
|
| Deployment | Single Docker container |
|
||||||
|
|
||||||
## Self-hosting
|
## Self-hosting
|
||||||
|
|
||||||
@@ -89,11 +89,11 @@ The app runs at `http://localhost:8080`. Database migrations run automatically o
|
|||||||
|
|
||||||
All configuration is done via environment variables:
|
All configuration is done via environment variables:
|
||||||
|
|
||||||
| Variable | Required | Description |
|
| Variable | Required | Description |
|
||||||
|----------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------------|----------|-----------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `DATABASE_URL` | Yes | JDBC connection string for PostgreSQL |
|
| `DATABASE_URL` | Yes | JDBC connection string for PostgreSQL |
|
||||||
| `MAX_ACTIVE_EVENTS` | No | Maximum number of non-expired events. Unset = unlimited |
|
| `MAX_ACTIVE_EVENTS` | No | Maximum number of non-expired events. Unset = unlimited |
|
||||||
| `UNSPLASH_API_KEY` | No | Enables header image search via Unsplash. Images are fetched server-side and stored locally — guests never contact Unsplash |
|
| `UNSPLASH_API_KEY` | No | Enables header image search via Unsplash. Images are fetched server-side and stored locally — guests never contact Unsplash |
|
||||||
|
|
||||||
### Image storage
|
### Image storage
|
||||||
|
|
||||||
@@ -138,6 +138,57 @@ cd backend && mvn package
|
|||||||
cd frontend && npm run build
|
cd frontend && npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Code quality
|
||||||
|
|
||||||
|
Automated quality gates run as Claude Code hooks during AI-assisted development. They provide immediate feedback after every file edit and block the agent from finishing when tests fail.
|
||||||
|
|
||||||
|
### Backend (Java / Maven)
|
||||||
|
|
||||||
|
**After editing a `*.java` file** (PostToolUse hook):
|
||||||
|
|
||||||
|
| What | Command | Fails on |
|
||||||
|
|---------------------|------------------|----------------------------------|
|
||||||
|
| Checkstyle | `./mvnw compile` | Style violations (Google Style) |
|
||||||
|
| Java compiler | `./mvnw compile` | Compile errors |
|
||||||
|
|
||||||
|
Checkstyle enforces Google Style (2-space indent, import order, Javadoc on public types) and is bound to the `validate` phase, so it runs automatically as part of every `compile`. Covers both `src/main` and `src/test`.
|
||||||
|
|
||||||
|
**When the agent finishes** (Stop hook — only if `backend/src/` has uncommitted changes):
|
||||||
|
|
||||||
|
| What | Command | Fails on |
|
||||||
|
|---------------------|----------------|---------------------------------------|
|
||||||
|
| JUnit 5 | `./mvnw test` | Test failures (fail-fast, stops at 1) |
|
||||||
|
| ArchUnit (9 rules) | `./mvnw test` | Hexagonal architecture violations |
|
||||||
|
|
||||||
|
ArchUnit enforces hexagonal boundaries: domain must not depend on adapters, application, config, or Spring; ports must be interfaces; web and persistence adapters must not cross-depend.
|
||||||
|
|
||||||
|
**Not hooked** (run manually):
|
||||||
|
|
||||||
|
| What | Command | Fails on |
|
||||||
|
|---------------------|------------------|----------------------------------------------------|
|
||||||
|
| SpotBugs | `./mvnw verify` | Potential bugs, null dereferences, resource leaks |
|
||||||
|
|
||||||
|
### Frontend (TypeScript / Vue)
|
||||||
|
|
||||||
|
**After editing a `*.ts` or `*.vue` file** (PostToolUse hook):
|
||||||
|
|
||||||
|
| What | Command | Fails on |
|
||||||
|
|---------------------|--------------------|-----------------|
|
||||||
|
| TypeScript (strict) | `vue-tsc --noEmit` | Type errors |
|
||||||
|
| oxlint + ESLint | `oxlint`, `eslint` | Lint violations |
|
||||||
|
|
||||||
|
**When the agent finishes** (Stop hook — only if `frontend/src/` has uncommitted changes):
|
||||||
|
|
||||||
|
| What | Command | Fails on |
|
||||||
|
|---------------------|------------------------------|---------------------------------------|
|
||||||
|
| Vitest | `npm run test:unit -- --run` | Test failures (fail-fast, stops at 1) |
|
||||||
|
|
||||||
|
**Not hooked** (run manually or via editor):
|
||||||
|
|
||||||
|
| What | Command | Fails on |
|
||||||
|
|---------------------|------------------|-------------------|
|
||||||
|
| Prettier | `npm run format` | Formatting issues |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL — see [LICENSE](LICENSE) for details.
|
GPL — see [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
@@ -33,10 +33,73 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.tngtech.archunit</groupId>
|
||||||
|
<artifactId>archunit-junit5</artifactId>
|
||||||
|
<version>1.4.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<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>
|
||||||
|
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>checkstyle-validate</id>
|
||||||
|
<phase>validate</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<!-- Fail-fast: stop on first test failure -->
|
||||||
|
<skipAfterFailureCount>1</skipAfterFailureCount>
|
||||||
|
<!-- Context-efficient output for agent backpressure -->
|
||||||
|
<trimStackTrace>true</trimStackTrace>
|
||||||
|
<redirectTestOutputToFile>true</redirectTestOutputToFile>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<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>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package de.fete;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/** Spring Boot entry point for the fete application. */
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class FeteApplication {
|
public class FeteApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
/** Starts the application. */
|
||||||
SpringApplication.run(FeteApplication.class, args);
|
public static void main(String[] args) {
|
||||||
}
|
SpringApplication.run(FeteApplication.class, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package de.fete.adapter.in.web;
|
package de.fete.adapter.in.web;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/** REST endpoint for health checks. */
|
||||||
@RestController
|
@RestController
|
||||||
public class HealthController {
|
public class HealthController {
|
||||||
|
|
||||||
@GetMapping("/health")
|
/** Returns a simple health status. */
|
||||||
public Map<String, String> health() {
|
@GetMapping("/health")
|
||||||
return Map.of("status", "ok");
|
public Map<String, String> health() {
|
||||||
}
|
return Map.of("status", "ok");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
package de.fete;
|
package de.fete;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class FeteApplicationTest {
|
class FeteApplicationTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
||||||
// Spring context starts successfully
|
// Spring context starts successfully
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void healthEndpointReturns200() throws Exception {
|
void healthEndpointReturns200() throws Exception {
|
||||||
mockMvc.perform(get("/health"))
|
mockMvc.perform(get("/health"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.status").value("ok"));
|
.andExpect(jsonPath("$.status").value("ok"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
backend/src/test/java/de/fete/HexagonalArchitectureTest.java
Normal file
63
backend/src/test/java/de/fete/HexagonalArchitectureTest.java
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package de.fete;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class)
|
||||||
|
class HexagonalArchitectureTest {
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule onionArchitectureIsRespected = 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 domainMustNotDependOnAdapters = noClasses()
|
||||||
|
.that().resideInAPackage("de.fete.domain..")
|
||||||
|
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter..");
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule domainMustNotDependOnApplication = noClasses()
|
||||||
|
.that().resideInAPackage("de.fete.domain..")
|
||||||
|
.should().dependOnClassesThat().resideInAPackage("de.fete.application..");
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule domainMustNotDependOnConfig = noClasses()
|
||||||
|
.that().resideInAPackage("de.fete.domain..")
|
||||||
|
.should().dependOnClassesThat().resideInAPackage("de.fete.config..");
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule inboundPortsMustBeInterfaces = classes()
|
||||||
|
.that().resideInAPackage("de.fete.domain.port.in..")
|
||||||
|
.should().beInterfaces();
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule outboundPortsMustBeInterfaces = classes()
|
||||||
|
.that().resideInAPackage("de.fete.domain.port.out..")
|
||||||
|
.should().beInterfaces();
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule domainMustNotUseSpring = noClasses()
|
||||||
|
.that().resideInAPackage("de.fete.domain..")
|
||||||
|
.should().dependOnClassesThat().resideInAPackage("org.springframework..");
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule webMustNotDependOnPersistence = noClasses()
|
||||||
|
.that().resideInAPackage("de.fete.adapter.in.web..")
|
||||||
|
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.out.persistence..");
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule persistenceMustNotDependOnWeb = noClasses()
|
||||||
|
.that().resideInAPackage("de.fete.adapter.out.persistence..")
|
||||||
|
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.in.web..");
|
||||||
|
}
|
||||||
12
backend/src/test/resources/logback-test.xml
Normal file
12
backend/src/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="WARN">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
487
docs/agents/plan/2026-03-04-backpressure-agentic-coding.md
Normal file
487
docs/agents/plan/2026-03-04-backpressure-agentic-coding.md
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
---
|
||||||
|
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 `<build><plugins>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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)
|
||||||
|
|
||||||
|
```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)
|
||||||
216
docs/agents/research/2026-03-04-backpressure-agentic-coding.md
Normal file
216
docs/agents/research/2026-03-04-backpressure-agentic-coding.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-04T01:40:21+01:00
|
||||||
|
git_commit: a55174b32333d0f46a55d94a50604344d1ba33f6
|
||||||
|
branch: master
|
||||||
|
topic: "Backpressure for Agentic Coding"
|
||||||
|
tags: [research, backpressure, agentic-coding, quality, tooling, hooks, static-analysis, archunit]
|
||||||
|
status: complete
|
||||||
|
---
|
||||||
|
|
||||||
|
# Research: Backpressure for Agentic Coding
|
||||||
|
|
||||||
|
## Research Question
|
||||||
|
|
||||||
|
What tools, methodologies, and patterns exist for implementing backpressure in agentic coding workflows? Which are applicable to the fete tech stack (Java 25, Spring Boot 3.5, Maven, Vue 3, TypeScript, Vitest)?
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Backpressure in agentic coding means: **automated feedback mechanisms that reject wrong output deterministically**, forcing the agent to self-correct before a human ever sees the result. The concept is borrowed from distributed systems (reactive streams, flow control) and applied to AI-assisted development.
|
||||||
|
|
||||||
|
The key insight from the literature: **90% deterministic, 10% agentic.** Encode constraints in the type system, linting rules, architecture tests, and test suites — not in prose instructions. The agent runs verification on its own output, sees failures, and fixes itself. Humans review only code that has already passed all automated gates.
|
||||||
|
|
||||||
|
### Core Sources
|
||||||
|
|
||||||
|
| Source | Author | Key Contribution |
|
||||||
|
|--------|--------|-----------------|
|
||||||
|
| [Don't waste your back pressure](https://ghuntley.com/pressure/) | Geoffrey Huntley | Coined "backpressure for agents." Feedback-driven quality, progressive delegation. |
|
||||||
|
| [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure) | JW | Verification hierarchy: types → linting → tests → agentic review. 90/10 rule. |
|
||||||
|
| [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure) | HumanLayer | Output filtering, fail-fast, context window preservation. |
|
||||||
|
| [Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks) | Anthropic | PostToolUse hooks for automated feedback after file edits. |
|
||||||
|
| [ArchUnit](https://www.archunit.org/) | TNG Technology Consulting | Architecture rules as unit tests. Hexagonal architecture enforcement. |
|
||||||
|
|
||||||
|
## Detailed Findings
|
||||||
|
|
||||||
|
### 1. The Backpressure Concept
|
||||||
|
|
||||||
|
In distributed systems, backpressure prevents upstream producers from overwhelming downstream consumers. Applied to agentic coding:
|
||||||
|
|
||||||
|
- **Producer:** The AI agent generating code
|
||||||
|
- **Consumer:** The quality gates (compiler, linter, tests, architecture rules)
|
||||||
|
- **Backpressure:** Automated rejection of output that doesn't pass gates
|
||||||
|
|
||||||
|
Geoffrey Huntley: *"If you aren't capturing your back-pressure then you are failing as a software engineer."*
|
||||||
|
|
||||||
|
The paradigm shift: instead of telling the agent what to do (prompt engineering), **engineer an environment where wrong outputs get rejected automatically** (backpressure engineering).
|
||||||
|
|
||||||
|
### 2. The Verification Hierarchy
|
||||||
|
|
||||||
|
JW's article establishes a strict ordering — deterministic first, agentic last:
|
||||||
|
|
||||||
|
```
|
||||||
|
Layer 1: Type System (hardest constraint, compile-time)
|
||||||
|
Layer 2: Static Analysis (linting rules, pattern enforcement)
|
||||||
|
Layer 3: Architecture Tests (dependency rules, layer violations)
|
||||||
|
Layer 4: Unit/Integration Tests (behavioral correctness)
|
||||||
|
Layer 5: Agentic Review (judgment calls — only after 1-4 pass)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical rule:** If a constraint can be checked deterministically, it MUST be checked deterministically. Relying on agentic review for things a linter could catch is "building on sand."
|
||||||
|
|
||||||
|
**Context efficiency:** Don't dump rules into CLAUDE.md that could be expressed as type constraints, lint rules, or tests. Reserve documentation for architectural intent and domain knowledge that genuinely requires natural language.
|
||||||
|
|
||||||
|
### 3. Context-Efficient Output
|
||||||
|
|
||||||
|
HumanLayer's research on context window management for coding agents:
|
||||||
|
|
||||||
|
- **On success:** Show only `✓` — don't waste tokens on 200 lines of passing test output
|
||||||
|
- **On failure:** Show the full error — the agent needs the details to self-correct
|
||||||
|
- **Fail-fast:** Enable `--bail` / `-x` / `-failfast` — one failure at a time prevents context-switching between multiple bugs
|
||||||
|
- **Filter output:** Strip generic stack frames, timing info, and irrelevant details
|
||||||
|
|
||||||
|
**Anti-pattern:** Piping output to `/dev/null` or using `head -n 50` — this hides information the agent might need and can force repeated test runs.
|
||||||
|
|
||||||
|
### 4. Claude Code Hooks
|
||||||
|
|
||||||
|
Hooks are shell commands that execute automatically at specific points in Claude Code's lifecycle:
|
||||||
|
|
||||||
|
| Event | Trigger | Use Case |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| `PreToolUse` | Before a tool runs | Block dangerous operations |
|
||||||
|
| `PostToolUse` | After a tool completes | Run compile/lint/test checks |
|
||||||
|
| `Stop` | Agent finishes response | Final validation |
|
||||||
|
| `UserPromptSubmit` | User sends a prompt | Inject context |
|
||||||
|
| `SessionStart` | Session begins | Setup checks |
|
||||||
|
|
||||||
|
**PostToolUse** is the primary backpressure mechanism: after every file edit, run deterministic checks and feed the result back to the agent.
|
||||||
|
|
||||||
|
**Configuration:** `.claude/settings.json` (project-level, committed) or `.claude/settings.local.json` (personal, gitignored).
|
||||||
|
|
||||||
|
**Hook format example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit:*.java",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "cd backend && ./mvnw compile -q 2>&1 || true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The hook output is fed back to the agent as context, enabling self-correction in the same conversation turn.
|
||||||
|
|
||||||
|
### 5. Applicable Tools for fete's Tech Stack
|
||||||
|
|
||||||
|
#### 5.1 Java / Maven Backend
|
||||||
|
|
||||||
|
**Checkstyle** (coding conventions)
|
||||||
|
- Maven plugin: `maven-checkstyle-plugin`
|
||||||
|
- Enforces formatting, naming, imports, Javadoc rules
|
||||||
|
- Rulesets: Google Style (most widely adopted), Sun Style (legacy)
|
||||||
|
- Fails build on violation when configured with `<failOnViolation>true</failOnViolation>`
|
||||||
|
- Actively maintained, open source (LGPL-2.1)
|
||||||
|
|
||||||
|
**SpotBugs** (bug detection)
|
||||||
|
- Maven plugin: `spotbugs-maven-plugin`
|
||||||
|
- Successor to FindBugs — finds null pointer dereferences, infinite loops, resource leaks, concurrency bugs
|
||||||
|
- Runs bytecode analysis (requires compilation first)
|
||||||
|
- Configurable effort/threshold levels
|
||||||
|
- Actively maintained, open source (LGPL-2.1)
|
||||||
|
|
||||||
|
**Error Prone** (compile-time bug detection)
|
||||||
|
- Google's javac plugin — catches errors during compilation
|
||||||
|
- Tighter feedback loop than SpotBugs (compile-time vs. post-compile)
|
||||||
|
- Requires `maven-compiler-plugin` configuration with annotation processor
|
||||||
|
- More invasive setup, Java version compatibility can lag
|
||||||
|
- Actively maintained, open source (Apache-2.0)
|
||||||
|
|
||||||
|
**ArchUnit** (architecture enforcement)
|
||||||
|
- Library for writing architecture rules as JUnit tests
|
||||||
|
- Built-in support for onion/hexagonal architecture via `onionArchitecture()`
|
||||||
|
- Dedicated hexagonal ruleset: [archunit-hexagonal](https://github.com/whiskeysierra/archunit-hexagonal)
|
||||||
|
- Rules: "domain must not depend on adapters", "ports are interfaces", "no Spring annotations in domain"
|
||||||
|
- Fails as a normal test — agent sees the failure and can fix it
|
||||||
|
- Actively maintained, open source (Apache-2.0)
|
||||||
|
|
||||||
|
#### 5.2 Vue 3 / TypeScript Frontend
|
||||||
|
|
||||||
|
**TypeScript strict mode** (already configured)
|
||||||
|
- `strict: true` via `@vue/tsconfig`
|
||||||
|
- `noUncheckedIndexedAccess: true` (already in `tsconfig.app.json`)
|
||||||
|
- `vue-tsc --build` for type-checking (already in `package.json` as `type-check`)
|
||||||
|
|
||||||
|
**ESLint + oxlint** (already configured)
|
||||||
|
- ESLint with `@vue/eslint-config-typescript` (recommended rules)
|
||||||
|
- oxlint as fast pre-pass (Rust-based, handles simple rules)
|
||||||
|
- Custom ESLint rules can encode repeated agent mistakes
|
||||||
|
|
||||||
|
**Vitest** (already configured)
|
||||||
|
- `--bail` flag available for fail-fast behavior
|
||||||
|
- `--reporter=verbose` for detailed output on failure
|
||||||
|
|
||||||
|
### 6. Current State Analysis (fete project)
|
||||||
|
|
||||||
|
| Layer | Backend | Frontend |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| Type System | Java 25 (strong, but no extra strictness configured) | TypeScript strict + `noUncheckedIndexedAccess` ✓ |
|
||||||
|
| Static Analysis | **Nothing configured** | ESLint + oxlint + Prettier ✓ |
|
||||||
|
| Architecture Tests | **Nothing configured** | N/A (flat structure) |
|
||||||
|
| Unit Tests | JUnit 5 via `./mvnw test` ✓ | Vitest via `npm run test:unit` ✓ |
|
||||||
|
| Claude Code Hooks | **Not configured** | **Not configured** |
|
||||||
|
| Fail-fast | **Not configured** | **Not configured** |
|
||||||
|
|
||||||
|
**Gaps:** The backend has zero static analysis or architecture enforcement. Claude Code hooks don't exist yet. Neither side has fail-fast configured.
|
||||||
|
|
||||||
|
### 7. Evaluation: What to Implement
|
||||||
|
|
||||||
|
| Measure | Effort | Impact | Privacy OK | Maintained | Recommendation |
|
||||||
|
|---------|--------|--------|------------|------------|----------------|
|
||||||
|
| Claude Code Hooks (PostToolUse) | Low | High | Yes (local) | N/A (config) | **Immediate** |
|
||||||
|
| Fail-fast + output filtering | Low | Medium | Yes (local) | N/A (config) | **Immediate** |
|
||||||
|
| Checkstyle Maven plugin | Low | Medium | Yes (no network) | Yes (LGPL) | **Yes** |
|
||||||
|
| SpotBugs Maven plugin | Low | Medium | Yes (no network) | Yes (LGPL) | **Yes** |
|
||||||
|
| ArchUnit hexagonal tests | Medium | High | Yes (no network) | Yes (Apache) | **Yes** |
|
||||||
|
| Error Prone | Medium | Medium | Yes (no network) | Yes (Apache) | **Defer** — overlaps with SpotBugs, more invasive setup, Java 25 compatibility uncertain |
|
||||||
|
| Custom ESLint rules | Low | Low-Medium | Yes (local) | N/A (project rules) | **As needed** — add rules when recurring agent mistakes are observed |
|
||||||
|
| MCP LSP Server | High | Medium | Yes (local) | Varies | **Defer** — experimental, high setup cost, unclear benefit vs. hooks |
|
||||||
|
|
||||||
|
### 8. Tool Compatibility Notes
|
||||||
|
|
||||||
|
**Java 25 compatibility:**
|
||||||
|
- Checkstyle: Confirmed support for Java 21+, Java 25 should work (runs on source, not bytecode)
|
||||||
|
- SpotBugs: Bytecode analysis — needs ASM version that supports Java 25 classfiles. Latest SpotBugs (4.9.x) supports up to Java 24; Java 25 support may require a newer release. **Verify before adopting.**
|
||||||
|
- ArchUnit: Runs via JUnit, analyzes compiled classes. Similar ASM dependency concern as SpotBugs. **Verify before adopting.**
|
||||||
|
- Error Prone: Tightly coupled to javac internals. Java 25 compatibility typically lags. **Higher risk.**
|
||||||
|
|
||||||
|
**Privacy compliance:** All recommended tools are offline-only. None phone home, none require external services. All are open source with permissive or copyleft licenses compatible with GPL.
|
||||||
|
|
||||||
|
## Decisions Required
|
||||||
|
|
||||||
|
| # | Decision | Options | Recommendation |
|
||||||
|
|---|----------|---------|----------------|
|
||||||
|
| 1 | Hooks in which settings file? | `.claude/settings.json` (project, committed) vs. `.claude/settings.local.json` (personal, gitignored) | **Project-level** — every agent user benefits |
|
||||||
|
| 2 | Checkstyle ruleset | Google Style vs. Sun Style vs. custom | **Google Style** — most widely adopted, well-documented |
|
||||||
|
| 3 | Include Error Prone in plan? | Yes (more coverage) vs. defer (simpler, overlap with SpotBugs) | **Defer** — Java 25 compatibility uncertain, overlaps with SpotBugs |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
- Anthropic: [Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks)
|
||||||
|
- Anthropic: [2026 Agentic Coding Trends Report](https://resources.anthropic.com/hubfs/2026%20Agentic%20Coding%20Trends%20Report.pdf)
|
||||||
|
- ArchUnit: [User Guide](https://www.archunit.org/userguide/html/000_Index.html)
|
||||||
|
- ArchUnit Hexagonal: [GitHub](https://github.com/whiskeysierra/archunit-hexagonal)
|
||||||
|
- SpotBugs: [Documentation](https://spotbugs.github.io/)
|
||||||
|
- Checkstyle: [Documentation](https://checkstyle.sourceforge.io/)
|
||||||
|
- Claude Code Hooks Guide: [Luiz Tanure](https://www.letanure.dev/blog/2025-08-06--claude-code-part-8-hooks-automated-quality-checks)
|
||||||
|
- lsp-mcp: [GitHub](https://github.com/jonrad/lsp-mcp)
|
||||||
@@ -9,6 +9,7 @@ export default mergeConfig(
|
|||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||||
|
bail: 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user