Initial Commit
This commit is contained in:
96
openspec/changes/retry-circuit-breaker-wrapper/proposal.md
Normal file
96
openspec/changes/retry-circuit-breaker-wrapper/proposal.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Retry — `withCircuitBreaker` convenience wrapper
|
||||
|
||||
**Package**: `@suaveplan/retry`
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/retry` ships both retry strategies (`retry`, `retryAttempt`, `retrySync`) and a circuit breaker (`CircuitBreaker` with closed → open → half-open state machine), but it does not ship the convenience wrapper that combines them. Consumers wanting "retry until the breaker opens, then short-circuit" — the canonical resilience pattern — currently have to wire it themselves:
|
||||
|
||||
```ts
|
||||
const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 })
|
||||
const result = await retry(() => breaker.execute(fn), { maxAttempts: 3, strategy: 'exponential' })
|
||||
return result.value
|
||||
```
|
||||
|
||||
This is correct but error-prone — the order matters (retry outside, breaker inside), the unwrap of `result.value` is easy to forget, and the breaker handle is leaked across the call site if anyone needs to inspect state. Upstream shipped `withCircuitBreaker(cbConfig, retryConfig?)` (`packages/universal/retry/src/retry/retry.ts` L241-L260) which packages this composition behind a single factory call returning `{ execute, getCircuitBreaker }`. Genesis dropped it without a replacement.
|
||||
|
||||
The 2026-05-02 audit found nine genesis packages already depending on `@suaveplan/retry` (`auth`, `auth-jwt-tenant`, `db-sqlite`, `surrealdb`, `network-websockets`, `webhook-client`, `storage-s3`, `elevenlabs`, `retry` itself), and at least three of them (`webhook-client`, `storage-s3`, `network-websockets`) are exactly the kind of "remote network call → retry with breaker" use case the wrapper was designed for. They currently re-implement the composition inline, which is the opposite of "build feature-rich, production-grade packages" (CLAUDE.md §Workflow rule 3).
|
||||
|
||||
## What changes
|
||||
|
||||
### `@suaveplan/retry`
|
||||
|
||||
- ADDED: `withCircuitBreaker<TReturn>(options: WithCircuitBreakerOptions): WithCircuitBreakerHandle<TReturn>` — composition factory.
|
||||
- ADDED: `WithCircuitBreakerOptions` interface — `{ breakerConfig: CircuitBreakerConfig; retryConfig?: RetryConfig }` (named for clarity; the legacy positional form is rejected — see Out of scope).
|
||||
- ADDED: `WithCircuitBreakerHandle<TReturn>` interface — `{ execute<T>(fn: () => Promise<T>): Promise<T>; getCircuitBreaker(): CircuitBreaker }`. The `execute` generic threads through `T`, not the outer `TReturn`, because the same handle can run heterogeneous functions.
|
||||
- ADDED: subpath export `./compose/with-circuit-breaker` in `package.json::exports`.
|
||||
- ADDED: error code `RETRY.CIRCUIT_OPEN_AFTER_RETRY` raised when the breaker opens *during* a retry pass and short-circuits the remaining attempts. The thrown `RetryError` carries the underlying `CircuitBreakerError` as `cause`.
|
||||
- ADDED: OTel span `retry.with-circuit-breaker.execute` wrapping the entire composition; child spans from the existing `retry` and `circuit-breaker` instrumentation nest underneath. Counter `retry.with-circuit-breaker.invocation` increments per `execute` call.
|
||||
- MODIFIED: `src/index.ts` re-exports `withCircuitBreaker` from the root barrel.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Positional `(breakerConfig, retryConfig)` signature.** Legacy upstream took two positional args; genesis takes a single options object for ergonomic parity with every other genesis factory. Callers porting from upstream change one call site.
|
||||
- **`retrySync` + breaker composition.** `retrySync` is a rare path; the breaker's `execute` is async because the half-open state involves a timer. A sync composition would have to drop the half-open behaviour. Deferred until a real consumer asks.
|
||||
- **AbortSignal threading.** `retry` already accepts an `AbortSignal`; `CircuitBreaker.execute` does too. The wrapper passes them through but does not introduce a new signal. Callers wanting cancellation supply it via `retryConfig.signal`.
|
||||
- **Breaker-state observers as wrapper option.** The `CircuitBreaker` already publishes events via `@suaveplan/events`; `getCircuitBreaker()` returns the live handle so callers subscribe directly. We do not duplicate that surface.
|
||||
- **Auto-recovery of stale breaker state across processes.** The breaker is process-local. Distributed breaker state is a separate package's problem.
|
||||
|
||||
## Parity Checklist
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| `withCircuitBreaker(cbConfig, retryConfig?)` | ✅ | ⬆️ | Signature reshaped to `withCircuitBreaker({ breakerConfig, retryConfig? })`. Same execution semantics: outer retry, inner breaker. |
|
||||
| `{ execute, getCircuitBreaker }` handle | ✅ | ✅ | Verbatim shape. |
|
||||
| Retry-only path when `retryConfig` omitted | ✅ | ✅ | `breaker.execute(fn)` called directly; no retry. |
|
||||
| `result.value` unwrapping | ✅ | ✅ | Wrapper handles the `RetryResult` unwrap so callers receive `T` directly. |
|
||||
| Structured `RetryError(RETRY.CIRCUIT_OPEN_AFTER_RETRY)` | 🚫 (legacy threw `CircuitBreakerError` raw) | ⬆️ | Genesis-wide error-routing rule: every error gets a stable code on the package's own error type, with the underlying `CircuitBreakerError` chained as `cause`. |
|
||||
| OTel span / counter | 🚫 (not in upstream) | ⬆️ | Genesis-wide observability rule. |
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/packages/universal/retry/src/retry/retry.ts`.
|
||||
|
||||
| Requirement | Source file | Source lines | Notes |
|
||||
|---|---|---|---|
|
||||
| `withCircuitBreaker` composition (outer retry, inner breaker) | `packages/universal/retry/src/retry/retry.ts` | L241-L260 | Verbatim semantics. |
|
||||
| `{ execute, getCircuitBreaker }` handle shape | `packages/universal/retry/src/retry/retry.ts` | L247-L259 | Verbatim. |
|
||||
| `result.value` unwrap inside `execute` | `packages/universal/retry/src/retry/retry.ts` | L249-L254 | Verbatim. |
|
||||
| Retry-only path when `retryConfig` is undefined | `packages/universal/retry/src/retry/retry.ts` | L255-L256 | Verbatim. |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| Options-object signature `withCircuitBreaker({ breakerConfig, retryConfig? })` | Genesis-wide ergonomic rule — every factory takes one named-options object. The legacy positional shape is harder to read at call sites and harder to extend without breaking changes. |
|
||||
| `RETRY.CIRCUIT_OPEN_AFTER_RETRY` error code with `cause` chain | Genesis-wide error-routing rule. Legacy let `CircuitBreakerError` propagate raw; genesis wraps it so callers can `instanceof RetryError` AND inspect `.cause`. |
|
||||
| OTel span `retry.with-circuit-breaker.execute` and counter `retry.with-circuit-breaker.invocation` | Genesis-wide observability rule. Upstream had no tracing on the wrapper. |
|
||||
| Subpath export `./compose/with-circuit-breaker` | Genesis-wide subpath rule (AGENTS.md §6) — every feature module gets a tree-shakeable subpath. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/compose/with-circuit-breaker/with-circuit-breaker.test.ts` — composition runs `n` retries inside breaker; breaker opens mid-retry and short-circuits remaining attempts; retry-only path when `retryConfig` is omitted; `getCircuitBreaker()` returns the live handle.
|
||||
- `src/compose/with-circuit-breaker/with-circuit-breaker.spec-conformance.test.ts` — `RetryError(RETRY.CIRCUIT_OPEN_AFTER_RETRY)` thrown when breaker opens during retry, with `cause` set to `CircuitBreakerError`; OTel span emitted; counter increments per `execute`; subpath export resolves.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/retry` to enumerate. Nine packages depend on `@suaveplan/retry` today; this change adds a new export so none are broken.
|
||||
|
||||
- `@suaveplan/third-party-elevenlabs` — [ ] tests run / [ ] docs reviewed / [x] no action needed (uses `retry` only; new wrapper is additive)
|
||||
- `@suaveplan/network-websockets` — [x] tests run / [x] docs reviewed / [ ] no action needed (good candidate for the wrapper; doc recommends migration but does not require it in this change)
|
||||
- `@suaveplan/jwt-tenant` — [ ] tests run / [ ] docs reviewed / [x] no action needed (uses `retry` only)
|
||||
- `@suaveplan/auth` — [ ] tests run / [ ] docs reviewed / [x] no action needed (uses `retry` only)
|
||||
- `@suaveplan/storage-s3` — [x] tests run / [x] docs reviewed / [ ] no action needed (good candidate; doc recommends migration)
|
||||
- `@suaveplan/surrealdb` — [ ] tests run / [ ] docs reviewed / [x] no action needed (uses `retry` only)
|
||||
- `@suaveplan/db-sqlite` — [ ] tests run / [ ] docs reviewed / [x] no action needed (uses `retry` only)
|
||||
- `@suaveplan/webhook-client` — [x] tests run / [x] docs reviewed / [ ] no action needed (good candidate; doc recommends migration)
|
||||
|
||||
The dependent sweep `bunx turbo test --filter=...@suaveplan/retry` runs in §5.6 of `tasks.md` to verify no consumer compiles or tests broken by the new export.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. No existing `retry` / `retrySync` / `CircuitBreaker` surface is touched.
|
||||
@@ -0,0 +1,76 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: retry (retry-circuit-breaker-wrapper)
|
||||
|
||||
**Package**: `@suaveplan/retry`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `withCircuitBreaker` composition factory
|
||||
|
||||
The package SHALL expose `withCircuitBreaker(options: WithCircuitBreakerOptions): WithCircuitBreakerHandle` where `WithCircuitBreakerOptions = { breakerConfig: CircuitBreakerConfig; retryConfig?: RetryConfig }`. The factory SHALL construct a fresh `CircuitBreaker` from `breakerConfig` and return a handle exposing two methods: `execute<T>(fn: () => Promise<T>): Promise<T>` and `getCircuitBreaker(): CircuitBreaker`.
|
||||
|
||||
When `retryConfig` is supplied, `execute(fn)` SHALL run `retry(() => breaker.execute(fn), retryConfig)` and return the inner `result.value` so callers receive `T` directly rather than a `RetryResult<T>` wrapper.
|
||||
|
||||
When `retryConfig` is omitted, `execute(fn)` SHALL delegate directly to `breaker.execute(fn)` (no retry).
|
||||
|
||||
#### Scenario: retry composes inside the breaker
|
||||
|
||||
- **WHEN** a caller invokes `withCircuitBreaker({ breakerConfig: { failureThreshold: 5, resetTimeoutMs: 30000 }, retryConfig: { maxAttempts: 3, strategy: 'exponential', baseDelayMs: 10 } })` AND the wrapped `fn` fails twice then succeeds
|
||||
- **THEN** `execute(fn)` returns the success value AND `breaker.execute` was called 3 times AND the breaker's failure counter recorded 2 failures + 1 success
|
||||
|
||||
#### Scenario: retry-only path when retryConfig omitted
|
||||
|
||||
- **WHEN** a caller omits `retryConfig` AND invokes `execute(fn)` against a function that fails
|
||||
- **THEN** `breaker.execute(fn)` is called exactly once AND the rejection propagates without retry
|
||||
|
||||
#### Scenario: getCircuitBreaker returns the live handle
|
||||
|
||||
- **WHEN** a caller invokes `getCircuitBreaker()` AND inspects `state` after a failed `execute`
|
||||
- **THEN** the returned `CircuitBreaker` reports the up-to-date state (`closed` / `open` / `half-open`) reflecting the failures observed
|
||||
|
||||
**Verified by:** `src/compose/with-circuit-breaker/with-circuit-breaker.test.ts::"retries inside breaker boundary and unwraps result.value"` AND `src/compose/with-circuit-breaker/with-circuit-breaker.test.ts::"runs without retry when retryConfig omitted"` AND `src/compose/with-circuit-breaker/with-circuit-breaker.test.ts::"getCircuitBreaker returns the live breaker handle"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Breaker-open during retry surfaces structured error
|
||||
|
||||
When the inner `breaker.execute` throws a `CircuitBreakerError` because the breaker opens during a retry pass, the wrapper SHALL convert the propagated rejection into `RetryError` with code `RETRY.CIRCUIT_OPEN_AFTER_RETRY`. The original `CircuitBreakerError` SHALL be set as the `cause` of the `RetryError`. Callers can `instanceof RetryError` AND inspect `.cause` to programmatically distinguish "all attempts exhausted" from "breaker opened mid-retry".
|
||||
|
||||
#### Scenario: breaker opens during retry
|
||||
|
||||
- **WHEN** a caller invokes `execute(fn)` where `fn` always rejects AND the breaker `failureThreshold` is hit before retry attempts are exhausted
|
||||
- **THEN** the call rejects with `RetryError(RETRY.CIRCUIT_OPEN_AFTER_RETRY)` AND `error.cause` is an instance of `CircuitBreakerError`
|
||||
|
||||
**Verified by:** `src/compose/with-circuit-breaker/with-circuit-breaker.spec-conformance.test.ts::"throws RetryError(RETRY.CIRCUIT_OPEN_AFTER_RETRY) with CircuitBreakerError cause when breaker opens mid-retry"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel observability for composed execution
|
||||
|
||||
`withCircuitBreaker(...).execute(fn)` SHALL open an OTel span named `retry.with-circuit-breaker.execute` for each invocation. The span SHALL nest under any active span supplied by the caller. The package SHALL increment a counter `retry.with-circuit-breaker.invocation` once per `execute` call regardless of outcome.
|
||||
|
||||
#### Scenario: span and counter emitted on every execute call
|
||||
|
||||
- **WHEN** a caller invokes `execute(fn)` 3 times under an in-memory `TracerProvider` + `MeterProvider`
|
||||
- **THEN** 3 spans named `retry.with-circuit-breaker.execute` are recorded AND the `retry.with-circuit-breaker.invocation` counter equals 3
|
||||
|
||||
**Verified by:** `src/compose/with-circuit-breaker/with-circuit-breaker.spec-conformance.test.ts::"emits retry.with-circuit-breaker.execute span and increments invocation counter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath export
|
||||
|
||||
The package SHALL expose the new module at the `./compose/with-circuit-breaker` subpath in `package.json::exports`. Importing `withCircuitBreaker` from that subpath SHALL NOT pull in unrelated `@suaveplan/retry` modules.
|
||||
|
||||
#### Scenario: subpath import resolves
|
||||
|
||||
- **WHEN** a caller writes `import { withCircuitBreaker } from "@suaveplan/retry/compose/with-circuit-breaker"`
|
||||
- **THEN** TypeScript resolves the import
|
||||
|
||||
**Verified by:** `src/compose/with-circuit-breaker/with-circuit-breaker.spec-conformance.test.ts::"subpath ./compose/with-circuit-breaker resolves and tree-shakes"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
77
openspec/changes/retry-circuit-breaker-wrapper/tasks.md
Normal file
77
openspec/changes/retry-circuit-breaker-wrapper/tasks.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user