Files
suaveplan/openspec/changes/retry-circuit-breaker-wrapper/tasks.md
2026-05-27 23:29:53 +01:00

6.3 KiB

Tasks — Retry withCircuitBreaker convenience wrapper

Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened.

0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

Audit Note (2026-05-02)

Minimal progress — DEFERRED, do not archive.

  • packages/flow/retry/src/ has circuit-breaker/ and retry/ modules but no compose/ directory.
  • grep -rln "withCircuitBreaker\|with-circuit-breaker\|compose/" packages/flow/retry/src/ returns zero hits.
  • bunx turbo lint typecheck test --filter=@suaveplan/retry is GREEN — but the composition wrapper is not implemented.
  • All 38 tasks remain: register RETRY.CIRCUIT_OPEN_AFTER_RETRY code, with-circuit-breaker.ts feature module with WithCircuitBreakerOptions + WithCircuitBreakerHandle<TReturn>, cause-chained RetryError wrapping of CircuitBreakerError, OTel span + counter, subpath export, sibling .md, conformance + property tests, dependent sweep across all 8 retry consumers.

1. Spec authoring

  • 1.1 Read ../suaveplan/packages/universal/retry/src/retry/retry.ts (withCircuitBreaker section, L222-L260)
  • 1.2 Read openspec/specs/retry/spec.md and the existing circuit-breaker.spec-conformance.test.ts
  • 1.3 Read openspec/AGENTS.md Sections 13 and 14
  • 1.4 Draft proposal.md from openspec/templates/proposal.md
  • 1.5 Fill in ## Source mapping table — every requirement cites a line range OR appears in the "no source line" table with justification
  • 1.6 Draft specs/retry/spec.md from openspec/templates/spec-delta.md
  • 1.7 Every requirement in the spec delta carries a **Verified by:** line naming concrete it(...) titles

2. Implementation

  • 2.1 Register error code RETRY.CIRCUIT_OPEN_AFTER_RETRY in src/errors/
  • 2.2 Create src/compose/with-circuit-breaker/ feature module with with-circuit-breaker.ts, with-circuit-breaker.types.ts, with-circuit-breaker.schemas.ts, with-circuit-breaker.md, index.ts
  • 2.3 Define WithCircuitBreakerOptions and WithCircuitBreakerHandle<TReturn> interfaces in with-circuit-breaker.types.ts
  • 2.4 Implement withCircuitBreaker(options) — compose retry(() => breaker.execute(fn), retryConfig) with result.value unwrap; retry-only path when retryConfig omitted
  • 2.5 Wrap CircuitBreakerError raised inside the retry pass with RetryError(RETRY.CIRCUIT_OPEN_AFTER_RETRY) carrying cause
  • 2.6 Wire OTel span retry.with-circuit-breaker.execute and counter retry.with-circuit-breaker.invocation
  • 2.7 Add subpath export ./compose/with-circuit-breaker to package.json::exports
  • 2.8 Re-export withCircuitBreaker from the root barrel src/index.ts

3. Tests

  • 3.1 Unit tests at src/compose/with-circuit-breaker/with-circuit-breaker.test.ts — composition runs retries inside breaker; breaker opens mid-retry and short-circuits remaining attempts; retry-only path when retryConfig omitted; getCircuitBreaker() returns live handle
  • 3.2 Spec-conformance tests at src/compose/with-circuit-breaker/with-circuit-breaker.spec-conformance.test.ts — RetryError(RETRY.CIRCUIT_OPEN_AFTER_RETRY) thrown with cause set to CircuitBreakerError; OTel span emitted; counter increments per execute; subpath export resolves
  • 3.3 Property test (fast-check): for any maxAttempts ∈ [1, 10] and any pre-failed attempts ∈ [0, maxAttempts], the wrapper either returns success OR throws RetryError with the breaker state observable via getCircuitBreaker()
  • 3.4 Every it(...) carries { timeout: 5000 }

4. Documentation

  • 4.1 src/compose/with-circuit-breaker/with-circuit-breaker.md — sibling doc (≥200 words, basic + advanced examples, full API reference)
  • 4.2 README links ./compose/with-circuit-breaker/with-circuit-breaker.md under ## Modules
  • 4.3 Update CLAUDE.md @suaveplan/retry entry to mention withCircuitBreaker

5. Pre-archive verification (the 5-gate triple)

  • 5.1 bun run check — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
  • 5.2 bunx turbo lint typecheck test --filter=@suaveplan/retry — green
  • 5.3 bunx turbo test:coverage --filter=@suaveplan/retry — 100/100/100/100
  • 5.4 bun scripts/openspec-conformance.ts @suaveplan/retry — exit 0, no error findings
  • 5.5 bunx turbo build --filter=@suaveplan/retry — green
  • 5.6 bunx turbo test --filter=...@suaveplan/retry — every dependent (auth, jwt-tenant, db-sqlite, surrealdb, network-websockets, webhook-client, storage-s3, elevenlabs) passes

6. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 6.1 Generics threaded through WithCircuitBreakerHandle<TReturn> and execute<T>; expectTypeOf confirms execute does not narrow to unknown
  • 6.2 Zod schema wired at WithCircuitBreakerOptions boundary
  • 6.3 RETRY.CIRCUIT_OPEN_AFTER_RETRY thrown by at least one production path
  • 6.4 Strict-mode behaviors tested (with retry vs without retry; breaker open during first attempt vs after multiple attempts)
  • 6.5 OTel span and counter emitted from production paths and asserted with a real in-memory TracerProvider + MeterProvider
  • 6.6 Lifecycle dedup — N/A (factory returns a fresh handle per call)
  • 6.7 No retroactive tick — every box above reflects work done in this change

7. Archive

  • 7.1 Fold spec delta into openspec/specs/retry/spec.md
  • 7.2 Move openspec/changes/retry-circuit-breaker-wrapper/ → openspec/archive/<yyyy-mm-dd>-retry-circuit-breaker-wrapper/
  • 7.3 Refresh MANIFEST.md if dep set changed (no expected change)