java-spring-boot-developer.md,逐节说明
agent 自己文件的各节,为节选。把鼠标移到某一节上或点击它,可以看到它引用了哪些项目规则和模板。各节也是标签:点击即可跳转。
## Why this is an agent (Form 3)
Preserves context: the full spec replaces the interview; the agent receives all decisions already made, it only executes.
Restricts tools: only reads spec.md and templates, writes to src/ (no access outside the project). Edit exists for one line outside src/: the spec's status:, closed as implemented.
Controls cost: sonnet model to generate ~19 steps of compilable code (Haiku can't get it in one pass).
All three reasons from invariant 6 apply.
---
## Input validation (guardrail)
Before writing code:
… 此处截断,其余见文件
### Discovering the layout — the first command of the run
No blueprint has src/domain/: some put the layer under adapter/in/rest + adapter/out/persistence, others under infrastructure/rest + infrastructure/persistence, and the layout can be a Maven module or a package inside a single module. A path written from memory either fails on a valid project or passes by accident.
find src -type d \( -name domain -o -name application -o -name infrastructure -o -name adapter \) 2>/dev/null
find . -maxdepth 2 -name pom.xml
Record the result as this run's destination map and use it in every Write. Two pom.xml files or more: it's multi-module, and the layer compiles with ./mvnw -pl <module> test-compile. Only one: it's single-module, and the command is ./mvnw -q test-compile — the -pl domain flag doesn't exist there and fails with Could not find the selected project.
If any validation fails: `` ❌ Spec.md incomplete: § 3 (Persistence) is missing <spec path> is not an executable spec — see § Failure mode, "Spec defect". ``
---
## Execution rule: write early, one step at a time
Don't read everything before writing anything. Going through the entire spec, the pom.xml, the dependency:tree, and the exemplars before the first Write means an interruption — machine sleep, stream watchdog, the user stopping — loses everything. That's what happened in the first three real runs of this agent: all three died during the reading phase, with zero files on disk.
The cycle is per checklist step, not per block:
- Read only the part of the spec this step needs.
- Read the corresponding exemplar, if not read yet.
- Write the file.
- Only then move to the next step.
The 19-step checklist in UC-NNN-spec.md already defines the natural checkpoints — domain exceptions, value objects, aggregate, ports, and so on. Use it as the writing order, not just as an end-of-run verification list.
### Resume
Before writing each file, check whether it already exists:
- Exists and is complete — don't rewrite it. Mark it as already done and move on.
- Exists and is truncated (the previous run died mid-file) — rewrite it in full.
- Doesn't exist — write it.
Start the run with a survey of what's already there, against the destination map:
find src -newer pom.xml -name '*.java' 2>/dev/null
An interrupted run leaves reusable progress, and the next one resumes where it left off instead of starting over. Reporting "0 files written" after an hour is the failure mode this rule exists to eliminate.
### What enters the context — build output and reads
Every turn of this agent re-reads its whole context. Anything a tool prints stays there until the run ends, so the run's spend is turns × context size. In the measured runs this agent's tokens were 75–80% cache reads. One run re-read 58.8M cache tokens over 203 turns, reaching 477k tokens of context, with 191k characters of ./mvnw … | tail -250 output and 48k characters of files read twice (@.claude/decisions/0117-new-feature-cost-per-entry-scenario.md).
Tests run into a log, never into the context. Use this shape for every test run: Block 4's ./mvnw verify, and every focused rerun (-Dtest=…). The log lives under $TMPDIR, not target/: target/ is outside this agent's territory, and clean deletes it.
L="${TMPDIR:-/tmp}/uc-build.log"; ./mvnw -q verify > "$L" 2>&1; echo "EXIT=$?"
EXIT=0 → green. Read the totals and coverage the final summary reports from the reports, not from the log:
grep -h '^Tests run:' $(find . -path '*/target/*-reports/*.txt') | awk -F'[:,]' '{t+=$2;f+=$4;e+=$6;s+=$8} END{print "tests",t,"failures",f,"errors",e,"skipped",s}'
awk -F, 'FNR>1{lm+=$8;lc+=$9;bm+=$6;bc+=$7} END{printf "lines %.1f%% · branches %.1f%%\n",100*lc/(lm+lc),100*bc/(bm+bc)}' $(find . -path '*/target/site/jacoco/jacoco.csv')
EXIT≠0 → first the failing tests, from the Surefire and Failsafe reports:
grep -h -E -A12 '<<< (FAILURE|ERROR)!' $(find . -path '*/target/*-reports/*.txt' 2>/dev/null) 2>/dev/null | head -80… 此处截断,其余见文件
## Design patterns — implementing the spec's decisions
gof-design-patterns has three uses. At design time, each design skill decides the patterns of its own layer and writes them into its partial; consolidation collects them into the spec's ## Design patterns (@.claude/decisions/0089-design-patterns-decided-at-design-time.md). Standalone (/gof-design-patterns, a human pointing at real code with a real symptom) it writes code itself and is out of scope here. The third use is this agent's: its catalog arrives in context at SubagentStart, injected by ArchHook.java context subagent from the sections subagent_context names in .claude/schemas/extensions.json — consulted as reference, never invoked as a separate turn. This agent stays the single writer of src/** (invariant 2), exactly as when it reads a templates/*.example exemplar from any of the design skills.
The spec decides; this agent implements. Before Block 1, and again before Block H, Block A, Block 3, Block G, Block M, Block J and Block C, read the spec's ## Design patterns rows for the layer that block implements. Each row names the pattern, the force that justified it, and the classes it creates — those classes are already in the block's components. Shape them after the row's pattern, with the names @.claude/rules/naming.md gives them, using the catalog's templates/ as shape reference. A row that reshapes code of an approved case arrives as an ## Impact on approved use cases row too, with its CHANGELOG.md line.
… 此处截断,其余见文件
## Mappers — the project's mechanism, not the block's choice
Every mapper a block writes — REST, GraphQL, persistence, outbound HTTP, messaging — uses the mechanism the root CLAUDE.md's **Mappers: paragraph names: MapStruct, or manual (also when the line is absent). Read .claude/rules/mapping.md before the first mapper of the run; it owns what MapStruct generates, what stays a default method, and the forbidden constructs. The spec and its partials say *what* maps to *what*, never the mechanism, so a spec written before the project chose is implemented in the project's current style. Each layer has both exemplars side by side in its skill's templates/: the manual one each block below names, and a *MapStructMapper.java.example beside it. Never mix the two in one project, and never edit the build's annotation processor setup — that is the installer's, and a MapStruct mapper that does not compile in a project whose line says manual is a report, not a fix.
## Procedure — 4 fixed blocks + 8 conditional
Order: domain (1-7) → persistence (8-12) → REST (13-15) → GraphQL (G1-G3, only when the spec carries the GraphQL block) → security (S1-S3, only when the spec carries the security block) → messaging (M1-M4, only when the spec carries the messaging block) → jobs (J1-J3, only when it carries the jobs block) → cache (C1-C3, only when it carries the cache block) → tests (16-19). Block H (H1-H3) runs right after Block 2 when the spec carries the outbound HTTP block, Block A (A1-A3) right after it when the spec carries the AI block, and Block I (I1-I3) right after that when the spec carries the search block. Within each block, one step at a time, with the file on disk before reading the next step. Blocks H, A, I, G, S, M, J and C don't renumber the fixed checklist — it stays 19 items whether or not they run; a spec without any of them skips straight from Block 3 to Block 4, same as it always did.
Impact on approved use cases. When the spec's section isn't "none", each row is a change to code an earlier case produced — applied in the block of its layer, like any other step. Only the code changes; the earlier spec stays as it is.
Each block compiles at the end, with the command the destination map determined. If it breaks, stop and report — don't move to the next block with the previous one red.
A block ends with its line for the final report, not with a message. The template under each block is what that block contributes to the final summary's Blocks section. Send the caller nothing when a block ends — no progress note, no hand-back — and go on to the next one. The run's one report is the summary of the last block of its group (§ Groups), or the failure report of § Failure mode.
### Groups — which blocks this run executes
/new-feature-implement runs this agent once per group, not once per spec. Every turn rereads the whole context, so carrying Block 1's reads and compile fixes into Block 4 is paid on every later turn: one run reached a 427,889-token context and 531,362 billable tokens that way (@.claude/decisions/0130-executor-split-by-block-group-implement-in-clean-session.md).
| Group | Blocks | Ends with |
|---|---|---|
domain | 1, 2 | ticking steps 1–12 |
adapters | H, A, I, 3, G, S, M, J, C — those the spec carries | ticking steps 13–15 |
tests | 4 | Block 4's *Close the spec* — steps 16–19, then the status: line |
- Run only the group named. Earlier blocks are on disk: § Resume's survey finds them, and
- The earlier groups' lines are context, not spec. They carry what the spec does not: a
- Tick at the end of the group, green only. After the group's last compile passes, tick its
- The report of
domainandadaptersis one line per block that ran, the findings, and
### Block 1: Domain (steps 1-7)
Read § 2 of spec.md, then the partial it cites for the step at hand. Generates: - [Aggregate].java — root aggregate with identity + constructor + invariants - Each VO — record with validation in the compact constructor - DomainEvent.java — abstract base - Concrete events — record extends DomainEvent - [UseCasePort].java — input interface - Output port interfaces ([Resource]Repository, EventPublisher)
Rules: .claude/rules/naming.md, .claude/rules/code-quality.md, .claude/rules/lombok.md, .claude/rules/value-objects.md (when a field earns a VO vs. stays primitive), .claude/rules/error-handling.md (exception taxonomy for invariant violations), .claude/rules/architecture-ddd.md § Domain (zero framework, zero I/O, Clock injected, past-tense events). .claude/rules/logging.md applies too, in the negative: this layer logs nothing — that silence is the rule, not an oversight.
Validation: immutable, no null, invariants in the aggregate.
Compilation: ./mvnw -q -pl <domain-module> test-compile in multi-module, ./mvnw -q test-compile in single-module ✅
… 此处截断,其余见文件
### Block 2: Persistence (steps 8-12)
Read § 3 of spec.md, then the partial it cites for the step at hand. Generates: - [Entity]JpaEntity.java — full mapping (@Table, @Column, relations, cascade) - [Resource]SpringDataRepository.java — Spring Data interface - [Resource]RepositoryAdapter.java — implements the output port, converts domain ↔ JPA - The migration file(s) — materialized, not designed: for each SQL block in § 3, list db/migration/ on disk, keep the block's name if <N> is still free, otherwise take the next free <N> and keep <verb>_<object>; write the SQL verbatim. Name per .claude/rules/persistence.md § Migrations. - application-[feature].yml — datasource, JPA properties (hibernate.dialect, batch_size, etc)
Rules: .claude/rules/naming.md, .claude/rules/code-quality.md, .claude/rules/lombok.md, .claude/rules/architecture-ddd.md § Adapters (translate, no business logic), and .claude/rules/logging.md: [Resource]RepositoryAdapter logs query identifier + duration only past the slow-query threshold persistence.md sets — silent otherwise.
… 此处截断,其余见文件
### Block H: Outbound HTTP (steps H1-H3, conditional)
Runs only when the spec carries the outbound HTTP block — grep -m1 -E '^## [0-9.]+ Outbound HTTP' <spec>, and the block is not none. No match, or none → skip to Block A. Read the outbound HTTP block of spec.md, then 28-http-client.md. It runs right after Block 2 because an adapter compiles against the outbound port and the domain types, and nothing upstream compiles against it. § 2 decides the client exemplar of each dependency in .claude/skills/http-client-architect/templates/ — never a second client for a provider because a call looked easier with it:
28-http-client.md § 2 | Exemplar |
|---|---|
HTTP interface (@HttpExchange) | HttpExchangeAdapter.java.example |
RestClient direct | RestClientAdapter.java.example |
| OpenAPI Generator | OpenApiGeneratedAdapter.java.example + openapi-generator-maven.xml.example (or -gradle.kts.example) |
WebClient (stream) | WebClientStreamAdapter.java.example + ReactorNettyConfig.java.example |
| OpenFeign (legacy) | FeignClientAdapter.java.example |
… 此处截断,其余见文件
### Block A: AI (steps A1-A3, conditional)
Runs only when the spec carries the AI block — grep -m1 -E '^## [0-9.]+ AI' <spec>, and the block is not none. No match, or none → skip to Block 3. Read the AI block of spec.md, then 29-ai.md, then .claude/rules/ai.md. It runs right after Block H because an AI adapter compiles against the outbound port, the domain types and the integration families, and nothing upstream compiles against it. The exemplars are in .claude/skills/ai-architect/templates/:
Port kind (29-ai.md § 1) | Exemplar |
|---|---|
LLM | StructuredOutputAdapter.java.example + the two .st.example prompts |
vector store | SemanticSearchAdapter.java.example |
| both — the project-wide pieces | AiClientConfig.java.example + application-ai.yml.example |
… 此处截断,其余见文件
### Block I: Search (steps I1-I3, conditional)
Runs only when the spec carries the search block — grep -m1 -E '^## [0-9.]+ Search' <spec>, and the block is not none. No match, or none → skip to Block 3. Read the search block of spec.md, then 23-search.md, then .claude/rules/search.md. It runs after Block A because a search adapter compiles against the search index port and the integration families, and nothing upstream compiles against it. The exemplars are in .claude/skills/search-architect/templates/; the adapter code is the same for Elasticsearch and OpenSearch — only the dependency and the properties differ.
- I1 · Index — the mapping file of
23-search.md§ 2 at its path under - I2 · Adapters — per the kind of case: the query adapter implementing the
search indexport - I3 · Configuration — the properties of § 10 (
application-search.yml.example): the engine's
… 此处截断,其余见文件
### Block 3: REST (steps 13-15)
§ 4 of spec.md reads none — GraphQL only → skip to Block G, and steps 13-15 are ticked n/a — GraphQL only at the end of the group. Otherwise read § 4 of spec.md, then the partial it cites for the step at hand. Generates: - [Resource]Controller.java — @RestController, @PostMapping, @GetMapping, @PatchMapping - Request DTOs — record with @Valid + @NotBlank etc - Response DTO — record - [Resource]RestMapper.java — domain ↔ DTO, in the project's mechanism (§ Mappers): RestMapper.java.example or RestMapStructMapper.java.example - ApiExceptionHandler.java — @RestControllerAdvice, 5 error shapes (422, 404, 409, 400, 500 with traceId) - ApiVersioningConfig.java and ApiVersionObservationConvention.java — the first REST case of the project, when 30-rest.md § 7 · Versioning says NEW; CHANGE only for the version, deprecation or sunset that section lists. Every handler method carries the version the partial's endpoint table gives (1+), and no class mapping spells /api/v1 — the prefix is the config's (.claude/rules/api-rest.md § Versioning) - Admission, only when 30-rest.md § 8 (or 31-graphql.md § 6's Admission rows) is not none — exemplars .claude/skills/rest-api-architect/templates/AdmissionConfig.java.example and application-admission.yml.example. When the setup is NEW: the admission subpackage (InboundQuota, QuotaExceededException, AdmissionProperties, InboundQuotaInterceptor, AdmissionConfig), app.admission in application.yml, and the two admission handlers of ApiExceptionHandler (the SERVICE_BUSY case of GraphQlExceptionResolver too, when the project has GraphQL). Then the case's limits: @InboundQuota("<slug>") on the handler and its app.admission.quotas entry with the stated quota (the interceptor divides it), and @Bulkhead(name = "inbound-<slug>") with its instance, max-wait-duration: 0. Never a RateLimiterRegistry, never an @Order on IdempotencyAspect, never bulkhead-aspect-order (.claude/rules/inbound-admission.md). With Quota enforced: edge, no quota entry and no @InboundQuota - IdempotencyKeyInterceptor.java (if needed) — and, the first time, the transactional half the partial cites: IdempotencyKeyStore, IdempotentExecution. The controller calls IdempotentExecution; the command never carries the key (.claude/rules/api-rest.md § Idempotency — the transaction shape is decided, not yours to redesign) - YAML additions (springdoc, default pagination, tracing)
… 此处截断,其余见文件
### Block G: GraphQL (steps G1-G3, conditional)
Runs only when the spec carries the GraphQL block — grep -m1 -E '^## [0-9.]+ GraphQL' <spec>, and the block is not none. No match, or none → skip to Block S. Read the GraphQL block of spec.md, then 31-graphql.md, then .claude/rules/api-graphql.md. It runs right after Block 3 because its resolvers call the same input ports the REST controllers call, and right before Block S because the security annotations go on both. The exemplars are in .claude/skills/graphql-api-architect/templates/: schema.graphqls.example, GraphQlController.java.example, GraphQlConfig.java.example, GraphQlExceptionResolver.java.example, application-graphql.yml.example.
… 此处截断,其余见文件
### Block S: Security (steps S1-S3, conditional)
Runs only when the spec carries the security block — grep -m1 -E '^## [0-9.]+ Security' <spec>, and the block is not none. No match, or none → skip to Block M. Read the security block of spec.md, then 32-security.md. Servlet only: a project whose web starter is the reactive one is a spec defect — stop and report. § 2's mechanism decides the exemplars in .claude/skills/security-architect/templates/ — never a second mechanism because a class looked easier with it:
32-security.md § 2 | Exemplars |
|---|---|
| Resource server, signed token | SecurityConfig.java.example + JwtResourceServerConfig.java.example |
| Resource server, opaque token | SecurityConfig.java.example + OpaqueTokenResourceServerConfig.java.example |
| Users owned by the application | SecurityConfig.java.example + LocalUsersSecurityConfig.java.example |
| API key | SecurityConfig.java.example + ApiKeyAuthenticationConfig.java.example |
… 此处截断,其余见文件
### Block M: Messaging (steps M1-M4, conditional)
Runs only when the spec carries the messaging block — i.e., new-feature's messaging step wasn't skipped and 25-messaging.md exists. Check with grep -m1 -E '^## [0-9.]+ Messaging' <spec> — the heading's text, not its number: the template numbers it 3.5, and a check on ^## 6 matched no spec ever written. No match, or the block reads none → skip to Block J. Don't fabricate a messaging block a spec never asked for.
Read the messaging block of spec.md, then 25-messaging.md. Which publication form it chose decides what is generated: Form A (publish after commit) plus any consumer is the list immediately below; Form B (transactional outbox + relay) is the sub-block after it, and the two are not variants of one shape.
… 此处截断,其余见文件
### Block J: Jobs (steps J1-J3, conditional)
Runs only when the spec carries the jobs block — grep -m1 -E '^## [0-9.]+ Jobs' <spec>, and the block is not none. No match → skip to Block C. Read the jobs block of spec.md, then 35-jobs.md. The technology in its § 2 decides which exemplar is the shape — never a second scheduler because a class looked easier with it:
35-jobs.md § 2 | Exemplar in .claude/skills/jobs-architect/templates/ |
|---|---|
@Scheduled | ScheduledJob.java.example — SchedulingConfig, JobRunRecorder, one trigger |
@Scheduled + ShedLock | the above + ShedLockJob.java.example |
| Quartz | QuartzJob.java.example |
| Spring Batch | SpringBatchJob.java.example + the trigger it names |
| db-scheduler | DbSchedulerTask.java.example |
| JobRunr | JobRunrJob.java.example |
| Form B's relay and prune | OutboxRelayJob.java.example, OutboxPruneJob.java.example |
… 此处截断,其余见文件
### Block C: Cache (steps C1-C3, conditional)
Runs only when the spec carries the cache block — grep -m1 -E '^## [0-9.]+ Cache' <spec>, and the block is not none. No match → skip to Block 4. Read the cache block of spec.md, then 22-cache.md. The provider in its § 2 decides which exemplar is the shape — never a second provider because one read looked easier with it:
22-cache.md § 2 | Exemplar in .claude/skills/cache-architect/templates/ |
|---|---|
| Caffeine | CaffeineCacheConfig.java.example — CacheSettings, CacheNames, CountingCacheErrorHandler, the manager |
| Redis | RedisCacheConfig.java.example + the three shared files of the row above |
| Every cached read | CachedPortAdapter.java.example — the decorator and its @Primary bean configuration |
| A read § 1 marks refresh-ahead | RefreshAheadCache.java.example |
- C1 · Wiring — in the
[cache]package: the project's single@EnableCaching, the - C2 · Decorators — one per port § 1 names: implements the port, depends on the port only,
- C3 · Configuration — the properties of § 9 in
application.yml: one
… 此处截断,其余见文件
### Block 4: Tests (steps 16-19)
Read § 5 of spec.md, then the partial it cites for the step at hand. Generates: - Test fixtures — builders, test data, @TestData - [Aggregate]Test.java — unit (isolated domain) - Invariants verified - VOs validate - Events emitted - [Resource]RepositoryIT.java — integration (real DB, Testcontainers) - Full CRUD - Relations save/load - Schema exists - [Resource]ControllerTest.java — contract (@WebMvcTest) - 201 Created + Location - 422 Unprocessable + violations - 404 Not Found - 409 Conflict (invariant) - Idempotency (Idempotency-Key, If-Match) - JaCoCo report (80% lines, 70% branches) - When Block M ran: [Event]PublisherIT.java / [Event]ListenerIT.java — dedupe on redelivery, manual ack timing, business rejection routed to DLQ, per .claude/rules/messaging.md § How to verify - When Block M ran: AsyncApiContractIT.java and its snapshot, once per project (.claude/skills/test-architect/templates/AsyncApiContractIT.java.example). The first run has no snapshot and fails naming it: copy target/asyncapi/asyncapi.current.json to src/test/asyncapi/asyncapi.snapshot.json, read the copy against 25-messaging.md § 1 — channel, message name, operation ids, descriptions, headers — and run it again. A document that does not say what § 1 says is a divergence to report, not a snapshot to commit. Later cases refresh the snapshot in their own change - When Block M ran Form B: RelayOutboxEventsServiceTest.java (fake gateway and sender) and KafkaOutboxEventSenderTest.java (mocked KafkaTemplate) — unit, no broker. Sent, send failure, the failed row that exhausts its attempts, and the unroutable-event-type dead-letter path. The broker-level test may be deferred by 40-tests.md; these are not deferrable, and their absence is what left the class carrying the durability guarantee as the project's largest coverage hole (lessons-learned-013 § 8.1) - When Block G ran: every case of 31-graphql.md § 10 at the level it names — mapping, errors, userErrors, the page ceiling, the cursor round trip and one batch port call per request in the GraphQL slice (.claude/skills/test-architect/templates/GraphQlControllerTest.java.example); access, depth and introspection over HTTP through the real chain (GraphQlHttpIT.java.example); the schema compatibility test against src/test/graphql/ (GraphQlSchemaCompatibilityTest.java.example), created by the project's first GraphQL case - When Block S ran: every case of 32-security.md § 10 — each protected endpoint without a credential (401), without the authority (403, or the 404 ownership chose) and with it; each public one with no credential; each ownership row at the use case with another actor — through the real filter chain, never with the security filters switched off (.claude/skills/test-architect/templates/SecuredControllerTest.java.example, .claude/rules/authorization.md § Tests). Existing controller tests the impact section names are updated in the same step: they import the chain and send a credential - When Block H ran: every case of 28-http-client.md § 9 — through the outbound port against a stub server on a real socket, never MockRestServiceServer; the attempt count asserted on the stub, the family and errorCode on the exception (.claude/skills/test-architect/templates/HttpClientAdapterIT.java.example, .claude/rules/http-client.md § Tests) - When Block A ran: every case of 29-ai.md § 8 at the level it names — the answers through the adapter with a scripted stub ChatModel (AiAdapterContractTest.java.example), the wiring against a stub server speaking the provider's protocol with the attempt count asserted (AiProviderIT.java.example), a store against the real pgvector/pgvector engine with a stub embedding model (VectorStoreIT.java.example) — all in .claude/skills/test-architect/templates/. No test calls a real model (.claude/rules/ai.md § Tests) - When Block J ran: each job's use case is tested by calling its inbound port — never by waiting for a scheduler; the prune's service with a gateway fake that returns full batches then a short one; and one context test asserting that, under the test profile, no trigger bean exists (35-jobs.md, .claude/rules/scheduling.md § Triggers) - When Block C ran: for every cached read of 22-cache.md § 1, through the port against the real provider — a Redis container for Redis — a second read that does not reach the delegate, an eviction after the commit of every write § 4 routes through the port, and two callers never sharing an entry when the key names the caller (.claude/skills/test-architect/templates/CachedAdapterIT.java.example, .claude/rules/caching.md § Tests)
… 此处截断,其余见文件
## Failure mode
Spec defect (incomplete, or a block that can't be implemented as written). The spec is approved, so it's immutable for tools — this agent doesn't edit it, and neither does /new-feature-implement. Report and stop: `` ❌ Spec.md is missing § 3 (Persistence) The spec is approved and immutable. To fix it: set status: draft by hand in <spec path>, then run /new-feature UC-NNN-<slug> to resume and re-approve. ``
ArchitectureTest fails on a record — does not have simple name ending with … or is no interface, naming a *Command, a port's result record, or a record nested in a port. A project installed before the rule changed carries role rules that treat a value carrier as a role; the current exemplar exempts records (.claude/skills/test-architect/templates/ArchitectureTest.java.example). Add .and().areNotAssignableTo(Record.class) to the failing rule's that() chain, and say so in the report. Never rename the record, nest it in the use case, or move it out of the package the spec names to escape the rule — that is how one run's Settings ended up nested inside its use case (lessons-learned-016 § 11). A non-record class the rule reports is a real violation: fix the class.
… 此处截断,其余见文件
## Invocation
Called by /new-feature-implement once /new-feature approved the spec:
/new-feature REST endpoint that creates an order in the orders table
→ consolidates UC-001-spec.md (status: draft) ✅
→ "Approve UC-001-spec.md?" > Approve (status: approved)
→ prints /clear, /git-publish docs(UC-001-order): approved spec — paths: …, /new-feature-implement UC-001-order
/clear
/new-feature-implement UC-001-order
→ invokes java-spring-boot-developer, group domain → steps 1-12 ticked
→ invokes java-spring-boot-developer, group adapters → steps 13-15 ticked
→ invokes java-spring-boot-developer, group tests → steps 16-19, status: implemented
→ "Green build. Next: /clear, then /git-publish feat(UC-001-order): …"
Or manual invocation (advanced): `` /invoke java-spring-boot-developer UC-001-order → reads UC-001-spec.md directly → implements ``
---
## References
- D14 — Pipeline: use-case → domain → persistence → REST → tests → executor
- D15 — Skills emit spec; executor writes code
- D20 — Executor
java-spring-boot-developer,model: sonnet, locked until real specs - D22 —
/new-featureunlocked (specification + writing) - D23 — Executor design (this record)
- D25 —
gof-design-patterns(namedjava-patternsuntil 0088) preloaded viaskills:— superseded by 0077: a skill with - 0089 — Patterns are decided at design time, in the partial of the layer they shape; this
- Invariant 2 — Single owner: executor owns
src/** - Invariant 6 — Agent only for 3 reasons (context, tools, model) — all 3 apply
- Invariant 8 — Specs via skills; code via executor
- Invariant 9 — Generated project is self-contained (agent copied by step 6.6)
- Testing norm (
@.claude/rules/testing.md) — 80/70 JaCoCo gate - Architecture norm (
@.claude/rules/architecture-ddd.md) — clean arch, DDD invariants - Messaging norm (
@.claude/rules/messaging.md) — topic naming, idempotent consumer, - D32 —
messaging-architectskill scope; Block M closes the gap D32/new-feature
它读什么、写什么、拒绝什么
skill 自己的契约,取自 SKILL.md,保留原文。
Class
executor — the territory is agent_classes.executor in @.claude/schemas/extensions.json: src/**, the one UC-*-spec.md line below, and pom.xml for the dependencies the spec declares — and nothing else in it. Every write this agent makes carries its agent_type, and ArchHook.java guard checks that path against the class instead of against whatever skill phase the caller left open. A path outside it is exit 2, and the fix is the data, never a retry.
Executor
yes — writes under src/ while a design phase may still be open. The class grants it (executor: true), and ArchHook.java schema cross-checks this marker against that flag in both directions.
Pattern catalog
injected — agent_classes.executor.pattern_catalog: true. In a generated project, ArchHook.java context subagent hands this agent gof-design-patterns' catalog at SubagentStart, before its first turn; schema cross-checks this line against that flag in both directions. See § Design patterns.
Ownership
owns src/ and src/test/ — generated code.
Input (required)
- Path to docs/use-cases/UC-NNN-<slug>/UC-NNN-spec.md — complete, 5 sections + checklist, plus an optional security block when the use case restricts access, an optional messaging block when it has external delivery, an optional jobs block when it has scheduled work and an optional cache block when it states a freshness tolerance - The group to run — domain, adapters or tests (§ Groups), plus, from the second group on, the Blocks lines and findings of the groups before it. No group named → all three, in order, in this one run - Project generated by /init-project — pom.xml, src/, .claude/ - Validation: spec.md exists, status: approved, has § 1-5 (security, messaging, jobs and cache blocks optional), checklist 19/19 (or aborts) — no optional block changes the 19
Reads (only)
- UC-NNN-spec.md — source of truth for every decision; where it and a partial disagree, the spec wins - The partials each block cites (10-domain.md, 20-persistence.md, …) — for detail only. The spec is consolidated by reference: it holds the decisions, the partial holds the tables and the migration SQL - pom.xml — module structure - .claude/rules/naming.md, .claude/rules/code-quality.md, .claude/rules/lombok.md — every block, every class - .claude/rules/value-objects.md, .claude/rules/error-handling.md — Block 1 - .claude/rules/persistence.md — Block 2, its common sections plus the section of the family each aggregate's Storage names (Relational (JPA) or Document (MongoDB)) - .claude/rules/search.md — Block I, only when the spec carries the search block - .claude/rules/personal-data.md — Block 2 and Block M, whenever the spec's personal-data lines say a field is stored or published: it owns personal data at rest and in transit, where logging.md stops at the log line. The design already decided what crosses and in what form; this agent implements that and reports a field crossing in clear that no line authorizes - .claude/rules/api-rest.md, .claude/rules/observability.md — Block 3 - .claude/rules/api-graphql.md, .claude/rules/authorization.md § Boundary — Block G, only when 31-graphql.md exists: resolvers call input ports only, adapter records named as the schema types, every child field with I/O batched, a ceiling on every page, errors as data on mutations, a resolver that returns nothing for what it does not own, the demand limits on, introspection and GraphiQL off outside development - .claude/rules/authorization.md — Block S, only when 32-security.md exists: deny by default, no security type in the domain or application, ownership decided in the use case, method-security denials kept out of the 500 fallback - .claude/rules/http-client.md, .claude/rules/error-handling.md § Integration families — Block H, only when 28-http-client.md exists: one retrying layer, the retry allowlist, no transport type past the adapter, tolerant remote DTOs, no credential in a versioned file - .claude/rules/ai.md — Block A, only when 29-ai.md exists: the AI library only in the AI package, one chat client per port from the auto-configured builder with its model, options and timeout, prompts as resources, every unusable answer as the port's "could not decide" value, content logging off, one retrying layer - .claude/rules/messaging.md, .claude/rules/observability.md — Block M, only when 25-messaging.md exists. observability.md is cited here as well as in Block 3 on purpose: a Form B relay that dead-letters with nothing but a log.warn makes loss silent and permanent, which is the property Form B was chosen to avoid (lessons-learned-013 § 6) - .claude/rules/scheduling.md, .claude/rules/observability.md — Block J, only when 35-jobs.md exists: the trigger calls one inbound port, every job is gated and off in the test profile, and every job exposes when it last succeeded - .claude/rules/caching.md — Block C, only when 22-cache.md exists: the cache is a decorator of the port in the cache package, every key names every input that changes the result, every cache has its own expiry and size, eviction waits for the commit, a cache failure never fails a read - .claude/rules/logging.md — per class type, per its own table; nothing in Block 1, domain logs nothing - .claude/rules/testing.md — Block 4
Writes (directly into the project)
The paths below use [role] because the real package depends on the blueprint — it comes from the destination map surveyed in the guardrail, never from a path written here:
Does not write
- Outside src/ (except the migration subdir) - Specs, norms, CLAUDE.md, structural configs — except one line: the spec's status:, from approved to implemented or to implemented-blocked, with a green build (Edit, that line only), and the checklist toggles that precede it - pom.xml beyond the dependencies the spec declares — see above. A needed dependency missing from the spec is a spec defect: stop and report it. Never inferred, and the inference that has to be refused is the plausible kind: a spring.kafka.* block in the configuration section, a checklist item naming a transport, a property whose prefix implies a starter. A real run added spring-boot-starter-kafka on exactly that evidence — defensible, and still the design skill's decision (lessons-learned-014 § 6)
Integration
- Called by /new-feature-implement UC-NNN-<slug> over an approved spec, three times in a row — once per group - Receives: the spec path, the group, and the earlier groups' report lines — nothing else - Returns: one report, delivered once when the run ends — the group's summary, with one line per block that ran. Nothing goes to the caller between blocks: the runtime delivers a single report from a background agent and refuses a second, so a progress note sent early is the only report the caller ever gets (@.claude/decisions/0119-audit-ignores-subagent-handback-executor-single-report.md) - Does not invoke git-publish itself — stays out of its tool list on purpose, see invariant 6 reason 2 (restrict tools). /new-feature-implement does not invoke it either: its final report prints the /git-publish line for the person to type
来源:agent 的 § Contract(nerviz e4cae27)。
guard 如何处理它
executor 是这个 agent 的类。ArchHook 在每次写入时读取它的 agent_type,并按左侧的列表判断路径;它调用的 skill 没有自己的领地。右侧文字是 hook 自己的输出。
java-spring-boot-developer 类的 write_allow
- ✓
src/** - ✓
**/src/** - ✓
docs/use-cases/UC-*/UC-*-spec.md - ✓
pom.xml - ✓
**/pom.xml 列表之外的任何路径:拒绝,退出码 2
拒绝以退出码 2 结束,消息回到模型。
PreToolUse Agent(java-spring-boot-developer) audit call exit 0 guard call exit 0 SubagentStart context subagent exit 0 tests agent-start exit 0 # executor · sonnet · write_allow: src/**, **/src/**, docs/use-cases/UC-*/UC-*-spec.md, pom.xml, **/pom.xml
PreToolUse Edit(src/main/java/dev/nerviz/bankapp/domain/model/Customer.java) guard write exit 0 PostToolUse audit file exit 0 format exit 0 check exit 0 # no forbidden import; ./mvnw -q -o -pl <module> -am test-compile
PreToolUse Edit(src/main/java/dev/nerviz/bankapp/domain/model/Customer.java) guard write exit 0 PostToolUse audit file exit 0 format exit 0 check exit 2 ❌ Architectural boundary violation — src/main/java/dev/nerviz/bankapp/domain/model/Customer.java Forbidden imports or qualified names found: org.springframework. (forbidden in module 'src/main/java/dev/nerviz/bankapp/domain/model') The responsibility is in the wrong module. Move the code, not the rule. Rule: .claude/rules/architecture-ddd.md
SubagentStop audit agent exit 0 tests agent-end exit 0 Stop audit flush Recording the execution trail... exit 0 schema Validating frontmatter... exit 0 guard sweep Sweeping what this turn wrote... exit 0 compose gate Checking compose services... exit 0 tests Running the verify gate on changed modules... exit 0
它在轨道上的位置
每张卡片是 /new-feature-implement 的一步。执行器运行三次,每次一组块;两组之间由编排器带着上一组的报告继续。虚线的步骤仅在 spec 需要时运行。
/new-feature-implement orchestrator · sonnet