Drives the refs-comparison backlog. Optimized for parallel subagent execution: each work unit (WU) is sized so a single subagent can land it independently, with explicit dependencies, file scopes, and conflict surfaces.
Status: all ten work units have shipped. Phases 0 and 1 landed as designed; Phase 2 (auth)
landed in sdk-core rather than a standalone sdk-auth module — see WU-10 for where the code
actually lives and how the shape differs from the original sketch. Each WU below carries a
Status line so the plan doubles as the as-built record. Tier 3 remains the live backlog.
- Overview
- Dependency Graph
- Conflict Map
- Phase 0 — Foundational (2 parallel WUs)
- Phase 1 — Features & Adapters (7 parallel WUs)
- Phase 2 — Auth (1 WU)
- Tier 3 (deferred)
- Execution Protocol
- Subagent Briefing Template
Three sequenced phases. Within each phase, all work units run in parallel.
| Phase | WUs | Parallelism | Blocks |
|---|---|---|---|
| 0 | WU-1, WU-2 | 2 agents | All of Phase 1+2 |
| 1 | WU-3 .. WU-9 | 7 agents | Phase 2 (auth needs WU-3 + WU-2) |
| 2 | WU-10 | 1 agent | — |
Total: 10 work units. Wall-clock time with full parallelism ≈ time of the slowest WU per phase × 3 phases.
┌────────────────────────────────────────────┐
│ Phase 0 — foundational │
│ │
│ WU-1 AfterError WU-2 HttpException │
│ pipeline upgrade hierarchy │
└───────┬─────────────────────┬──────────────┘
│ │
┌───────────────┼─────────────┬───────┼──────┬───────────┬─────────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌──────────────┐ ┌────────┐ ┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐
│ WU-3 │ │ WU-4 │ │ WU-5 │ │ WU-6 │ │ WU-7 │ │ WU-8 │ │ WU-9 │
│ Retry │ │ HttpClient. │ │ Idemp. │ │ ID │ │ Tracer │ │ Serde │ │ Paginate │
│ │ │ close() │ │ Key │ │ hdr │ │ events │ │ Jackson│ │ │
└────┬────┘ └──────────────┘ └────────┘ └──────┘ └────────┘ └────────┘ └──────────┘
│ Phase 1 — parallel features + adapter modules
│
▼
┌─────────┐
│ WU-10 │ Phase 2 — auth (needs WU-2 typed exceptions + WU-3 retry semantics)
│ Auth │
└─────────┘
WU-8 and WU-9 are technically independent of Phase 0 (new modules/packages, no shared files) but are grouped in Phase 1 so the 7-agent batch is one cohesive parallel wave.
Files that more than one work unit might touch — surface these to each subagent so they don't trip over each other.
| File / path | Touched by | Resolution |
|---|---|---|
sdk-core/pipeline/step/ (directory — add new files only) |
WU-3, WU-5, WU-6 | Each adds its own file(s); no shared index. Safe. |
sdk-core/client/HttpClient.kt |
WU-4 only | WU-3/5/6/7 must NOT touch this file. |
sdk-core/client/AsyncHttpClient.kt |
WU-4 only | Same. |
sdk-core/instrumentation/ |
WU-7 only | WU-3/5/6 emit no tracer events in this pass — wired in a follow-up. |
sdk-core/pipeline/ResponsePipeline.kt and step contracts |
WU-1 only | WU-3 reads the contract WU-1 produces but does not modify pipeline types. |
sdk-core/http/response/Status.kt |
WU-2 only | New exception package alongside, but Status itself only WU-2. |
sdk-transport-okhttp/, sdk-transport-jdkhttp/ |
WU-4 only (lifecycle) | Other WUs don't touch transport modules. |
settings.gradle.kts |
WU-8 (sdk-serde-jackson) | WU-10's auth landed in sdk-core, so no second settings.gradle.kts edit materialized. |
Status: shipped. ResponseOutcome (sealed Success/Failure), ResponseRecoveryStep, the
recovery-aware ResponsePipeline, and ExecutionPipeline are all in sdk-core/.../pipeline.
Goal. Replace the empty ResponsePipeline placeholder with Airbyte-style recovery
semantics. Add a third step type that takes Either<Response, Throwable> and can rescue or
rethrow. Funnel all exceptions through this path uniformly (don't repeat Airbyte's bug
where BeforeRequest throws bypass AfterError).
Dependencies: none.
Files (create):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/ResponseRecoveryStep.kt— newfun interface; recovery type alias.sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/ResponseOutcome.kt— sealed classResponseOutcome { data class Success(response); data class Failure(throwable) }. Simpler thanEither<>.
Files (modify):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/ResponsePipeline.kt— promote from empty interface to a fold over(ResponsePipelineStep+, ResponseRecoveryStep+)lists. Document semantics.sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/ExecutionPipeline.kt— wire request → transport → outcome → recovery → response steps. Catch transport exceptions, wrap intoResponseOutcome.Failure, feed through the recovery chain.docs/pipelines.md— update to reflect new architecture.
Acceptance criteria:
- A
ResponseRecoveryStepcan convert a failure into a success (rescue case). - A
ResponseRecoveryStepcan leave the failure intact (passthrough case). - A
ResponseRecoveryStepcan replace the throwable with a different one (re-mapping). - Exceptions thrown inside any pipeline step (request, response, recovery) are caught and routed through subsequent recovery steps — never bypass the chain.
- Unit tests cover all four paths plus the "all steps no-op" baseline.
Estimated complexity: Medium. ~2-3 days of careful work + tests.
Status: shipped. The hierarchy lives in sdk-core/.../http/response/exception — HttpException,
the per-status subclasses in HttpExceptions.kt, NetworkException, and HttpExceptionFactory.
One change from the original sketch: retryable is derived, not hand-set per subclass. The
base class computes retryable = RetryUtils.isRetryable(status.code) once at construction, so the
flag can never drift from the live retry policy. That single source of truth also fixes two codes
the original per-subclass table got wrong: 408 is retryable (it has its own
RequestTimeoutException) and 501 / 505 are not.
Dependencies: none.
Files (create):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/exception/HttpException.kt— abstract base. Fields:status: Status,headers: Headers,body: ResponseBody?,retryable: Boolean(derived fromRetryUtils.isRetryable(status.code)),cause: Throwable?.sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/exception/HttpExceptions.kt— concrete subclasses, one per common status (none sets its ownretryable; the base derives it):BadRequestException(400)UnauthorizedException(401)ForbiddenException(403)NotFoundException(404)MethodNotAllowedException(405)RequestTimeoutException(408, retryable)ConflictException(409)GoneException(410)PayloadTooLargeException(413)UnsupportedMediaTypeException(415)UnprocessableEntityException(422)TooManyRequestsException(429, retryable)InternalServerErrorException(500, retryable)BadGatewayException(502, retryable)ServiceUnavailableException(503, retryable)GatewayTimeoutException(504, retryable)ClientErrorException/ServerErrorException— generic 4xx / 5xx fallbacks.
sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/exception/NetworkException.kt— sibling type (no status; for connect/read failures; always retryable).sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/exception/HttpExceptionFactory.kt—fun fromResponse(response): HttpExceptionswitch.
Files (modify):
docs/http.md— document the hierarchy.
Acceptance criteria:
HttpExceptionFactory.fromResponse(response)returns the correct subclass for each canonical status.- Unknown status falls back to either
ClientErrorException(4xx) orServerErrorException(5xx) generic class. retryableis read-only and derived at construction fromRetryUtils.isRetryable(status.code), so it always mirrors the live retry policy.- Body is exposed as
ResponseBody?(lazy stream), not eagerly buffered into a string. @JvmOverloadson public constructors.- Unit tests for factory dispatch + retryable flag values.
Estimated complexity: Low-Medium. ~1.5-2 days.
Status: shipped. RetrySettings, RetryStep, BackoffCalculator, and RetryAfterParser
are all in sdk-core/.../pipeline/step/retry.
Goal. Build the best-in-class retry step combining Square's Retry-After /
X-RateLimit-Reset parsing with gax's per-attempt shrinking deadline algorithm.
Dependencies: WU-1 (uses ResponseRecoveryStep), WU-2 (uses HttpException.retryable).
Files (create):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/RetrySettings.kt— immutable settings via Builder:totalTimeout,initialDelay,delayMultiplier,maxDelay,maxAttempts,jitter(fraction 0.0..1.0),retryableStatuses: Set<Int>,retryableMethods: Set<Method>.sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/RetryStep.kt— implements bothResponseRecoveryStep(decide retry on failure) and aRequestPipelineStep(records attempt start). UsesScheduledExecutorServicefor delay (neverThread.sleep).sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/BackoffCalculator.kt— exponential backoff + symmetric jitter; deadline-shrinking cap (per gax).sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/RetryAfterParser.kt—Retry-After(numeric seconds OR HTTP date) +X-RateLimit-Reset(Unix epoch) parsing. Precedence:Retry-Afternumeric >Retry-AfterHTTP-date >X-RateLimit-Reset> exponential fallback.
Files (modify):
docs/architecture.md— Cancellation section: cross-reference retry.
Critical rules:
- Idempotency-aware. Retry only when (a) request method is in
retryableMethods(default: GET/HEAD/OPTIONS/PUT/DELETE), OR (b) request body'sisReplayable()returns true. - No
Thread.sleep. UseScheduledExecutorServiceinjected via settings (default = lazily-created single-thread scheduler). - Interrupt-respect. If interrupted while waiting for next attempt: restore flag via
Thread.currentThread().interrupt(), throwInterruptedIOException. - Deadline shrinkage. Each attempt's effective timeout = min(per-attempt-timeout, totalTimeout - elapsed - nextDelay). Don't let the last attempt exceed the user's total budget.
Acceptance criteria:
- Retries on 429/500/502/503/504; does not retry on 400/401/404.
- Does not retry POST without replayable body.
- Honors
Retry-After: 5(5s wait, no jitter). - Honors
Retry-After: <HTTP-date>(parses date, computes delta). - Honors
X-RateLimit-Reset: <unix epoch>(positive jitter 100-120%). - Exponential fallback when no headers present, with symmetric jitter.
- Stops retrying at
maxAttemptsortotalTimeout, whichever first. - Tests cover virtual-thread safety (no carrier pinning during delay) using
Thread.ofVirtual().
Estimated complexity: High. ~4-5 days.
Status: shipped. HttpClient and AsyncHttpClient extend AutoCloseable with default no-op
close(); OkHttpTransport and JdkHttpTransport override it to release SDK-managed resources
(BYO clients are never closed).
Goal. Add AutoCloseable to HttpClient + AsyncHttpClient, plus close-on-error /
close-from-builder lifecycle. Wire transports to release their pools.
Dependencies: none.
Files (modify):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/client/HttpClient.kt— extendAutoCloseable. Default no-opclose().sdk-core/src/main/kotlin/org/dexpace/sdk/core/client/AsyncHttpClient.kt— same.sdk-transport-okhttp/src/main/kotlin/.../OkHttpTransport.kt— implementclose(): shutdownDispatcher.executorService(),ConnectionPool.evictAll().sdk-transport-jdkhttp/src/main/kotlin/.../JdkHttpTransport.kt— implementclose(): shutdown executor (if owned).
Acceptance criteria:
- Closing an
HttpClientreleases its resources (verified by checking the executor'sisShutdown()for OkHttp; JDK's HttpClient is GC'd). - Ownership distinction: user-supplied executors/dispatchers are NOT closed; SDK-created ones ARE.
close()is idempotent (multiple calls are safe).close()is interrupt-safe (interrupted shutdown propagates correctly).- Documented in
docs/architecture.md.
Estimated complexity: Low. ~1-1.5 days.
Status: shipped. IdempotencyKeyStep is in sdk-core/.../pipeline/step.
Goal. Auto-inject Idempotency-Key: UUID.randomUUID() for POST/PUT/PATCH if the
header isn't already set. Allow caller override + custom UUID strategy.
Dependencies: none.
Files (create):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/IdempotencyKeyStep.kt— implementsRequestPipelineStep. Configurable:header: String = "Idempotency-Key"methods: Set<Method> = setOf(POST, PUT, PATCH)keyStrategy: () -> String = { UUID.randomUUID().toString() }respectExisting: Boolean = true
Acceptance criteria:
- Injects header on POST/PUT/PATCH when absent.
- Skips when header already present (
respectExisting = true). - Replaces existing header when
respectExisting = false. - Custom
keyStrategyused when supplied. - Unit tests cover all method/strategy combinations.
Estimated complexity: Low. ~0.5-1 day.
Status: shipped. ClientIdentityStep is in sdk-core/.../pipeline/step; SdkInfo is in
sdk-core/.../util.
Goal. Adopt gax's composite token line. User-Agent and/or X-Dexpace-Client carrying
dexpace-sdk/<sdkver> jvm/<javaver> <transport>/<ver>.
Dependencies: none.
Files (create):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/ClientIdentityStep.kt— implementsRequestPipelineStep. Builds an ordered token list at construction (provided by builder); appends to existingUser-Agentrather than overwrites.sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/SdkInfo.kt— read SDK version from JAR manifest or generatedBuildInfoconstant; JVM version fromSystem.getProperty("java.version").
Files (modify):
sdk-core/build.gradle.kts— embed version in resources orBuildInfoconstant viabuildSrc/ generated source task.
Acceptance criteria:
- Default identity step emits
User-Agent: dexpace-sdk/<ver> jvm/<javaver>. - Transport adapters can register additional tokens (e.g. okhttp transport adds
okhttp/<ver>). - Existing user-set
User-Agentis preserved (appended to, not replaced) unless explicitly overridden. - Unit tests.
Estimated complexity: Low. ~1 day.
Status: shipped. HttpTracer, NoopHttpTracer, and HttpTracerFactory (whose default impl
is NoopHttpTracerFactory) are in sdk-core/.../instrumentation; InstrumentationContext carries
an httpTracerFactory slot defaulting to the no-op factory.
Goal. Extend InstrumentationContext with named, RPC-shape-aware events. Mirror gax's
ApiTracer vocabulary scaled to HTTP. Keep defaults no-op so the change is non-breaking.
Dependencies: none.
Files (create):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/HttpTracer.kt— new interface with default no-op methods:operationStarted(operationName: String?)operationSucceeded()operationFailed(error: Throwable)attemptStarted(attemptNumber: Int)attemptFailed(error: Throwable, nextDelayMillis: Long?)attemptRetriesExhausted(error: Throwable)requestUrlResolved(url: String)requestSent(byteCount: Long?)responseHeadersReceived(status: Int, headers: Headers)responseReceived(byteCount: Long?)connectionAcquired(host: String, port: Int)
sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/NoopHttpTracer.kt— singleton no-op impl.sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/HttpTracerFactory.kt— factory SPI;newTracer(operationName, attributes): HttpTracer.
Files (modify):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/InstrumentationContext.kt— addhttpTracerFactory: HttpTracerFactoryfield with no-op default; preserve existingSpanAPI.
Acceptance criteria:
- Default factory returns a no-op tracer (no performance regression).
- A test custom tracer captures every event and asserts the expected sequence for a typical request lifecycle.
- Wiring into existing pipeline/transport is documented as a follow-up (separate WU) so this WU stays small.
Estimated complexity: Low-Medium. ~2 days.
Status: shipped. The sdk-serde-jackson module is in settings.gradle.kts; it ships
JacksonSerde, JacksonObjectMappers, and TristateModule. The Tristate<T> type itself lives
in sdk-core/.../serde (it is part of the abstract surface, as planned).
Goal. New module providing a Jackson-backed Serde implementation with the right SDK
defaults (per Square: FAIL_ON_UNKNOWN_PROPERTIES=false, WRITE_DATES_AS_TIMESTAMPS=false,
Jdk8Module, JavaTimeModule). Plus a Tristate<T> sealed class for PATCH semantics.
Dependencies: none (new module).
Files (create):
sdk-serde-jackson/build.gradle.kts— JVM target Java 8; deps:sdk-core+com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2+jackson-datatype-jsr310+jackson-datatype-jdk8.sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/JacksonSerde.kt— implementsSerde<T>.sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/JacksonObjectMappers.kt—defaultObjectMapper(): ObjectMapperwith SDK defaults.sdk-core/src/main/kotlin/org/dexpace/sdk/core/serde/Tristate.kt(insdk-corebecause the type is part of the abstract surface) — sealed class withAbsent,Present<T>(value),Nullcases; Jackson serializer/deserializer lives insdk-serde-jackson.sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/TristateModule.kt— Jackson module registering ser/de forTristate<T>.
Files (modify):
settings.gradle.kts— includesdk-serde-jackson.docs/architecture.md— register the new module.- Root README.md — list new module.
Acceptance criteria:
- Round-trip serde of a representative model (nested DTO, list, optional, tri-state field).
- Tri-state distinguishes absent vs. null vs. value at both serialize and deserialize.
FAIL_ON_UNKNOWN_PROPERTIES=falseverified by deserializing a JSON with an extra field.- ISO-8601 dates by default (not numeric timestamps).
- Builder-pattern friendly (recognizes
@JsonDeserialize(builder=...)and@JsonPOJOBuilder).
Estimated complexity: Medium. ~2-3 days.
Status: shipped. Page, Paginator, PaginationStrategy, and the three strategies
(Cursor / PageNumber / LinkHeader) are in sdk-core/.../pagination, alongside
helper types SimplePage and RequestRebuilder. Paginator gained a maxPages safety cap
(default Long.MAX_VALUE) beyond the original sketch, to bound runaway iteration against servers
that never advance their cursor.
Goal. Add a generic pagination primitive set sized to cover the common cursor / page-number / link-header strategies without over-engineering. Sync first; async adapter follow-up.
Dependencies: none (new package).
Files (create):
sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/Page.kt— interface:items: List<T>,hasNext: Boolean,nextPageRequest(): Request?.sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/Paginator.kt— class wrappingHttpClient+ initialRequest+PaginationStrategy<T>. ExposesiterateAll(): Iterable<T>andstreamAll(): Stream<T>.sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/PaginationStrategy.kt— interface:parse(Response): Page<T>. Implementations:CursorPaginationStrategy<T>(cursorPath, itemsPath, parser)— readnext_cursorfrom bodyPageNumberPaginationStrategy<T>(pageParam, itemsPath, parser)— increment page numberLinkHeaderPaginationStrategy<T>(itemsPath, parser)— RFC 5988Link: <url>; rel="next"
sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/PaginatorTests.kt(test) — table-driven tests against MockWebServer fixtures.
Acceptance criteria:
- Each strategy handles its golden-path fixture.
iterateAll()is lazy — exhausting one page triggers exactly one fetch for the next.- Empty page short-circuits even if
hasNext = true(defensive, matches Square's pattern). - Strategies handle URL parsing safely (Link header values with quoted segments, special chars).
- Strategy interface allows future
BiDirectionalPaginationStrategywithout breaking changes.
Estimated complexity: Medium. ~3-4 days.
Status: shipped, but not as the sdk-auth module this WU sketched. Auth landed inside
sdk-core rather than a standalone module, and the type names differ from the sketch below — the
abstractions are credential-and-challenge-shaped instead of AuthProvider-shaped:
- Credentials (
sdk-core/.../http/auth): a sealedCredentialinterface withBearerToken(plusBearerTokenProviderfor rotation) andKeyCredential/NamedKeyCredential. NoAuthProvider/BasicAuthProvider/OAuth2ClientCredentialsProvidertypes — the planned OAuth client-credentials flow did not ship. - Challenge handling (RFC 7235):
AuthChallengeParser+AuthenticateChallenge, withBasicChallengeHandler,DigestChallengeHandler, andCompositeChallengeHandler. - Pipeline steps (
sdk-core/.../http/pipeline/steps): an abstractAuthSteppillar atStage.AUTH, with concreteBearerTokenAuthStepandKeyCredentialAuthStep. The challenge retry hook lives onAuthStepitself rather than in a separateUnauthorizedRecoveryStep.
The remaining body of this WU is the original sketch, kept for the design rationale; the type names and module layout above are authoritative.
Goal. Pluggable authentication. Pipeline-step-based, supports per-call override via
RequestContext, coalesces concurrent token refreshes, evicts cached tokens on 401.
Dependencies: WU-1 (uses ResponseRecoveryStep for 401-handling), WU-2 (uses
UnauthorizedException), WU-3 (uses retry semantics for OAuth token-fetch).
Files (create):
sdk-auth/build.gradle.kts— JVM Java 8; deps:sdk-core.sdk-auth/src/main/kotlin/org/dexpace/sdk/auth/AuthProvider.kt— interface:apply(request: Request): Request. Optionalrefresh(): AuthProviderfor token rotation.sdk-auth/src/main/kotlin/org/dexpace/sdk/auth/BasicAuthProvider.kt— Base64 ofuser:pass. Cached, thread-safe.sdk-auth/src/main/kotlin/org/dexpace/sdk/auth/BearerTokenAuthProvider.kt— static orSupplier<String>-backed bearer token.sdk-auth/src/main/kotlin/org/dexpace/sdk/auth/ApiKeyAuthProvider.kt— configurable header or query-param injection.sdk-auth/src/main/kotlin/org/dexpace/sdk/auth/OAuth2ClientCredentialsProvider.kt— full client_credentials flow: token endpoint, scopes, expiry buffer,Clockinjection, coalesced refresh viaConcurrentHashMap<CacheKey, CompletableFuture<Token>>.computeIfAbsent.sdk-auth/src/main/kotlin/org/dexpace/sdk/auth/AuthStep.kt—RequestPipelineStepthat readsAuthProviderfromRequestContext(per-call) or from a client-default.sdk-auth/src/main/kotlin/org/dexpace/sdk/auth/UnauthorizedRecoveryStep.kt—ResponseRecoveryStepthat onUnauthorizedExceptionevicts the cached token and retries once.
Files (modify):
settings.gradle.kts— includesdk-auth.sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/context/RequestContext.kt— add optionalauthProvider: AuthProvider?slot (just a typed key; sdk-core has nosdk-authdependency, so use a generic attribute map).
Anti-patterns to avoid (from Expedia/Airbyte):
- No
synchronizedaround network calls. UseReentrantLockfor cache mutation,CompletableFuturefor refresh coalescing. - No
Thread.sleep, no.join()mid-pipeline. - No per-request reflection. Concrete typed providers only.
- Refresh must NOT block other unrelated requests — partition by cache key.
Acceptance criteria:
- Concurrent calls during token expiry trigger exactly one refresh fetch.
- 401 response triggers eviction + single retry; second 401 propagates
UnauthorizedException. - Per-call override via
RequestContextwins over client-default. - OAuth token storage immutable;
Clockinjected for tests. - All blocking calls respect
Thread.interrupt(). - Virtual-thread safety verified.
Estimated complexity: High. ~5-6 days.
Out of scope for the phases above. Two of the original four have since shipped:
- WU-11: webhook-signature verification — HMAC-SHA256/SHA1 verifier with constant-time compare + replay protection (Square's
WebhooksHelperdone right). Still unbuilt. - WU-12: metrics seam — shipped in
sdk-core/.../instrumentation/metrics(Meter,LongCounter,DoubleHistogram,NoopMeter), distinct from the tracing vocabulary. - Configuration Settings → Context resolution split — gax's
StubSettings→ClientSettings→ClientContextpattern adapted. Still unbuilt. - Streaming responses (SSE) — shipped as the
sdk-core/.../http/ssepackage (ServerSentEvent,ServerSentEventReader,ServerSentEventListener); the Reactor adapter exposes the SSE →Fluxbackpressure path.
Run order:
- Phase 0 launch. Dispatch WU-1 and WU-2 as two parallel subagents. Each works on a git worktree (or branch) so they don't fight.
- Phase 0 gate. When both are merged to a
phase-0integration branch, run./gradlew build+ tests to confirm baseline. - Phase 1 launch. Dispatch WU-3 through WU-9 as seven parallel subagents off the
phase-0branch. Each on its own worktree/branch. - Phase 1 integration. Merge in order: WU-4 → WU-7 → WU-6 → WU-8 → WU-9 → WU-5 → WU-3 (retry last because it touches the most surface). Resolve conflicts only if they appear. After each merge,
./gradlew build. - Phase 2 launch. Dispatch WU-10 against the integrated
phase-1branch. - Phase 2 integration. Merge to main. Tag release.
Per-WU exit criteria (apply to every subagent):
- All new code has unit tests with meaningful assertions.
./gradlew buildpasses on its branch../gradlew :sdk-core:compileKotlinpasses (faster smoke check during dev).- Strict-mode Kotlin: every public declaration has explicit visibility + explicit return type.
- Public API stable: no public types removed or signatures changed without explicit callout in the WU summary.
docs/updated for any new public concept.
When dispatching a subagent for a work unit, the briefing should include:
You are implementing WU-X: <title>.
## Context
- Repo: /Users/omar/IdeaProjects/dexpace/java-sdk
- Branch: phase-N/wu-X (create from <base-branch>)
- Plan reference: docs/implementation-plan.md § WU-X
## Goal
<paste WU goal>
## Files to create
<paste list>
## Files to modify
<paste list>
## Hard constraints (project-wide)
- Java 8 bytecode target; no Java 9+ APIs.
- Zero non-SLF4J runtime deps in sdk-core.
- ReentrantLock not synchronized; respect Thread.interrupt().
- Explicit Kotlin visibility (Strict mode); @JvmSynthetic on Java-mangled internals.
- Immutable models with private constructor + Builder.
- @JvmOverloads on public constructors.
- No AI attribution in commits.
## Conflict surface (avoid)
<paste from Conflict Map for this WU>
## Acceptance criteria
<paste from WU>
## Done means
- Tests pass: ./gradlew build
- Code lands on phase-N/wu-X branch ready to merge
- Brief summary of what changed, with file:line citations