bmad-method-quarkus 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method-quarkus",
4
- "version": "1.0.6",
4
+ "version": "1.0.7",
5
5
  "description": "BMAD Method with Quarkus support",
6
6
  "keywords": [
7
7
  "agile",
@@ -56,7 +56,7 @@ These 9 standards are installed at `.claude/skills/` and auto-trigger on their o
56
56
  | `quarkus-kafka-messaging` | Domain events — the outbox row, the relay mode, a `Consumer`, DLQ, idempotency |
57
57
  | `quarkus-observability-otel` | `@WithSpan` on `process()`, `trace_id`/`span_id` in logs, the `audit_event` context, metrics |
58
58
  | `quarkus-security-standards` | Any credential/URL/API key, `.env`/`.env.example`, `application.properties` secrets, OIDC/JWT auth, CORS/TLS, or a security review |
59
- | `quarkus-temporal-workflows` | **Temporal projects only** — a saga/compensation, a `@WorkflowInterface`/`@ActivityInterface`, the worker bootstrap, a multi-slice process one transaction can't make atomic, or scaffolding a **worker-only deployable** (no REST, no slices — it overrides the slice layout) |
59
+ | `quarkus-temporal-workflows` | **Temporal projects only** — a saga/compensation, a `@WorkflowInterface`/`@ActivityInterface`, `quarkus.temporal.*` configuration, a multi-slice process one transaction can't make atomic, or scaffolding a **worker-only deployable** (no REST, no slices — it overrides the slice layout) |
60
60
 
61
61
  ## On Activation
62
62
 
@@ -98,7 +98,7 @@ Read the eight universal Quarkus standards **in full** before doing any work. Th
98
98
  6. `{project-root}/.claude/skills/quarkus-kafka-messaging/SKILL.md`
99
99
  7. `{project-root}/.claude/skills/quarkus-observability-otel/SKILL.md`
100
100
  8. `{project-root}/.claude/skills/quarkus-security-standards/SKILL.md`
101
- 9. `{project-root}/.claude/skills/quarkus-temporal-workflows/SKILL.md` — **only if the project uses Temporal** (`io.temporal:temporal-sdk` in a `pom.xml`, or an `orchestration/` package). Skip it otherwise; it governs nothing in a project without workflows.
101
+ 9. `{project-root}/.claude/skills/quarkus-temporal-workflows/SKILL.md` — **only if the project uses Temporal** (`io.quarkiverse.temporal:quarkus-temporal` in a `pom.xml`, a `quarkus.temporal.*` property, or an `orchestration/` package). Skip it otherwise; it governs nothing in a project without workflows.
102
102
 
103
103
  These are **sibling** skills installed flat at `.claude/skills/`, not nested under this one — a bare `skills/<name>/SKILL.md` path resolves from `{skill-root}`, finds nothing, and fails silently. If a file is missing at that path, try `{project-root}/src/bmm-skills/agents/bmad-quarkus-build/skills/<name>/SKILL.md` (pre-install staging layout) and use whichever resolves. If neither resolves, **say so before writing any code** rather than working from memory.
104
104
 
@@ -140,6 +140,7 @@ Rules:
140
140
  - Retries only for idempotent RPCs and only `UNAVAILABLE`/`DEADLINE_EXCEEDED`, with budget — prefer platform/mesh retry policy over hand-rolled loops.
141
141
  - TLS/mTLS per platform standard (`quarkus.grpc.clients.*.ssl.*` / mesh-provided).
142
142
  - Translate `StatusRuntimeException` into a `BusinessException` carrying the caller's own error code, inside the integration bean — gRPC types (and `Uni`) never reach a `Handler`. Read the upstream code from the `error-code` trailer and map it through the caller's `ErrorCatalog`.
143
+ - **Exception: a bean called from a Temporal activity rethrows the raw `StatusRuntimeException`.** Flattening it to `BusinessException` destroys the `Status.Code`, and that code is the only thing telling Temporal whether the failure is retryable — collapsed, an `UNAVAILABLE` blip becomes indistinguishable from an `INVALID_ARGUMENT` and would permanently fail the saga. The activity classifies it instead (see quarkus-temporal-workflows skill §9). Keep such a bean's activity-facing method separate from any `Handler`-facing one rather than changing behaviour based on the caller.
143
144
 
144
145
  ## Health & reflection
145
146
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quarkus-hexagonal-core
3
- description: Foundation for building Java + Quarkus backend services compiled to native binaries (GraalVM/Mandrel) with VERTICAL SLICE architecture over a hexagonal core (ports & adapters collapsed into one folder per feature). Applies to ANY Java/Quarkus backend project — DDD bounded contexts, standalone services, internal tools alike. Use this skill whenever creating, scaffolding, reviewing, or modifying ANY Quarkus backend service, slice, feature, REST/gRPC/Kafka adapter, Sql data class, Handler, or DTO — even if the user only says "create a service", "add an endpoint", "new microservice", or "backend project". Also the canonical source for the SLICE FILE LAYOUT (dto/, Handler, Sql, Resource, GrpcService, Consumer, README) and CLASS NAMING CONVENTIONS, the domain-monorepo layout (apps/, libs/, contracts/, deploy/, db/, docs/), deployable app naming ({module}-{service or submodule}-ms, tenant-agnostic), the mandatory per-app service.yaml + README.md standard, and the Handler contract (@Transactional only on process(), never setAutoCommit/commit/rollback, one Connection per business operation). Enforces Java 25, JDBC-only persistence via Agroal (NO Panache, NO Hibernate/ORM), ArchUnit slice-isolation + naming tests, and native-image compatibility. Project-specific directives (CLAUDE.md, ADRs, explicit instructions) override this standard where they conflict.
3
+ description: Foundation for Java + Quarkus backend services compiled to native binaries (GraalVM/Mandrel) with VERTICAL SLICE architecture over a hexagonal core. Use when creating, scaffolding, reviewing or modifying ANY Quarkus backend service, slice, feature, REST/gRPC/Kafka adapter, Sql data class, Handler or DTO — even if the user only says "create a service", "add an endpoint" or "new microservice". Canonical source for the SLICE FILE LAYOUT, CLASS NAMING CONVENTIONS, the domain-monorepo layout, deployable naming (`-ms` with an API, `-worker` without one), the mandatory per-app service.yaml + README standard including API gateway base paths, and the Handler contract (@Transactional only on process(), one Connection per business operation). Enforces Java 25, JDBC-only persistence via Agroal (NO Panache/ORM), ArchUnit slice-isolation + naming tests, and native-image compatibility. Project directives (CLAUDE.md, ADRs) override this where they conflict.
4
4
  ---
5
5
 
6
6
  # Quarkus Vertical Slice + Hexagonal Core (Native-First)
@@ -399,7 +399,12 @@ Code shared across **apps** in the domain goes to `libs/` and uses constructor i
399
399
 
400
400
  ## Scaffolding checklist
401
401
 
402
- When creating a new service. A **Temporal worker-only deployable** follows the same steps with a reduced scope (quarkus-temporal-workflows skill §3): in step 2 drop `quarkus-rest*`, `quarkus-smallrye-openapi`, and — unless it owns a table of its own `quarkus-agroal`, `quarkus-jdbc-*` and `quarkus-flyway`; in step 3 keep only the error/exception classes it actually throws; skip step 5's slice-oriented rules and ship the ArchUnit subset that skill lists. It has no slices and no `Resource`.
402
+ When creating a new service. A **Temporal worker-only deployable** (quarkus-temporal-workflows skill §3) follows the same steps with a reduced scope — it has no slices and no `Resource`:
403
+
404
+ - **Step 2 — add** `io.quarkiverse.temporal:quarkus-temporal` and `quarkus-grpc`; **keep** `quarkus-arc`, `quarkus-smallrye-health` and the three observability extensions; **drop** `quarkus-rest*`, `quarkus-smallrye-openapi`, `quarkus-hibernate-validator` and `quarkus-oidc`, and — unless it owns a table of its own — `quarkus-agroal`, `quarkus-jdbc-*`, `quarkus-narayana-jta` and `quarkus-flyway`.
405
+ - **Step 3 —** keep only the error/exception classes it actually throws.
406
+ - **Step 4 —** drop the datasource and Flyway blocks from the baseline below whenever step 2 dropped those extensions. **Leaving `quarkus.datasource.db-kind=postgresql` in place without a JDBC driver fails the Quarkus build** with "Unable to find a JDBC driver corresponding to the database kind". Add the `quarkus.temporal.*` block that skill's §2 defines instead.
407
+ - **Step 5 —** skip the slice-oriented rules and ship the ArchUnit subset that skill lists.
403
408
 
404
409
  1. App folder `apps/<module>-<service-or-submodule>-ms/` per the monorepo layout above — `-worker` instead of `-ms` for a Temporal worker deployable.
405
410
  2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (latest 3.x LTS), extensions: `quarkus-rest`, `quarkus-rest-jackson`, `quarkus-agroal`, `quarkus-jdbc-postgresql`, `quarkus-arc`, `quarkus-narayana-jta`, `quarkus-hibernate-validator`, `quarkus-smallrye-health`, `quarkus-smallrye-openapi`, `quarkus-opentelemetry`, `quarkus-micrometer-registry-prometheus` (mandatory metrics — brings `quarkus-micrometer` transitively) and `quarkus-logging-json` (mandatory structured logs) — the three observability extensions are all required, see that skill — `quarkus-oidc` wherever the app exposes an authenticated API (security skill §4), `quarkus-flyway` (kept even where migrations are disabled — see sql skill §10), plus Lombok and skill-specific extensions as needed.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quarkus-observability-otel
3
- description: Observability standard for Quarkus native services using OpenTelemetry — distributed tracing with W3C Trace Context (traceparent), trace_id/span_id in every log line (JSON logs + MDC), OTLP export, MANDATORY Micrometer metrics (`quarkus-micrometer-registry-prometheus` in every service), custom spans, and trace propagation across REST, gRPC, and Kafka. Also covers build identity in telemetry (service.version/service.instance.id, build_info gauge), the export pipeline (batch span processor, graceful-shutdown flush budget, span attribute/event/link limits) and propagation safety (propagators, baggage, untrusted inbound traceparent). Use this skill whenever the user mentions OpenTelemetry, OTel, tracing, trace_id, span_id, traceparent, distributed tracing, logging standard, log correlation, metrics, Micrometer, quarkus-micrometer, MeterRegistry, counter/timer/gauge, `@Timed`/`@Counted`, /q/metrics, scrape, dashboards or alerts, Grafana/Tempo/Jaeger/Prometheus, baggage, propagators, sampling, service.version or build metadata, telemetry lost on shutdown/SIGTERM/terminationGracePeriodSeconds, span limits or telemetry volume/cost, or debugging requests across services. Includes span, metric, logger and audit-bean naming conventions for the vertical-slice layout (one span per Handler.process()).
3
+ description: Observability standard for Quarkus native services OpenTelemetry tracing with W3C Trace Context, trace_id/span_id in every log line (JSON logs + MDC), OTLP export, MANDATORY Micrometer metrics (`quarkus-micrometer-registry-prometheus` in every service), build identity in every signal (service.version, build_info gauge), the export pipeline (batch span processor, graceful-shutdown flush budget, span limits) and propagation safety (propagators, baggage, untrusted inbound traceparent). Use when the user mentions OpenTelemetry, OTel, tracing, trace_id, span_id, traceparent, log correlation, metrics, Micrometer, MeterRegistry, counter/timer/gauge, /q/metrics, dashboards or alerts, Grafana/Tempo/Jaeger/Prometheus, baggage, sampling, telemetry lost on shutdown, span limits or telemetry cost. Includes span, metric, logger and audit-bean naming for the vertical-slice layout (one span per Handler.process()).
4
4
  ---
5
5
 
6
6
  # OpenTelemetry Observability Standard (Quarkus)
@@ -368,7 +368,7 @@ public class RegisterUserHandler {
368
368
  5. Micrometer binders enabled (jvm, system, http-server/client, plus grpc/kafka where used) and `quarkus.datasource.metrics.enabled=true`; every custom metric follows the naming table, carries only bounded tags, and duplicates no binder metric.
369
369
  6. `/q/metrics` returns data from the **native** binary and is reachable only from inside the cluster.
370
370
  7. `service.version` + `service.instance.id` present in resource attributes AND in the JSON log fields; `<module>_build_info` gauge registered — and `version` is **not** a common tag on other metrics.
371
- 8. Shutdown budget satisfied: `terminationGracePeriodSeconds > quarkus.shutdown.timeout + quarkus.otel.bsp.export.timeout`, set in the deployment manifest, not left at the 30s default by accident.
371
+ 8. Shutdown budget satisfied: `terminationGracePeriodSeconds > quarkus.shutdown.timeout + (flush worst case)` — **plus `quarkus.temporal.termination-timeout` on a Temporal worker**, which drains in-flight activity tasks out of the same grace period. Set in the deployment manifest, not left at the 30s default by accident.
372
372
  9. `quarkus.otel.propagators` explicitly set (`tracecontext` unless baggage is documented in `service.yaml`); no PII, token or `tenantId` in baggage; public-edge `traceparent` treated as untrusted.
373
373
  10. Span attribute/event/link limits configured; the BSP dropped-span counter is alerted on.
374
374
  11. Verify end-to-end in dev: one request produces a single trace spanning REST → DB → Kafka → consumer (Dev UI or Jaeger/Tempo), and increments the metrics that back its dashboard.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quarkus-temporal-workflows
3
- description: Standard for durable orchestration and distributed sagas in Quarkus native services using the Temporal Java SDK — plain `io.temporal:temporal-sdk` (1.27.0+) with NO Quarkiverse extension, manual CDI bootstrap (`@Produces WorkflowClient` + `@Observes StartupEvent` starting the `WorkerFactory`), the `io.temporal.workflow.Saga` compensation pattern, Java records as `@RegisterForReflection` payloads, the GraalVM Netty/gRPC native fix, and `TestWorkflowEnvironment` in-memory testing. Workflows are invoked predominantly over gRPC inbound through a `*GrpcService` that starts the workflow without waiting, outbound from activities through `common/client` beans so it also covers the deadline-vs-activity-timeout ordering and the stacked-retry trap. Use this skill whenever the user mentions Temporal, temporal-sdk, workflow orchestration, durable execution, saga, compensation, compensating transaction, activity/activities, WorkflowClient, WorkerFactory, task queue, workflow id, signal/query, `@WorkflowInterface`/`@ActivityInterface`, TestWorkflowEnvironment, long-running or multi-step business processes spanning several slices or services, or "roll back what already committed in another service". Covers BOTH topologies and says which to pick — a standalone **worker-only project** (the default — an independent deployable named with the `-worker` suffix, with no REST layer, no vertical slices and no datasource, where the REST/OpenAPI/slice/SQL standards do not apply) and an embedded worker inside a normal slice app. Use it as well whenever the user asks about a Temporal-only project, an orchestrator service, a worker deployable, or whether a workflow project needs REST endpoints or slices. Applies ONLY to projects that use Temporal; it never applies to a single-slice operation that one `Handler` and one database transaction already cover.
3
+ description: Standard for durable orchestration and distributed sagas in Quarkus native services using Temporal via the `io.quarkiverse.temporal:quarkus-temporal` extensionthe plain `io.temporal:temporal-sdk` with manual wiring cannot produce a working native image. Covers `quarkus.temporal.*` config and auto-discovery (no bootstrap bean), the `io.temporal.workflow.Saga` compensation pattern, `@RegisterForReflection` record payloads, gRPC at both ends, non-retryable `ApplicationFailure`, and `TestWorkflowEnvironment` testing. Defines two topologies — a worker-only deployable (default, `-worker` suffix, no REST, no slices, no datasource) and an embedded worker in a slice app. Use when the user mentions Temporal, quarkus-temporal, workflow orchestration, durable execution, saga, compensation, activities, WorkflowClient, WorkerFactory, task queue, workflow id, `@WorkflowInterface`/`@ActivityInterface`, TestWorkflowEnvironment, a worker-only or orchestrator project, or a long-running process spanning services.
4
4
  ---
5
5
 
6
6
  # Temporal Durable Orchestration Standard (Quarkus)
@@ -13,23 +13,23 @@ Temporal is for the work a single `@Transactional Handler.process()` cannot make
13
13
 
14
14
  **Scope inside a slice architecture (embedded only):** Temporal is quarantined to `orchestration/` and `common/temporal`, the same way Mutiny is quarantined to `*GrpcService` and `common/client` (see quarkus-hexagonal-core skill). No `io.temporal.*` type ever appears in a slice folder, a `Handler`, a `Sql` class or a slice DTO. A slice stays fully testable and deployable with Temporal absent from the classpath.
15
15
 
16
- ## 1. Dependencies — plain SDK, no Quarkiverse extension
16
+ ## 1. Dependencies — the Quarkiverse extension, not the plain SDK
17
17
 
18
18
  ```xml
19
19
  <properties>
20
- <temporal.version>1.27.0</temporal.version>
20
+ <quarkus.platform.version>3.33.3</quarkus.platform.version>
21
+ <quarkus-temporal.version>0.6.0</quarkus-temporal.version>
22
+ <!-- Must track the version quarkus-temporal itself pins (0.6.0 -> 1.38.0):
23
+ temporal-testing is the plain SDK's own artifact and has to match the
24
+ SDK the extension pulls in transitively. -->
25
+ <temporal.version>1.38.0</temporal.version>
21
26
  </properties>
22
27
 
23
28
  <dependencies>
24
29
  <dependency>
25
- <groupId>io.temporal</groupId>
26
- <artifactId>temporal-sdk</artifactId>
27
- <version>${temporal.version}</version>
28
- </dependency>
29
- <dependency>
30
- <groupId>io.temporal</groupId>
31
- <artifactId>temporal-opentracing</artifactId> <!-- trace propagation — see §10 -->
32
- <version>${temporal.version}</version>
30
+ <groupId>io.quarkiverse.temporal</groupId>
31
+ <artifactId>quarkus-temporal</artifactId>
32
+ <version>${quarkus-temporal.version}</version>
33
33
  </dependency>
34
34
  <dependency>
35
35
  <groupId>io.temporal</groupId>
@@ -40,90 +40,44 @@ Temporal is for the work a single `@Transactional Handler.process()` cannot make
40
40
  </dependencies>
41
41
  ```
42
42
 
43
- - **`io.temporal:temporal-sdk` 1.27.0 or newer.** Pin an explicit version the Quarkus BOM does not manage Temporal, so nothing else will.
44
- - **Do not use a Quarkiverse Temporal extension.** The SDK is wired by hand (§2). The trade is explicit and worth naming: no extension means no build-time wiring, so the CDI bootstrap and the native-image flags (§8) are *yours* to maintain — and in exchange the SDK version moves independently of the Quarkus platform release, which is what a durable-execution dependency needs.
45
- - `temporal-testing` is `test` scope only. It must never reach the runtime classpath.
46
- - The code you write follows the patterns in Temporal's official Java samples — interface + implementation pair, activity stubs built inside the workflow, `Saga` for compensation. Do not invent a house abstraction over the SDK; a wrapper around `Workflow.newActivityStub` hides the options that make a workflow correct.
43
+ **Use `io.quarkiverse.temporal:quarkus-temporal`. Do not wire `io.temporal:temporal-sdk` by hand — it cannot produce a working native image.** This reverses earlier guidance, and the reason is specific enough to be worth recording so nobody re-litigates it: the manual channel-building path hits a hard GraalVM `BUILD_TIME`/`RUN_TIME` policy conflict on `io.grpc.netty.shaded.io.netty.buffer.PooledByteBufAllocator` that **no `--initialize-at-run-time` / `--initialize-at-build-time` combination can override**. The extension ships GraalVM substitution classes for the shaded gRPC/Netty classes, which is what makes the binary build at all. Since the native binary is the delivery artifact (see quarkus-hexagonal-core skill), a JVM-only approach is not an option to fall back on.
47
44
 
48
- ## 2. Bootstrap: one CDI bean owns the client and the worker
45
+ - The extension pulls `temporal-sdk` transitively do not declare it yourself, or the two versions will drift.
46
+ - `temporal-testing` stays `test` scope and must match the SDK version the extension pins. Bump both together.
47
+ - `quarkus-smallrye-health` is still required even though the worker serves no REST (see quarkus-hexagonal-core skill, README §11) — liveness/readiness are not optional because the app has no HTTP surface of its own.
48
+ - Everything you write is still plain Temporal SDK code following the official Java samples — `@WorkflowInterface`/`@ActivityInterface` pairs, activity stubs built inside the workflow, `Saga` for compensation. The extension owns the *lifecycle*, not your workflow code. Do not build a house abstraction over `Workflow.newActivityStub`; it hides the options that make a workflow correct.
49
49
 
50
- There is no extension to start the worker, so exactly one `@ApplicationScoped` bean in `common/temporal` produces the `WorkflowClient` and starts the `WorkerFactory` on `StartupEvent`.
50
+ ## 2. Configuration and lifecycle owned by the extension
51
51
 
52
- ```java
53
- // common/temporal
54
- @ApplicationScoped
55
- public class TemporalBootstrap {
52
+ There is **no composition root to write**. The extension owns the `WorkflowServiceStubs` / `WorkflowClient` / `WorkerFactory` lifecycle and **auto-discovers** your workflow and activity implementations from their `@WorkflowInterface` / `@ActivityInterface` contracts. Everything is configuration:
56
53
 
57
- @Inject TemporalConfig config;
58
-
59
- private WorkerFactory factory;
60
-
61
- @Produces
62
- @ApplicationScoped
63
- public WorkflowServiceStubs serviceStubs() {
64
- return WorkflowServiceStubs.newServiceStubs(
65
- WorkflowServiceStubsOptions.newBuilder()
66
- .setTarget(config.target())
67
- .build());
68
- }
69
-
70
- @Produces
71
- @ApplicationScoped
72
- public WorkflowClient workflowClient(WorkflowServiceStubs stubs) {
73
- return WorkflowClient.newInstance(stubs,
74
- WorkflowClientOptions.newBuilder()
75
- .setNamespace(config.namespace())
76
- .build());
77
- }
78
-
79
- void onStart(@Observes StartupEvent event,
80
- WorkflowClient client,
81
- PartyOnboardingActivitiesImpl activities) {
82
- factory = WorkerFactory.newInstance(client);
83
- Worker worker = factory.newWorker(config.taskQueue());
84
- worker.registerWorkflowImplementationTypes(PartyOnboardingWorkflowImpl.class); // by CLASS
85
- worker.registerActivitiesImplementations(activities); // by INSTANCE
86
- factory.start();
87
- }
88
-
89
- void onStop(@Observes ShutdownEvent event, WorkflowServiceStubs stubs) {
90
- if (factory != null) {
91
- factory.shutdown(); // stop polling
92
- factory.awaitTermination(20, TimeUnit.SECONDS); // drain in-flight tasks
93
- }
94
- stubs.shutdown();
95
- }
96
- }
54
+ ```properties
55
+ quarkus.temporal.connection.target=${TEMPORAL_SERVICE_TARGET:127.0.0.1:7233}
56
+ quarkus.temporal.worker.task-queue=alva.customer.party-onboarding.v1
57
+ # Blocks shutdown until in-flight activity tasks finish, or this timeout hits
58
+ quarkus.temporal.termination-timeout=20s
97
59
  ```
98
60
 
99
- The two registration calls are **not** symmetrical, and the asymmetry is the rule that trips people:
61
+ - **Do not hand-write a `TemporalBootstrap`/`TemporalLifecycle` bean, a `@Produces WorkflowClient`, or a `WorkerFactory.start()` in a `StartupEvent` observer.** The extension already did it; a second registration on the same task queue is a duplicate poller. If you are migrating from the old manual wiring, delete the bean and the `@ConfigMapping` that fed it.
62
+ - `quarkus.temporal.termination-timeout` replaces the manual `factory.shutdown()` + `awaitTermination` drain. Budget it inside the pod's grace period alongside the telemetry flush (see quarkus-observability-otel skill, "The shutdown budget").
63
+ - The task queue is configured **and** referenced from code, so pin it once and keep the two in sync — a constant on the workflow interface, mirrored by the property:
100
64
 
101
- | Registration | Form | Why |
102
- |---|---|---|
103
- | Workflows | `registerWorkflowImplementationTypes(XImpl.class)` — a **class** | Temporal instantiates a fresh workflow object per execution and replays it. **CDI cannot inject into a workflow implementation**; a field injected once would not survive replay. Its only dependencies are activity stubs it builds itself (§4). |
104
- | Activities | `registerActivitiesImplementations(bean)` — an **instance** | Activities are ordinary side-effecting beans. Pass the CDI-managed instance so `@Inject`ed `Handler`s are wired normally. |
65
+ ```java
66
+ @WorkflowInterface
67
+ public interface PartyOnboardingWorkflow {
105
68
 
106
- Rules:
107
- - **One bootstrap bean per app.** A second one registering a second worker on the same task queue is a duplicate-poller bug, not scaling — scale with replicas.
108
- - `onStop` matters: `factory.shutdown()` + `awaitTermination` lets in-flight activity tasks finish instead of being abandoned to a timeout-and-retry cycle. Budget it inside the pod's grace period alongside the telemetry flush (see quarkus-observability-otel skill, "The shutdown budget").
109
- - Connection settings come from `@ConfigMapping` (`<Area>Config`, per the hexagonal-core naming table), never from literals:
69
+ /** Must match quarkus.temporal.worker.task-queue in application.properties. */
70
+ String TASK_QUEUE = "alva.customer.party-onboarding.v1";
110
71
 
111
- ```java
112
- @ConfigMapping(prefix = "temporal")
113
- public interface TemporalConfig {
114
- String target();
115
- String namespace();
116
- String taskQueue();
72
+ @WorkflowMethod
73
+ PartyOnboardingResultDto onboard(PartyOnboardingRequestDto request);
117
74
  }
118
75
  ```
119
76
 
120
- ```properties
121
- temporal.target=${TEMPORAL_TARGET:localhost:7233}
122
- temporal.namespace=${TEMPORAL_NAMESPACE:default}
123
- temporal.task-queue=alva.customer.party-onboarding.v1
124
- ```
77
+ - Inject `WorkflowClient` wherever you start a workflow (§6); the extension produces it.
78
+ - The endpoint, namespace and any mTLS certificate path are environment-specific — `.env` locally, platform vault in deployed environments, `${VAR}` in properties (see quarkus-security-standards skill §1–2). A Temporal Cloud client certificate is a secret and is never committed.
125
79
 
126
- The endpoint, namespace and any mTLS certificate path are environment-specific: `.env` locally, platform vault in deployed environments, `${VAR}` in propertiessee quarkus-security-standards skill §1–2. A Temporal Cloud client certificate is a secret and is never committed.
80
+ **What has not changed:** a workflow implementation is still instantiated fresh per execution and replayed, so **CDI cannot inject into it** its only collaborators are the activity stubs it builds itself (§4). Activity implementations, by contrast, are ordinary `@ApplicationScoped` beans and are injected normally.
127
81
 
128
82
  ## 3. Topology: a worker-only project (default), or an embedded worker
129
83
 
@@ -140,8 +94,7 @@ A deployable that hosts workflows and activities **and nothing else**: no REST,
140
94
  apps/customer-onboarding-worker/ # worker-only deployable — note the -worker suffix
141
95
  ├── src/main/java/com/alva/customer/onboarding/
142
96
  │ ├── common/
143
- │ │ ├── temporal/TemporalConfig.java # @ConfigMapping
144
- │ │ ├── temporal/TemporalBootstrap.java # §2 — the only class touching WorkerFactory
97
+ │ │ │ # no TemporalConfig, no bootstrap bean — §2
145
98
  │ │ ├── client/PartyRegistrar.java # gRPC beans — how activities reach other services
146
99
  │ │ ├── client/WalletOpener.java
147
100
  │ │ └── exception/BusinessException.java # the house error taxonomy still applies (§9)
@@ -181,7 +134,13 @@ Marcus reads eight universal standards; several of them have nothing to govern h
181
134
  | `quarkus-kafka-messaging` | Applies when a workflow is triggered by, or emits, domain events. |
182
135
  | Rest of `quarkus-hexagonal-core` | Naming, `apps/` layout, Java 25, native build + Dockerfile, `service.yaml` + README: **all unchanged**. |
183
136
 
184
- ArchUnit ships a **subset**: `bannedSuffixes` (with the `..orchestration..` `*Impl` carve-out below), `reactiveIsQuarantined`, `noOrm`, the DTO rules, plus `workflowsDoNoIo`. Drop `slicesAreIndependent`, `onlyHandlersTouchSql`, `transactionalOnlyOnProcess`, `handlersExposeOnlyProcess`, `restResources` and `transportHasNoJdbc` with no slices to match they can never fail, and a rule that cannot fail is noise that makes the suite look stronger than it is.
137
+ ArchUnit ships a **subset**, and the two lists below partition hexagonal-core's canonical suiteevery rule it defines is either kept or explicitly dropped, so nothing is left ambiguous.
138
+
139
+ **Keep:** `bannedSuffixes` (with the `..orchestration..` `*Impl` carve-out below), `serviceSuffixIsReserved` and `grpcServices` (they bite the moment the worker owns its trigger RPC), `reactiveIsQuarantined`, `noOrm`, `businessExceptionIsTheOnlyOne`, `dtosStayInDtoPackage`, `dtosHaveNoLogic`, plus this skill's own `workflowsDoNoIo`.
140
+
141
+ **Drop:** `slicesAreIndependent`, `onlyHandlersTouchSql`, `transactionalOnlyOnProcess`, `handlersExposeOnlyProcess`, `handlerKnowsNoTransport`, `crossCuttingWritesAreCentralised`, `restResources` and `transportHasNoJdbc` — with no slices, no `Handler` and no `Sql` to match, they can never fail, and a rule that cannot fail is noise that makes the suite look stronger than it is.
142
+
143
+ `temporalIsQuarantined` is **dropped too, and only here**: in a worker-only app the whole codebase is Temporal, so the rule would forbid what the app exists to do. It is mandatory in the embedded topology (B).
185
144
 
186
145
  #### Who starts the workflow
187
146
 
@@ -196,7 +155,7 @@ A normal `-ms` app that also hosts a worker — and it **stays `-ms`**, because
196
155
 
197
156
  ```
198
157
  src/main/java/com/<company>/<module>/
199
- ├── common/temporal/… # as above, plus PartyOnboardingStarter (§6)
158
+ ├── common/temporal/PartyOnboardingStarter.java # §6 the only Temporal-aware class outside orchestration/
200
159
  ├── orchestration/
201
160
  │ └── party_onboarding/… # same contents; activities call local Handlers
202
161
  └── create_party_individual/ # slices — untouched, no Temporal import
@@ -282,16 +241,16 @@ public class PartyOnboardingWorkflowImpl implements PartyOnboardingWorkflow {
282
241
  .setScheduleToCloseTimeout(Duration.ofMinutes(10)) // the outer bound — never omit
283
242
  .setRetryOptions(RetryOptions.newBuilder()
284
243
  .setInitialInterval(Duration.ofSeconds(1))
244
+ .setMaximumInterval(Duration.ofSeconds(30))
285
245
  .setBackoffCoefficient(2.0)
286
- .setMaximumAttempts(5)
287
- .setDoNotRetry("BusinessException") // matches the FAILURE TYPE — see §9
288
- .build())
246
+ .build()) // no setDoNotRetry — see §9
289
247
  .build());
290
248
 
291
249
  @Override
292
250
  public PartyOnboardingResultDto onboard(PartyOnboardingRequestDto request) {
293
251
  Saga saga = new Saga(new Saga.Options.Builder()
294
252
  .setParallelCompensation(false) // compensate in reverse order, one at a time
253
+ .setContinueWithError(true) // attempt every compensation — see below
295
254
  .build());
296
255
  try {
297
256
  String partyId = activities.createParty(request);
@@ -303,7 +262,7 @@ public class PartyOnboardingWorkflowImpl implements PartyOnboardingWorkflow {
303
262
  activities.sendWelcomeNotification(partyId);
304
263
  return new PartyOnboardingResultDto(partyId, walletId);
305
264
 
306
- } catch (TemporalFailure e) { // NOT just ActivityFailure — see below
265
+ } catch (RuntimeException e) { // deliberately broad — see below
307
266
  log.warn("Onboarding failed, compensating: {}", e.getMessage());
308
267
  saga.compensate();
309
268
  throw e; // the workflow still fails — do not swallow
@@ -313,8 +272,9 @@ public class PartyOnboardingWorkflowImpl implements PartyOnboardingWorkflow {
313
272
  ```
314
273
 
315
274
  Rules:
316
- - **Catch `TemporalFailure`, not `ActivityFailure`.** `ActivityFailure` misses `ChildWorkflowFailure`, `CanceledFailure` and every other `TemporalFailure` subtype and a miss means the compensation chain silently never runs, which is the exact failure the saga exists to prevent.
317
- - **Decide `Saga.Options.setContinueWithError` deliberately.** It defaults to `false`, so the *first* failing compensation aborts the rest of the chain and leaves the earlier steps uncompensated. Set it to `true` when the compensations are independent and you would rather attempt all of them; leave it `false` only when a failed compensation genuinely makes the remaining ones unsafe.
275
+ - **Catch `RuntimeException`, not `ActivityFailure`.** `ActivityFailure` misses `ChildWorkflowFailure`, `CanceledFailure` and every other `TemporalFailure` subtype, and a miss means the compensation chain silently never runs the exact failure the saga exists to prevent. `RuntimeException` is deliberately broader than even `TemporalFailure`: anything that escapes the forward path should compensate.
276
+ - **`setContinueWithError(true)` is the default here.** Out of the box it is `false`, which makes the *first* failing compensation abort the rest of the chain and leave earlier steps uncompensated almost never what you want, since the compensations are usually independent deletes. Leave it `false` only when a failed compensation genuinely makes the remaining ones unsafe.
277
+ - **A step whose effect is an idempotent, resubmittable correction needs no compensation at all.** Register one only for steps that created something. Say so in a comment where you skip it, or the next reader will assume it was forgotten.
318
278
  - **Register the compensation *after* the forward activity returns**, using its result. Registering before means compensating something that never happened.
319
279
  - Compensations run in **reverse registration order** (LIFO). Keep `setParallelCompensation(false)` unless the steps are provably independent — parallel compensation of dependent resources reintroduces the ordering bug the saga exists to avoid.
320
280
  - **`saga.compensate()` then rethrow.** A saga that compensates and returns normally reports success for a business process that did not happen.
@@ -360,7 +320,7 @@ Either way the activity is a transport adapter in the sense the hexagonal-core s
360
320
 
361
321
  The `*Starter` bean below is the same in both topologies — what differs is **which deployable owns it**:
362
322
 
363
- - **Worker-only (A):** it lives in the *calling* service, which therefore carries `temporal-sdk` and a `common/temporal` package of its own (client only it registers no worker). The worker project itself does not start workflows; it runs them.
323
+ - **Worker-only (A):** the `*Starter` lives in the *calling* service, which therefore carries `quarkus-temporal` of its own — configured as a **client without a worker**, since it starts workflows rather than running them. That falls out of auto-discovery on its own: the calling service contains no `@WorkflowInterface`/`@ActivityInterface` implementations, so the extension has nothing to register and starts no poller — just set `quarkus.temporal.connection.target` and leave the worker's task queue unset. Never register a worker on the orchestrator's task queue from a service that cannot execute the workflow. When the worker app owns its own trigger RPC (§3), its `*GrpcService` injects `WorkflowClient` and starts the workflow inline — no separate bean.
364
324
  - **Embedded (B):** it lives in this app, and a slice `Handler` injects it.
365
325
 
366
326
  In the embedded case a `Handler` must start the workflow through this bean and **never** by injecting `WorkflowClient` into the slice — that would drag `io.temporal` past the §3 quarantine:
@@ -370,14 +330,13 @@ In the embedded case a `Handler` must start the workflow through this bean and *
370
330
  @ApplicationScoped
371
331
  public class PartyOnboardingStarter {
372
332
 
373
- @Inject WorkflowClient client;
374
- @Inject TemporalConfig config;
333
+ @Inject WorkflowClient client; // produced by the extension
375
334
 
376
335
  public void start(String partyId, PartyOnboardingRequestDto request) {
377
336
  PartyOnboardingWorkflow workflow = client.newWorkflowStub(
378
337
  PartyOnboardingWorkflow.class,
379
338
  WorkflowOptions.newBuilder()
380
- .setTaskQueue(config.taskQueue())
339
+ .setTaskQueue(PartyOnboardingWorkflow.TASK_QUEUE) // the interface constant, §2
381
340
  .setWorkflowId("party-onboarding-" + partyId) // deterministic = deduplicated
382
341
  .setWorkflowIdReusePolicy(
383
342
  WorkflowIdReusePolicy.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY)
@@ -464,19 +423,13 @@ Get this backwards and the activity times out while the gRPC call is still in fl
464
423
 
465
424
  ### Mapping gRPC failures onto the retry policy
466
425
 
467
- A `StatusRuntimeException` from a downstream is classified at the activity boundary, the same way §9 classifies the house exceptions:
468
-
469
- | gRPC status from the downstream | Temporal treatment |
470
- |---|---|
471
- | `INVALID_ARGUMENT`, `FAILED_PRECONDITION`, `NOT_FOUND`, `ALREADY_EXISTS`, `PERMISSION_DENIED` | **non-retryable** — translate to the slice's `BusinessException` code, then to a non-retryable `ApplicationFailure` |
472
- | `UNAVAILABLE`, `DEADLINE_EXCEEDED`, `RESOURCE_EXHAUSTED`, `ABORTED` | retryable — this is what the activity retry policy is for |
473
- | `INTERNAL`, `UNKNOWN` | retryable, but bounded by `scheduleToCloseTimeout` |
426
+ A `StatusRuntimeException` from a downstream is classified at the activity boundary. **§9 holds the single authoritative list of which codes are terminal — do not restate it here or anywhere else**, because two copies of a retry-classification table drift and the drift is silent: a code that is terminal in one list and retryable in the other produces either an infinite retry or a saga that compensates on a blip.
474
427
 
475
- The downstream already maps its `BusinessException` codes onto gRPC statuses on the way out (see the grpc skill's error-mapping table), so this table is that one read in reverse — keep the two consistent when either changes.
428
+ The downstream already maps its `BusinessException` codes onto gRPC statuses on the way out (see the grpc skill's error-mapping table), so §9's table is that one read in reverse — keep the two consistent when either changes.
476
429
 
477
430
  ### The SDK's own transport
478
431
 
479
- The worker and the client talk to the Temporal service over gRPC as well, on shaded Netty. That is not trivia: it is why the native build needs the run-time initialization flag in §8, why a Temporal Cloud connection is mTLS with a client certificate handled as a secret (§2), and why `WorkflowServiceStubsOptions` carries its own RPC timeout and keepalive settings, separate from every timeout above.
432
+ The worker and the client talk to the Temporal service over gRPC as well, on shaded Netty. That is not trivia: it is why the native build depends on the GraalVM substitutions `quarkus-temporal` ships rather than on any initialization flag (§8), and why a Temporal Cloud connection is mTLS with a client certificate handled as a secret (§2). The connection's own RPC timeouts and keepalive are separate from every timeout above and are configured through `quarkus.temporal.connection.*`, not by hand-building stubs.
480
433
 
481
434
  ## 8. Payloads and native image
482
435
 
@@ -493,45 +446,70 @@ public record PartyOnboardingRequestDto(
493
446
 
494
447
  This is a deliberate local deviation from the slice DTO convention (Lombok `@Data` classes, per hexagonal-core): a Temporal payload is serialized into an immutable event history, so a mutable DTO with setters is the wrong shape. The `*Dto` suffix and the `dto/` package still apply, so the existing `dtosStayInDtoPackage` ArchUnit rule keeps passing.
495
448
 
496
- **The native build needs the gRPC/Netty run-time initialization flag.** Temporal talks gRPC over shaded Netty, which fails at image-build time without it:
449
+ ### The native build needs no Temporal-specific flags
497
450
 
498
451
  ```properties
499
- quarkus.native.additional-build-args=--initialize-at-run-time=io.grpc.netty.shaded.io.netty
452
+ # That is the whole native configuration — no additional-build-args for Temporal
453
+ quarkus.native.container-build=true
454
+ quarkus.native.builder-image=mandrel
500
455
  ```
501
456
 
502
- - This property is a **comma-separated list**: if the app already sets it, append rather than overwrite — a silently replaced flag is a native failure two builds later.
503
- - The SDK builds workflow and activity stubs with JDK dynamic proxies, which is precisely the pattern hexagonal-core tells you to avoid. Here it is unavoidable, so it is a *verified* exception, not an assumed one: if the native build or the binary fails on a missing proxy, register the interface with Quarkus's `@RegisterForProxy`. (The old `-H:DynamicProxyConfigurationResources` flag is gone on GraalVM for JDK 21+ — proxies are handled through reachability metadata now; do not reach for it.) Either way, **a Temporal app is not "native-ready" until `@QuarkusIntegrationTest` has run green against the built binary** — the JVM-mode test proves nothing about this.
457
+ **Do not add `--initialize-at-run-time=io.grpc.netty.shaded.io.netty` (or any variant of it).** This is the trap that cost a spike: it looks like the obvious fix, it is what the plain-SDK path appears to need, and **it does not work** the conflict on `io.grpc.netty.shaded.io.netty.buffer.PooledByteBufAllocator` is a build-time/run-time *policy* clash that an initialization flag cannot resolve in either direction. What resolves it is the substitution classes `quarkus-temporal` ships (§1). Adding the flag on top of the extension buys nothing and misleads the next person debugging a native failure.
458
+
459
+ - If the app sets `quarkus.native.additional-build-args` for some *other* reason, remember the property is a comma-separated list — append, never overwrite.
460
+ - The SDK builds workflow and activity stubs with JDK dynamic proxies, which is precisely the pattern hexagonal-core tells you to avoid. The extension's build step registers what it needs; if a proxy is still missing, use Quarkus's `@RegisterForProxy` rather than a raw flag. (`-H:DynamicProxyConfigurationResources` is gone on GraalVM for JDK 21+ — proxies go through reachability metadata now.)
461
+ - **A Temporal app is not "native-ready" until the binary has actually been built and exercised.** The JVM-mode test proves nothing here: every problem in this section appears only at image-build time.
504
462
 
505
463
  ## 9. Errors and retries
506
464
 
507
- Temporal's retry policy is the automatic remediation this codebase otherwise has to hand-write — but only if failures are classified correctly. The house exception taxonomy (see quarkus-error-handling-i18n and quarkus-sql-jdbc-agroal skills) maps cleanly:
465
+ Temporal's retry policy is the automatic remediation this codebase otherwise hand-writes — but only if failures are classified correctly. **The classification lives in the activity, expressed one way only: throw a non-retryable `ApplicationFailure` for what must not be retried, and let everything else propagate unchanged.**
508
466
 
509
- | Exception from the `Handler` | Temporal treatment | Why |
510
- |---|---|---|
511
- | `BusinessException` (validation, business rule) | **non-retryable** — `setDoNotRetry(...)` | The input will not become valid on attempt 4. Retrying burns the retry budget and delays compensation. |
512
- | `TransientPersistenceException`, `PersistenceTimeoutException` (deadlock, `40001`, `57014`) | retryable — the default | Exactly what the retry policy exists for. |
513
- | `StaleVersionException` (optimistic lock) | retryable, small `maximumAttempts` | A genuine concurrent-update race that usually clears. |
514
- | `PersistenceException` (infrastructure) | retryable, capped by `scheduleToCloseTimeout` | Recoverable, but must not retry forever. |
467
+ ```java
468
+ RuntimeException translate(StatusRuntimeException e, String activityName) {
469
+ Status.Code code = e.getStatus().getCode();
470
+ if (TERMINAL_CODES.contains(code)) {
471
+ String description = e.getStatus().getDescription();
472
+ Log.warnf("%s: terminal gRPC failure code=%s, non-retryable: %s", activityName, code, description);
473
+ return ApplicationFailure.newNonRetryableFailure(
474
+ description != null ? description : e.getMessage(), code.name());
475
+ }
476
+ // UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL, ... rethrown as-is, so the workflow's
477
+ // ActivityOptions retry policy applies with backoff up to scheduleToCloseTimeout.
478
+ Log.warnf("%s: retryable gRPC failure code=%s: %s", activityName, code, e.getMessage());
479
+ return e;
480
+ }
481
+ ```
515
482
 
516
- Make the classification explicit at the activity boundary rather than relying on class-name matching alone:
483
+ **Do not also configure `setDoNotRetry(...)` on the `ActivityOptions`.** It is a second, independent mechanism that matches on the failure *type* string, so the moment that string and the type you pass to `ApplicationFailure` drift apart the declarative rule silently matches nothing — and a rule that looks wired but is inert is worse than no rule. One mechanism, at the activity boundary.
484
+
485
+ Which gRPC codes are terminal:
486
+
487
+ | Code from the downstream | Treatment |
488
+ |---|---|
489
+ | `INVALID_ARGUMENT`, `ALREADY_EXISTS`, `UNAUTHENTICATED`, `PERMISSION_DENIED` | **non-retryable** `ApplicationFailure` — a malformed or duplicate request, or a token that will not become valid inside this run |
490
+ | everything else (`UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INTERNAL`, `RESOURCE_EXHAUSTED`, …) | rethrown as-is → retried with backoff, bounded by `scheduleToCloseTimeout` |
491
+
492
+ **A compensation activity classifies differently, and getting this wrong deadlocks the rollback.** For a delete-style compensation, `NOT_FOUND` means the thing is already gone and `ALREADY_EXISTS` may encode the downstream's own conflict mapping — both mean *the compensation is as done as it can be*, so the activity must **return normally**, not fail. The same `ALREADY_EXISTS` that correctly fails a forward `create` must not fail its matching `delete`:
517
493
 
518
494
  ```java
519
- catch (BusinessException e) {
520
- // The 2nd argument is the failure TYPE and must match setDoNotRetry("BusinessException") in §5 —
521
- // passing e.code() there instead would leave the declarative rule matching nothing.
522
- // The error code travels as a detail, so the caller still gets it.
523
- throw ApplicationFailure.newNonRetryableFailure(e.getMessage(), "BusinessException", e.code());
495
+ } catch (StatusRuntimeException e) {
496
+ Status.Code code = e.getStatus().getCode();
497
+ if (code == Status.Code.NOT_FOUND || code == Status.Code.ALREADY_EXISTS) {
498
+ Log.infof("%s: compensation handled (%s)", activityName, code);
499
+ return; // idempotent: already compensated
500
+ }
501
+ throw translate(e, activityName);
524
502
  }
525
503
  ```
526
504
 
527
- `newNonRetryableFailure` already marks the failure non-retryable on its own, so the `setDoNotRetry` entry in §5 is the declarative belt to this braces but only while the two strings agree. Keep them in sync, or the retry policy silently stops matching.
505
+ The house exception taxonomy applies in both topologies the worker-only layout in §3 ships `common/exception/BusinessException` too and splits the same way: `BusinessException` non-retryable; `TransientPersistenceException` / `PersistenceTimeoutException` / `StaleVersionException` retryable, which is exactly what the retry policy exists for; `PersistenceException` → retryable but capped by `scheduleToCloseTimeout`.
528
506
 
529
- The failure the client ultimately sees is still a house error code: `WorkflowFailedException` surfacing at the REST edge is translated by `GlobalExceptionHandler` into the catalogued `<MOD>-<HTTP>-<seq>` for that capability, localized as usual. A raw Temporal stack trace never reaches an API consumer.
507
+ The failure a client ultimately sees is still a house error code: a `WorkflowFailedException` surfacing at the REST edge is translated by `GlobalExceptionHandler` into the catalogued `<MOD>-<HTTP>-<seq>`, localized as usual. A raw Temporal stack trace never reaches an API consumer.
530
508
 
531
509
  ## 10. Observability
532
510
 
533
- - Propagate trace context into workflows and activities with `OpenTracingClientInterceptor` / `OpenTracingWorkerInterceptor`, registered on `WorkflowClientOptions` and `WorkerFactoryOptions` in the bootstrap bean. They come from `io.temporal:temporal-opentracing` (declared in §1) and need `io.opentelemetry:opentelemetry-opentracing-shim` to bridge onto the OTel SDK this platform uses there is no OTel-native interceptor module, which is why a tracing-only dependency shows up in an otherwise minimal pom. Without them a workflow is a hole in the trace: the REST span ends, and the activity spans belong to no one.
534
- - Feed SDK metrics into the mandatory Micrometer registry (see quarkus-observability-otel skill) via `MicrometerClientStatsReporter` on `WorkflowServiceStubsOptions.setMetricsScope(...)`. Workflow task latency and activity failure rates are the client-side signals that a worker fleet is unhealthy. **Task-queue backlog is not among them** — it is server-side state, read from `DescribeTaskQueue` or the Temporal server's own metrics, so wire the autoscaling signal there rather than expecting it in `/q/metrics`.
511
+ - Propagate trace context into workflows and activities with `OpenTracingClientInterceptor` / `OpenTracingWorkerInterceptor`, registered through the extension's interceptor configuration. They need two dependencies §1 does not list because they are tracing-only — `io.temporal:temporal-opentracing` and `io.opentelemetry:opentelemetry-opentracing-shim` to bridge onto the OTel SDK this platform uses; there is no OTel-native interceptor module. Without them a workflow is a hole in the trace: the REST span ends, and the activity spans belong to no one.
512
+ - Feed SDK metrics into the mandatory Micrometer registry (see quarkus-observability-otel skill). The reporter is `io.temporal.common.reporter.MicrometerClientStatsReporter`, wrapped in a tally `Scope` but **the stubs are the extension's, so do not hand-build `WorkflowServiceStubsOptions` to attach it**; supply the scope through whatever customization hook `quarkus-temporal` exposes for the connection. Verify the metrics actually appear in `/q/metrics` before considering this done. Workflow task latency and activity failure rates are the client-side signals that a worker fleet is unhealthy. **Task-queue backlog is not among them** — it is server-side state, read from `DescribeTaskQueue` or the Temporal server's own metrics, so wire the autoscaling signal there rather than expecting it in `/q/metrics`.
535
513
  - Log inside workflow code only through `Workflow.getLogger(...)`; a plain logger re-emits every line on every replay and makes the log unreadable exactly when you need it.
536
514
  - Workflow ids and task queues are bounded, business-meaningful values — good span attributes. The workflow *arguments* are not: they carry the same PII prohibition as everything else.
537
515
 
@@ -553,9 +531,7 @@ class PartyOnboardingWorkflowTest {
553
531
  testEnv = TestWorkflowEnvironment.newInstance();
554
532
  worker = testEnv.newWorker(TASK_QUEUE);
555
533
  worker.registerWorkflowImplementationTypes(PartyOnboardingWorkflowImpl.class);
556
- // withoutAnnotations() is REQUIRED: Mockito copies @ActivityInterface/@ActivityMethod
557
- // onto the mock class and the worker rejects the registration without it.
558
- activities = mock(PartyOnboardingActivities.class, withSettings().withoutAnnotations());
534
+ activities = mock(PartyOnboardingActivities.class);
559
535
  worker.registerActivitiesImplementations(activities);
560
536
  }
561
537
 
@@ -589,7 +565,7 @@ Mandatory scenarios per workflow — the compensation paths are the point of the
589
565
  |---|---|---|
590
566
  | 1 | Happy path | result DTO correct; activities invoked in order; **no** compensation invoked |
591
567
  | 2 | Failure at step *n* | compensations for steps `1..n-1` ran, in reverse order; none for `n..end`; the workflow still fails |
592
- | 3 | Non-retryable business failure | the activity ran **once** (`verify(activities, times(1))`) — proves `setDoNotRetry` is wired |
568
+ | 3 | Non-retryable failure | the activity ran **once** (`verify(activities, times(1))`) — proves the activity threw a non-retryable `ApplicationFailure` instead of being retried |
593
569
  | 4 | Compensation is idempotent | a compensation invoked twice leaves the same end state |
594
570
  | 5 | Signal / timer paths, where present | time-skipping drives the timer; assert the state the signal produced |
595
571
 
@@ -601,16 +577,16 @@ Mandatory scenarios per workflow — the compensation paths are the point of the
601
577
 
602
578
  1. The operation genuinely crosses a slice, service or time boundary — a single `Handler` + one transaction cannot do it.
603
579
  2. Topology chosen and written in the app README: **worker-only** (default — deployable named `{module}-{capability}-worker`, no REST, no slices, no datasource, activities call remote services) or **embedded** (stays `-ms`). In a worker-only project, none of the REST/slice/SQL standards were scaffolded and the ArchUnit suite ships the documented subset.
604
- 3. `temporal-sdk` (1.27.0+) compile scope, `temporal-testing` test scope, explicit version, no Quarkiverse extension.
605
- 4. Exactly one `TemporalBootstrap` in `common/temporal`: `@Produces WorkflowClient`, `@Observes StartupEvent` register (workflows **by class**, activities **by instance**) `factory.start()`; `@Observes ShutdownEvent` `shutdown()` + `awaitTermination`.
580
+ 3. `io.quarkiverse.temporal:quarkus-temporal` on the classpath; `temporal-sdk` **not** declared directly; `temporal-testing` test scope at the version the extension pins; `quarkus-smallrye-health` present.
581
+ 4. No bootstrap bean anywhere lifecycle and discovery are the extension's. `quarkus.temporal.connection.target`, `worker.task-queue` (matching the interface constant) and `termination-timeout` set in `application.properties`.
606
582
  5. Endpoint, namespace and certificates come from `${VAR}` / `.env` / vault — never literals (security skill §1).
607
583
  6. Workflow lives in `orchestration/<capability>/`; in an embedded app, no `io.temporal` import inside any slice; ArchUnit quarantine + `*Impl` carve-out in place.
608
584
  7. `*WorkflowImpl` is deterministic: no clock, no random, no threads, no I/O, no `@Inject`, no unordered-collection iteration.
609
585
  8. Saga compensations registered **after** each forward step, LIFO, idempotent; irreversible steps ordered last; `compensate()` then rethrow.
610
- 9. Every activity: idempotent, `scheduleToCloseTimeout` set, `BusinessException` non-retryable, delegating to a `Handler` with no logic of its own.
586
+ 9. Every activity: idempotent, `scheduleToCloseTimeout` set, terminal failures thrown as non-retryable `ApplicationFailure` (and **no** `setDoNotRetry` alongside it), delete-style compensations treating `NOT_FOUND`/`ALREADY_EXISTS` as success.
611
587
  10. Workflow started with a deterministic workflow id, after commit — never mid-transaction.
612
588
  11. Transport is gRPC in and out: the trigger reaches the `*Starter` through a `Handler` (embedded) or directly from the worker's own `*GrpcService` (worker-only), it **returns the workflow id without waiting**, and every outbound call goes through a `common/client` bean.
613
589
  12. Timeouts ordered `grpc deadline < startToCloseTimeout < scheduleToCloseTimeout`, and retries are not stacked — Temporal owns the retry, the mesh keeps at most one.
614
- 13. Payload records annotated `@RegisterForReflection`; `--initialize-at-run-time=io.grpc.netty.shaded.io.netty` appended (not overwriting) `quarkus.native.additional-build-args`; `@QuarkusIntegrationTest` green against the native binary.
590
+ 13. Payload records annotated `@RegisterForReflection`; **no** `--initialize-at-run-time` flag for Netty/gRPC (the extension's substitutions handle it); the native binary actually built and exercised, not just the JVM tests.
615
591
  14. `TestWorkflowEnvironment` tests cover happy path **and** every compensation path; tracing interceptors and Micrometer metrics scope registered.
616
592
  15. `orchestration/<capability>/README.md` documents the steps, their compensations, the timeouts and the task queue.