@hegemonart/get-design-done 1.33.5 → 1.33.6

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
8
- "version": "1.33.5"
8
+ "version": "1.33.6"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
15
- "version": "1.33.5",
15
+ "version": "1.33.6",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.33.5",
4
+ "version": "1.33.6",
5
5
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain. v1.27.7 ships gdd-mcp (Phase 27.7): 12 read-only MCP tools for sub-3s priming. v1.28.0 (Phase 28): Foundational References Tier 2 — 5 new reference files (color-theory, composition, proportion-systems, i18n, contrast-advanced), 2 verifier i18n probes + 1 explore i18n-readiness probe, 12 additive cross-link insertions across 10 existing references, 2 orthogonal audit-scoring lens-tags (composition_alignment + i18n_readiness).",
6
6
  "author": {
7
7
  "name": "hegemonart",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.33.6] - 2026-05-31
8
+
9
+ ### Phase 33.6 — OpenRouter Provider Adapter
10
+
11
+ Adds **OpenRouter** as a tier-resolver provider so users can route any agent's tier (`opus`/`sonnet`/`haiku`) through OpenRouter's aggregator catalog (one API key → Claude/GPT/Llama/Gemini/DeepSeek) **alongside** native provider auth. This introduces the plugin's **first plugin-side outbound REST client** — it lands under the Phase-33.5 audited outbound baseline (the catalog-fetcher's egress is explicitly allowlisted, the outbound CI gate stays green). A decimal release on the v1.33.x arc (CHANGELOG-only, D-01); no new runtime dependency (Node built-in `fetch` only, D-10). OpenRouter is **opt-in alongside** native auth, never OpenRouter-only (D-08).
12
+
13
+ ### Added
14
+
15
+ - **Dynamic OpenRouter catalog fetcher.** `scripts/lib/openrouter/catalog-fetcher.cjs` fetches `https://openrouter.ai/api/v1/models`, maps it into the cache shape, and writes it ATOMICALLY to `.design/cache/openrouter-models.json` (gitignored runtime artifact) with a **24h TTL** skip-if-fresh (D-02, configurable via `.design/config.json#openrouter_catalog_ttl_hours`). The fetch is gated behind an **injectable `fetchImpl`** (default global `fetch`) so the entire default test suite is hermetic — no live network in `npm test` (D-07) — and uses the `sdk/primitives` jittered-backoff + error-classifier + `rate-guard` for bounded transient/rate-limit retry. The `fetch(` egress is allowlisted via `scripts/lib/openrouter/**` in `scripts/security/outbound-allowlist.json`, with a matching egress entry in `reference/gdd-threat-model.md` (D-06). The `OPENROUTER_API_KEY` is sent only as an `Authorization: Bearer` header and is never persisted to the cache.
16
+ - **OpenRouter tier-resolver adapter.** `scripts/lib/tier-resolver-openrouter.cjs` — `resolve(tier) → openrouter-model-id | null` — maps GDD's `opus`/`sonnet`/`haiku` vocabulary (D-04) onto a concrete catalog id via a deterministic closed-vs-open + completion-price heuristic (opus = top closed, sonnet = mid/top-open, haiku = cheap open), with a `.design/config.json#openrouter_tier_overrides` escape hatch that wins verbatim (D-03). Never throws; an absent key / missing cache / unknown tier degrades to `null` so the caller falls back to the native provider via the existing Phase-26 tier-resolver chain (graceful-degrade — D-08). `reference/openrouter-tier-mapping.md` documents the heuristic and is registered in `reference/registry.json` in the same plan that created it (D-11).
17
+ - **OpenRouter connection + status skill.** `connections/openrouter.md` (the Phase-14 connection spec — Setup / Probe Pattern / Tools / Pipeline Integration / Fallback Behavior) and the `/gdd:openrouter-status` skill (`skills/openrouter-status/SKILL.md`) report catalog freshness, the resolved tier→model mapping, and override state.
18
+ - **Optional `cost.update` provider tag (back-compat).** The `cost.update` event payload gains an OPTIONAL `provider` field, set to `'openrouter'` when the adapter resolved the model; absent otherwise. Additive — the events JSON schema is unchanged, so existing consumers are unaffected.
19
+ - **OpenRouter price sub-table + catalog-drift watch.** `reference/prices.openrouter.md` (a catalog-derived view; the dynamic catalog stays the source of truth) and an authority-watcher catalog-drift classifier (`diffOpenRouterCatalog`) that surfaces deprecated/withdrawn models matching `openrouter_tier_overrides` (noise-controlled — new-model / pricing-change are classified but not surfaced).
20
+ - **Regression baseline.** `test/fixtures/baselines/phase-33-6/` freezes the OpenRouter surface — a golden tier-resolution snapshot (the `opus`/`sonnet`/`haiku` ids the adapter resolves from the shared fixture catalog) plus encoded TTL / fallback-no-key / drift-on-synthetic-deprecation expectations — pinned by `test/suite/phase-33-6-baseline.test.cjs` so a future change cannot silently undo the heuristic, the TTL, the graceful-degrade, or the drift classifier.
21
+
22
+ ### Notes
23
+
24
+ - **OpenRouter is a tier-RESOLUTION-layer adapter, not an install-registry runtime (D-12).** SC#5's "runtimes.cjs extension" wording is reinterpreted: `scripts/lib/install/runtimes.cjs` is the Phase-24-locked install matrix (how to install GDD *into* a runtime; guarded at exactly 16 entries) — you don't install GDD "into" OpenRouter. OpenRouter lives only in the tier-resolution adapter (`scripts/lib/tier-resolver-openrouter.cjs`), which reads the dynamic catalog rather than the install registry, so it fully delivers SC#5's intent without polluting or weakening the Phase-24 install lock. `install/runtimes.cjs` and its 16-count guard are preserved intact.
25
+ - All Phase 33.6 tests are hermetic (injected stub fetch + a fixture catalog; no network, no real `OPENROUTER_API_KEY`), so the default `npm test` stays green (D-07), and `npm run scan:outbound` stays green with the OpenRouter egress allowlisted (D-06).
26
+ - The 31.5 tarball golden (`test/fixtures/baselines/phase-31-5/tarball-manifest.txt`) was regenerated as a reviewed delta: **+6** shipped OpenRouter files (`connections/openrouter.md`, `reference/openrouter-tier-mapping.md`, `reference/prices.openrouter.md`, `scripts/lib/openrouter/catalog-fetcher.cjs`, `scripts/lib/tier-resolver-openrouter.cjs`, `skills/openrouter-status/SKILL.md`), zero removals (627 paths).
27
+ - 6-manifest lockstep at **v1.33.6** (`package.json` + `package-lock.json` (root + `packages.""`) + `.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json` (metadata.version + plugins[0].version) + `.cursor-plugin/plugin.json` + `.codex-plugin/plugin.json`). Version-sync hygiene done upfront (D-09): `OFF_CADENCE_VERSIONS.add('1.33.6')` + the 14 live-pinned `manifests-version.txt` baselines forward-propagated 1.33.5 → 1.33.6.
28
+
29
+ ---
30
+
7
31
  ## [1.33.5] - 2026-05-31
8
32
 
9
33
  ### Phase 33.5 — GDD Runtime Security Hardening
package/README.md CHANGED
@@ -100,6 +100,10 @@ Closes the **outbound** half of multi-runtime: gdd agents now OPTIONALLY delegat
100
100
 
101
101
  See [docs/PEER-DELEGATION.md](docs/PEER-DELEGATION.md) for the ops guide (when delegation fires, fallback diagnostics, broker lifecycle, Windows quirks) and [reference/peer-protocols.md](reference/peer-protocols.md) for the ACP + ASP protocol cheat sheet.
102
102
 
103
+ ### OpenRouter provider (v1.33.6, opt-in)
104
+
105
+ You can route any agent's tier (`opus`/`sonnet`/`haiku`) through **OpenRouter** — an aggregator that fronts Claude, GPT, Llama, Gemini, DeepSeek and more behind a single API key — *alongside* your native provider auth, never instead of it. Set `OPENROUTER_API_KEY` and GDD's tier-resolver adapter dynamically fetches the OpenRouter catalog (24h TTL cache) and maps each tier onto a concrete model via a closed-vs-open + pricing heuristic, with a `.design/config.json#openrouter_tier_overrides` escape hatch for pinning exact ids. When the key is absent or the catalog is unreachable, resolution gracefully falls back to your native provider — OpenRouter is purely additive. See [`connections/openrouter.md`](connections/openrouter.md) for setup, the probe pattern, and fallback behavior, and run `/gdd:openrouter-status` to inspect catalog freshness and the resolved tier→model mapping.
106
+
103
107
  ### Previous releases
104
108
 
105
109
  - **v1.26.0** — Headless Model Resolver (per-runtime tier→model map, `resolved_models` router field, per-runtime price tables, `reasoning-class` runtime-neutral alias).
package/SKILL.md CHANGED
@@ -92,6 +92,7 @@ Each stage produces artifacts in `.design/` inside the current project.
92
92
  | `quality-gate` | `get-design-done:quality-gate` | Phase 25 — parallel lint/type/test/visual command runner; classifies failures via quality-gate-runner agent |
93
93
  | `turn-closeout` | `get-design-done:turn-closeout` | Phase 25 — Stop-hook mirror skill; finalizes per-turn STATE blocks and emits closeout events |
94
94
  | `bandit-status` | `get-design-done:bandit-status` | Phase 27.5 — read-only diagnostic surface for the bandit posterior; per-(agent, bin, delegate, tier) snapshots (alpha, beta, mean, stddev, count, last-used). Use `/gdd:bandit-reset` to mutate. |
95
+ | `openrouter-status [--refresh]` | `get-design-done:gdd-openrouter-status` | Phase 33.6 — read-only OpenRouter catalog + tier-mapping diagnostic; surfaces catalog freshness (vs 24h TTL), last-fetch, resolved opus/sonnet/haiku → model mappings, per-tier preview. `--refresh` re-fetches (needs `OPENROUTER_API_KEY`). |
95
96
  | `peers` | `get-design-done:peers` | Phase 27 — `/gdd:peers` capability matrix command; shows installed peer-CLIs (codex/gemini/cursor/copilot/qwen), allowlist status, claimed roles, posterior delta vs local |
96
97
  | `peer-cli-customize` | `get-design-done:peer-cli-customize` | Phase 27 — rewire role→peer mappings on a per-agent basis (edits frontmatter `delegate_to:` directly) |
97
98
  | `peer-cli-add` | `get-design-done:peer-cli-add` | Phase 27 — guided ladder for adding a brand-new peer (verification ladder + adapter scaffolding + capability-matrix update) |
@@ -115,6 +115,10 @@ Apply the decision table below to each new entry. Emit `{ ...entry, classificati
115
115
 
116
116
  The skip row is evaluated LAST and overrides the kind-based row — a component-system release titled "Sponsored: shipping our new sponsor tier" still ends up `skip`.
117
117
 
118
+ ### OpenRouter catalog drift (Phase 33.6, SC#8)
119
+
120
+ Beyond the design-authority feeds above, the **OpenRouter model catalog** (`.design/cache/openrouter-models.json`, fetched by `scripts/lib/openrouter/catalog-fetcher.cjs`) is a **weekly-diff feed**. Diff the prior vs current catalog via `scripts/lib/authority-watcher/index.cjs#diffOpenRouterCatalog(prevModels, currModels, { overrides })`, which classifies each delta as `new-model` / `pricing-change` / `deprecated` / `withdrawn`. To keep the report actionable and quiet, **surface ONLY `deprecated`/`withdrawn` entries whose id matches a configured `.design/config.json#openrouter_tier_overrides` pin** — i.e. the user pinned a model that is going away. `new-model` and `pricing-change` deltas are classified (returned, `surfaced:false`) but never surfaced as alerts (noise control). When OpenRouter is not configured (no catalog), this feed is silently skipped.
121
+
118
122
  ## Step 6 — Write Snapshot
119
123
 
120
124
  For each feed, merge the newly-fetched entries into `feeds[feed-id].entries`:
@@ -23,6 +23,7 @@ This directory contains connection specifications for external tools and MCPs th
23
23
  | pencil.dev | Active | [`connections/pencil-dev.md`](connections/pencil-dev.md) | File-based; `.pen` YAML specs; git-tracked; no MCP |
24
24
  | 21st.dev Magic MCP | Active | [`connections/21st-dev.md`](connections/21st-dev.md) | Uses `mcp__21st*` tools; `TWENTY_FIRST_API_KEY` required |
25
25
  | Magic Patterns | Active | [`connections/magic-patterns.md`](connections/magic-patterns.md) | Claude connector (`mcp__magic_patterns*`) + API key fallback |
26
+ | OpenRouter | Active | [`connections/openrouter.md`](connections/openrouter.md) | Model-router (no MCP); env: `OPENROUTER_API_KEY` (optional `OPENROUTER_BASE_URL`); opt-in tier-resolution overlay, graceful-degrade-to-native |
26
27
 
27
28
  ---
28
29
 
@@ -45,6 +46,7 @@ Each cell describes what the connection contributes at that pipeline stage, or `
45
46
  | pencil.dev | `.pen` discovery | `.pen` as canonical design source | — | pencil-writer: annotate/roundtrip | spec-vs-impl diff | ✓ | — |
46
47
  | 21st.dev | — | prior-art gate: marketplace search before greenfield build | — | component-generator (21st impl) | — | — | ✓ |
47
48
  | Magic Patterns | — | — | — | component-generator (magic-patterns impl) | preview_url → `? VISUAL` check | — | ✓ |
49
+ | OpenRouter | — | — | — | — | — | — | ✓ (model-router: tier→model resolution, all stages) |
48
50
 
49
51
  **Column definitions:**
50
52
 
@@ -0,0 +1,86 @@
1
+ # OpenRouter — Connection Specification
2
+
3
+ This file is the connection specification for OpenRouter within the get-design-done pipeline. OpenRouter is a **model aggregator** — one API key fronts dozens of upstream providers (Anthropic Claude, OpenAI GPT, Meta Llama, Google Gemini, DeepSeek, Qwen, Mistral, …). GDD treats it as a **tier-router**: the Phase-33.6 adapter (`scripts/lib/tier-resolver-openrouter.cjs`) maps GDD's `opus`/`sonnet`/`haiku` tiers onto a concrete OpenRouter catalog model id. It is **not** a canvas or visual-design tool — it does not read or write design surfaces. See `connections/connections.md` for the full connection index and capability matrix.
4
+
5
+ OpenRouter is **opt-in alongside** native provider auth, never OpenRouter-only (D-08). When it is not configured, tier resolution falls back to the native provider via the existing `scripts/lib/tier-resolver.cjs` fallback chain — nothing breaks.
6
+
7
+ ---
8
+
9
+ ## Setup
10
+
11
+ ### Prerequisites
12
+
13
+ - An OpenRouter account and API key at [openrouter.ai](https://openrouter.ai).
14
+ - `OPENROUTER_API_KEY` environment variable set — this is the **only** required secret. It is sent solely as an `Authorization: Bearer` header by the catalog fetcher (Phase 33.6-01) and is **never** persisted to the cache or any log.
15
+ - **OPTIONAL** `OPENROUTER_BASE_URL` environment variable — point the catalog fetch at a custom upstream (a self-hosted proxy, or a Vertex/Bedrock/Azure-fronting gateway). Defaults to `https://openrouter.ai/api/v1`. The cache always records the canonical public `source` URL regardless of `OPENROUTER_BASE_URL`.
16
+ - **OPTIONAL** opt-in flag — set `openrouter_enabled: true` in `.design/config.json` to route tiers through OpenRouter even before a key triggers a fetch. The presence of `OPENROUTER_API_KEY` also enables the consultation path. When neither is set, the OpenRouter adapter is never consulted (default behavior unchanged — D-08).
17
+ - **OPTIONAL** tier pins — `.design/config.json#openrouter_tier_overrides` (e.g. `{ "opus": "anthropic/claude-opus-4-7" }`) force a specific catalog id for a tier; the override wins over the heuristic (D-03).
18
+
19
+ ### Verification
20
+
21
+ ```
22
+ node -e "console.log(process.env.OPENROUTER_API_KEY ? 'available' : 'not_configured')"
23
+ ```
24
+
25
+ Key present → the catalog fetch (Phase 33.6-01) can run and the adapter resolves tiers from the dynamic catalog. Absent → `not_configured`; native auth stays primary and tier resolution falls back to the native provider.
26
+
27
+ To inspect the resolved catalog + tier mappings on demand, run `/gdd:openrouter-status` (read-only — see `skills/openrouter-status/SKILL.md`).
28
+
29
+ ---
30
+
31
+ ## Probe Pattern
32
+
33
+ OpenRouter has no MCP surface — it is probed by environment variable, not ToolSearch.
34
+
35
+ ```
36
+ Step OR1 — Env check:
37
+ OPENROUTER_API_KEY set → openrouter: available
38
+ OPENROUTER_API_KEY unset → openrouter: not_configured
39
+ ```
40
+
41
+ Write the result to STATE.md `<connections>`: `openrouter: <status>`.
42
+
43
+ `not_configured` is a normal, expected state — it is **not** an error. The pipeline continues with native auth.
44
+
45
+ ---
46
+
47
+ ## OpenRouter Capability
48
+
49
+ The capability classification for the `connections/connections.md` matrix:
50
+
51
+ | Capability | Value | Rationale |
52
+ |------------|-------|-----------|
53
+ | canvas | **no** | OpenRouter does not read or write a design canvas. |
54
+ | generator | **yes** | It fronts text/code generation models the pipeline can call. |
55
+ | model-router | **yes** | Its defining capability — it routes a GDD tier to a concrete upstream model id. |
56
+
57
+ OpenRouter is a model **provider/router**, not a design tool. Per D-12 it lives **only** in the tier-resolution layer (the adapter), not in the Phase-24 install registry (`scripts/lib/install/runtimes.cjs`) — you do not install GDD "into" OpenRouter.
58
+
59
+ ---
60
+
61
+ ## Pipeline Integration
62
+
63
+ | Stage | What OpenRouter provides |
64
+ |-------|--------------------------|
65
+ | (all stages, model selection) | When opted in, the tier resolver consults the OpenRouter catalog (Phase 33.6-02 adapter) to map `opus`/`sonnet`/`haiku` → a concrete catalog model id. |
66
+ | cost telemetry | Cost rows tag `provider: openrouter` when a model was resolved via the adapter (Phase 33.6-03, SC#6) — additive/back-compat with the Phase-27 `runtime_role`/`peer_id` cost-row fields. |
67
+ | drift | The authority-watcher diffs the OpenRouter catalog weekly and surfaces `deprecated`/`withdrawn` models that match a configured `openrouter_tier_overrides` pin (SC#8). |
68
+
69
+ The catalog is **dynamically fetched** with a 24h TTL cache at `.design/cache/openrouter-models.json` (gitignored runtime artifact) — there is no static catalog file. `reference/prices.openrouter.md` is a catalog-derived price snapshot (a derived view, not authority); `reference/openrouter-tier-mapping.md` documents the resolution heuristic.
70
+
71
+ ---
72
+
73
+ ## Fallback Behavior
74
+
75
+ OpenRouter resolution is **graceful-degrade-to-native** (D-08). The adapter returns `null` — and the caller falls back to the native provider via the existing `scripts/lib/tier-resolver.cjs` fallback chain — in every one of these cases:
76
+
77
+ - `openrouter: not_configured` (no `OPENROUTER_API_KEY`, opt-in flag off).
78
+ - The catalog cache is missing.
79
+ - The catalog is stale and cannot be re-fetched (no key, network failure, rate-limited).
80
+ - No catalog model matches the requested tier and no override pins it.
81
+
82
+ When `not_configured`:
83
+ - Print: `OpenRouter not configured — tier resolution uses the native provider.`
84
+ - The native provider (resolved from `reference/runtime-models.md`) remains primary.
85
+
86
+ OpenRouter is **never** the only path. Native auth is always the floor; OpenRouter is an opt-in overlay on top of it. The adapter `resolve(tier)` never throws — any error (unknown tier, corrupt config, corrupt cache) degrades to `null` and the native fallback takes over.
@@ -93,6 +93,22 @@ interface BudgetEnforcerBackend {
93
93
  reason: string | null;
94
94
  };
95
95
  modelFromResolved(resolved: unknown, agent: string): string | null;
96
+ // Plan 33.6-03 (SC#6): the canonical cost-row payload builder (the
97
+ // types.ts:237-designated emit site). Threads the optional `provider` tag
98
+ // ("openrouter" when the OpenRouter adapter resolved the model), omitting it
99
+ // when absent (back-compat).
100
+ buildCostEventPayload(args: {
101
+ runtime: string;
102
+ agent: string;
103
+ model_id: string | null;
104
+ tier: string | null;
105
+ tokens_in: number;
106
+ tokens_out: number;
107
+ cost_usd: number | null;
108
+ runtime_role?: 'host' | 'peer';
109
+ peer_id?: string | null;
110
+ provider?: string;
111
+ }): Record<string, unknown>;
96
112
  }
97
113
  const budgetBackend = nodeRequire('../scripts/lib/budget-enforcer.cjs') as BudgetEnforcerBackend;
98
114
  // Plan 26-05: runtime detection for the cost-event runtime tag. Returns
@@ -175,6 +191,51 @@ const tierResolver = nodeRequire(
175
191
  '../scripts/lib/tier-resolver.cjs',
176
192
  ) as TierResolverModule;
177
193
 
194
+ // Plan 33.6-03 (SC#6, D-08, D-12): OpenRouter tier-resolver adapter. When the
195
+ // user opts in (`.design/config.json#openrouter_enabled: true` OR
196
+ // `OPENROUTER_API_KEY` present), the hook consults this adapter FIRST for a
197
+ // resolved model; a non-null result routes to OpenRouter and tags the cost row
198
+ // `provider: "openrouter"`, a null result falls back to the native resolution
199
+ // path (unchanged default behavior). `resolve(tier, opts)` never throws.
200
+ interface TierResolverOpenRouterModule {
201
+ resolve(
202
+ tier: string,
203
+ opts?: { catalog?: unknown; models?: unknown; overrides?: unknown; cachePath?: string; configPath?: string; cwd?: string },
204
+ ): string | null;
205
+ }
206
+ const tierResolverOpenRouter = nodeRequire(
207
+ '../scripts/lib/tier-resolver-openrouter.cjs',
208
+ ) as TierResolverOpenRouterModule;
209
+
210
+ /**
211
+ * Plan 33.6-03 (SC#6 opt-in). OpenRouter is consulted ONLY when the user opts
212
+ * in — either `.design/config.json#openrouter_enabled === true` OR
213
+ * `OPENROUTER_API_KEY` is present in the environment. Best-effort + never
214
+ * throws: a missing/corrupt config degrades to "env var only". This keeps the
215
+ * default (no OpenRouter) behavior byte-identical for every existing user
216
+ * (D-08, D-12).
217
+ *
218
+ * @param cwd base dir for `.design/config.json` (default process.cwd())
219
+ */
220
+ function isOpenRouterEnabled(cwd?: string): boolean {
221
+ if (
222
+ typeof process.env.OPENROUTER_API_KEY === 'string' &&
223
+ process.env.OPENROUTER_API_KEY.length > 0
224
+ ) {
225
+ return true;
226
+ }
227
+ try {
228
+ const configPath = join(cwd ?? process.cwd(), '.design', 'config.json');
229
+ if (!existsSync(configPath)) return false;
230
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as {
231
+ openrouter_enabled?: unknown;
232
+ };
233
+ return Boolean(parsed && parsed.openrouter_enabled === true);
234
+ } catch {
235
+ return false;
236
+ }
237
+ }
238
+
178
239
  // ── Types ───────────────────────────────────────────────────────────────────
179
240
 
180
241
  /**
@@ -661,6 +722,11 @@ function emitCostRecorded(
661
722
  tokens_in: number;
662
723
  tokens_out: number;
663
724
  cost_usd: number | null;
725
+ // Plan 33.6-03 SC#6 — optional resolution provider ("openrouter" when the
726
+ // OpenRouter adapter resolved the model). Additive/back-compat: omitted
727
+ // from the on-disk row when absent, so the legacy cost_recorded shape is
728
+ // preserved for every native-resolution + pre-33.6 spawn.
729
+ provider?: string;
664
730
  },
665
731
  cycle?: string,
666
732
  ): void {
@@ -677,6 +743,10 @@ function emitCostRecorded(
677
743
  tokens_in: payload.tokens_in,
678
744
  tokens_out: payload.tokens_out,
679
745
  cost_usd: payload.cost_usd,
746
+ // Omit-when-absent (mirrors the .cjs buildCostEventPayload discipline).
747
+ ...(typeof payload.provider === 'string' && payload.provider.length > 0
748
+ ? { provider: payload.provider }
749
+ : {}),
680
750
  },
681
751
  };
682
752
  try {
@@ -1149,6 +1219,36 @@ export async function main(): Promise<void> {
1149
1219
  }
1150
1220
  }
1151
1221
 
1222
+ // ── Plan 33.6-03 — OpenRouter resolution consultation (SC#6, D-08, D-12) ────
1223
+ //
1224
+ // When the user opts in (`.design/config.json#openrouter_enabled: true` OR
1225
+ // `OPENROUTER_API_KEY` present), consult the OpenRouter adapter for the
1226
+ // effective tier FIRST. A non-null result routes this spawn to OpenRouter:
1227
+ // we override the model id and tag the cost row `provider: "openrouter"`. A
1228
+ // null result (no key / catalog missing-or-stale / no match) falls through to
1229
+ // the native resolution that's already in `effectiveModelId` — so the default
1230
+ // (OpenRouter disabled) path is byte-identical to pre-33.6 behavior (D-08).
1231
+ // The adapter never throws; this whole branch is also wrapped defensively.
1232
+ let costProvider: string | undefined;
1233
+ if (isOpenRouterEnabled()) {
1234
+ try {
1235
+ const openrouterModel = tierResolverOpenRouter.resolve(effectiveTier);
1236
+ if (typeof openrouterModel === 'string' && openrouterModel.length > 0) {
1237
+ effectiveModelId = openrouterModel;
1238
+ costProvider = 'openrouter';
1239
+ // Reflect the OpenRouter pick into resolved_models so downstream
1240
+ // consumers see the actual model (mirrors the bandit override above).
1241
+ if (routerDecision !== undefined) {
1242
+ const rm = routerDecision.resolved_models ?? {};
1243
+ rm[agent] = openrouterModel;
1244
+ routerDecision.resolved_models = rm;
1245
+ }
1246
+ }
1247
+ } catch {
1248
+ // Fail open — never let OpenRouter resolution block a spawn (D-08).
1249
+ }
1250
+ }
1251
+
1152
1252
  // Compute runtime-aware cost via the shared backend. Failures return
1153
1253
  // null cost; we emit the event regardless so the cost-aggregator sees
1154
1254
  // the lookup attempt (Phase 22 events.jsonl tagging).
@@ -1169,6 +1269,9 @@ export async function main(): Promise<void> {
1169
1269
  tokens_in: Number(toolInput._tokens_in_est ?? 0),
1170
1270
  tokens_out: Number(toolInput._tokens_out_est ?? 0),
1171
1271
  cost_usd: costLookup.cost_usd,
1272
+ // Plan 33.6-03 SC#6 — tag the row when OpenRouter resolved the model.
1273
+ // Omitted (undefined) on the native path → buildCostEventPayload drops it.
1274
+ ...(costProvider !== undefined ? { provider: costProvider } : {}),
1172
1275
  },
1173
1276
  cycle,
1174
1277
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.33.5",
3
+ "version": "1.33.6",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -113,6 +113,7 @@
113
113
  "ws": "^8.20.0"
114
114
  },
115
115
  "overrides": {
116
- "fast-json-patch": "^3.1.1"
116
+ "fast-json-patch": "^3.1.1",
117
+ "qs": ">=6.15.2"
117
118
  }
118
119
  }
@@ -43,6 +43,7 @@ controls; the table names what crosses the line.
43
43
  | gdd-state MCP `←` environment / config / tool input | Whoever sets `GDD_STATE_PATH` or supplies a tool-call payload, or authors `.design/config.json` | The `GDD_STATE_PATH` env value + the JSON tool-input payloads |
44
44
  | Peer-CLI broker `↔` spawned child | A spawned peer CLI (Codex / Gemini / Cursor / Copilot / Qwen) and its stdout stream | The child's stdout JSON frames + the parent env handed to the child |
45
45
  | Outbound call sites `↔` external host | The remote HTTP host / GitHub / Figma the call reaches | The outbound request payload + whatever the remote returns |
46
+ | OpenRouter catalog fetch `→` openrouter.ai | The OpenRouter `/models` API host (and any MITM on the path) | The `Authorization: Bearer <OPENROUTER_API_KEY>` request header + the untrusted `/models` JSON the host returns |
46
47
 
47
48
  The event payloads that traverse the bus (and therefore the WS transport and
48
49
  any persisted JSONL) are scrubbed at serialize time — see Component 4's
@@ -311,6 +312,68 @@ model).
311
312
 
312
313
  ---
313
314
 
315
+ ## Component 6 — OpenRouter catalog fetcher (scripts/lib/openrouter/catalog-fetcher.cjs)
316
+
317
+ > Added in Phase 33.6 (OR-01, CONTEXT D-06). This is the runtime's **first
318
+ > plugin-side outbound REST client** — the issue-reporter (Component 5) reaches
319
+ > the network only through the user's `gh` CLI, and the WS transport (Component
320
+ > 4) is a *server*, not an outbound client. The catalog fetcher is the first
321
+ > first-party code to open an outbound HTTP request to a third-party host
322
+ > directly, which is why it lands only after the 33.5 audited baseline and the
323
+ > `scan:outbound` gate (33.5-04) are in place.
324
+
325
+ `scripts/lib/openrouter/catalog-fetcher.cjs` performs a read-only GET to the
326
+ OpenRouter model catalog (`https://openrouter.ai/api/v1/models`) through an
327
+ **injectable `fetchImpl`** (default global `fetch`), maps the response into the
328
+ `.design/cache/openrouter-models.json` cache shape, and writes it atomically.
329
+ The live fetch is opt-in — gated on `OPENROUTER_API_KEY` being present at
330
+ runtime; absent it, the fetcher returns cached-if-any-else-null and tier
331
+ resolution falls back to the native provider.
332
+
333
+ - **Assets:** The **`OPENROUTER_API_KEY`** (a billable provider credential) and
334
+ the integrity of the cached catalog the tier-resolver later trusts.
335
+ - **Entry points:** The **`/models` JSON the OpenRouter host returns** (untrusted
336
+ remote input the fetcher must parse), and the `OPENROUTER_BASE_URL` env (an
337
+ operator-supplied endpoint override).
338
+ - **STRIDE threats:**
339
+ - **Spoofing:** A spoofed `/models` endpoint (DNS/MITM, or a hostile
340
+ `OPENROUTER_BASE_URL`) could feed a forged catalog.
341
+ - **Tampering:** A malformed/oversized `/models` body could try to corrupt the
342
+ cache the resolver reads, or smuggle unexpected fields downstream.
343
+ - **Information disclosure:** **The headline risk** — leaking the
344
+ `OPENROUTER_API_KEY` by persisting it to the cache, logging it, or sending it
345
+ to an unintended host.
346
+ - **Denial of service:** A hung or slow host could stall the fetch; a giant
347
+ catalog could pressure memory.
348
+ - **Elevation of privilege:** A forged catalog could steer tier resolution to
349
+ an attacker-chosen model id.
350
+ - **Current mitigations:** The key is read from **`OPENROUTER_API_KEY` env only**,
351
+ sent **solely** as an `Authorization: Bearer` request header, and is **never
352
+ persisted to the cache nor written to any log seam** — the cache shape carries
353
+ only `id`/`name`/`context_length`/`pricing`, and the mapper keeps **only** those
354
+ fields, dropping everything else (the `/models` body is **mapped, never
355
+ eval'd**). The cache write is **atomic** (per-pid temp + rename) into the
356
+ **gitignored** `.design/cache/`, so a partial/corrupt fetch can't leave a
357
+ half-written catalog and the cache never enters git history. The fetcher
358
+ **never throws** (D-08): no key / fetch failure / parse failure all degrade to
359
+ cached-if-any-else-null, bounding the DoS surface, and retries are **bounded**
360
+ (max 3 attempts) on a jittered-backoff curve with `rate-guard` awareness.
361
+ Egress is **allowlisted** via `scripts/lib/openrouter/**` in
362
+ `scripts/security/outbound-allowlist.json` — the only sanctioned outbound site
363
+ in that subtree — so the 33.5 `scan:outbound` gate proves no un-approved egress
364
+ crept in. The **injectable `fetchImpl`** keeps the default `npm test` suite
365
+ hermetic (D-07) — no live network — and there is **no new HTTP dependency**
366
+ (global `fetch` + `sdk/primitives` only — D-10), avoiding both a new supply-chain
367
+ surface and the gate's `axios`/`node-fetch`/`undici` package patterns.
368
+ - **Residual risks:** None this phase leaves open. The catalog is advisory data
369
+ consumed by the tier-resolver heuristic (33.6-02), which already clamps to
370
+ GDD's `opus`/`sonnet`/`haiku` vocabulary and supports user overrides, so a
371
+ forged catalog cannot escalate beyond model-id selection within that bounded
372
+ set; a future hardening could pin the OpenRouter TLS cert or sign the cache,
373
+ but neither is required for the current trust model.
374
+
375
+ ---
376
+
314
377
  ## Residual-risk → closing-plan map
315
378
 
316
379
  Every residual risk identified above is routed to the Phase 33.5 plan (or
@@ -0,0 +1,98 @@
1
+ # OpenRouter Tier-Mapping Heuristic
2
+
3
+ How `scripts/lib/tier-resolver-openrouter.cjs` maps GDD's tier vocabulary onto a
4
+ dynamic OpenRouter catalog model id. This document is the human-readable companion
5
+ to that adapter; the adapter's `resolve(tier, opts)` is the canonical, executable
6
+ source of the mapping. Phase 33.6, decision D-03 (heuristic + override), D-04
7
+ (tier vocabulary), D-08 (graceful-null → native fallback).
8
+
9
+ ## What it maps
10
+
11
+ The plugin speaks one tier vocabulary everywhere a model tier is named in
12
+ frontmatter or config: `opus`, `sonnet`, `haiku` — the same `VALID_TIERS` the
13
+ Phase-26 `tier-resolver.cjs` enforces. OpenRouter, by contrast, exposes a flat
14
+ catalog of provider-prefixed model ids (`anthropic/claude-opus-4-7`,
15
+ `meta-llama/llama-3.1-8b-instruct`, `qwen/qwen-2.5-72b-instruct`, …). The adapter
16
+ bridges the two by assigning each GDD tier to one internal capability bucket and
17
+ then picking the catalog id that best fits that bucket.
18
+
19
+ The ROADMAP's SC#4 names the buckets `high` / `medium` / `low`; those are the
20
+ heuristic's INTERNAL labels. They map one-to-one to the public tiers (D-04):
21
+
22
+ - `opus` ← HIGH bucket
23
+ - `sonnet` ← MEDIUM bucket
24
+ - `haiku` ← LOW bucket
25
+
26
+ The adapter's public `resolve(tier)` always speaks `opus` / `sonnet` / `haiku`;
27
+ `high` / `medium` / `low` never leak across the API boundary.
28
+
29
+ ## The buckets
30
+
31
+ - **opus (HIGH) = top-tier closed.** The most capable closed-vendor model in the
32
+ catalog — the priciest premium id from a closed namespace. This is the
33
+ "spare-no-expense, hardest reasoning" slot.
34
+ - **sonnet (MEDIUM) = mid / top-open.** A capable model that sits below the opus
35
+ pick — typically the mid-priced closed model, or the strongest open model when
36
+ no second closed tier is present. The everyday workhorse slot.
37
+ - **haiku (LOW) = cheap open.** The cheapest capable OPEN model — the
38
+ fast/inexpensive slot for high-volume, low-stakes calls.
39
+
40
+ ## The signals
41
+
42
+ The heuristic is computed from fields already present on each catalog model, so it
43
+ stays deterministic for a fixed catalog (no clock, no randomness — important so the
44
+ 33.6-04 golden baseline is stable):
45
+
46
+ - **Namespace (closed vs open).** The id prefix before the `/` names the vendor.
47
+ `anthropic`, `openai`, `google` are treated as CLOSED (premium, frontier).
48
+ `meta-llama`, `qwen`, `mistralai`, `deepseek` are treated as OPEN (commodity,
49
+ cheap). The closed/open split is the primary axis: opus and sonnet prefer closed,
50
+ haiku requires open.
51
+ - **Pricing.** Each model carries `pricing.prompt` / `pricing.completion` as string
52
+ decimals (USD per token). Parsed to Number, the completion price is the tie-break:
53
+ highest completion price wins the opus slot; lowest completion price wins the
54
+ haiku slot. Models with unparseable or missing pricing sort last.
55
+ - **Context length.** `context_length` is a secondary capability signal used only to
56
+ break a pricing tie (longer context is treated as more capable).
57
+
58
+ For the canonical fixture catalog (closed `anthropic/claude-opus-4-7` +
59
+ `anthropic/claude-sonnet-4-7`, open `meta-llama/llama-3.1-70b-instruct`,
60
+ `meta-llama/llama-3.1-8b-instruct`, `qwen/qwen-2.5-72b-instruct`) the heuristic
61
+ resolves opus → `anthropic/claude-opus-4-7` (top closed, highest completion price),
62
+ sonnet → `anthropic/claude-sonnet-4-7` (mid closed), and haiku →
63
+ `meta-llama/llama-3.1-8b-instruct` (cheapest open).
64
+
65
+ ## The override escape hatch
66
+
67
+ The heuristic is a sensible default, not a straitjacket. A user can pin any tier to
68
+ an exact catalog id via `.design/config.json`:
69
+
70
+ ```
71
+ {
72
+ "openrouter_tier_overrides": {
73
+ "opus": "anthropic/claude-opus-4-7",
74
+ "haiku": "meta-llama/llama-3.1-8b-instruct"
75
+ }
76
+ }
77
+ ```
78
+
79
+ An override **wins** over the heuristic: when `openrouter_tier_overrides[tier]` is a
80
+ non-empty string, the adapter returns it verbatim — even if that id is not present
81
+ in the live catalog (the user's explicit choice is honored over catalog membership).
82
+ Tests inject the same map via `opts.overrides` instead of reading the live config
83
+ file, so the override path is exercised hermetically. The config read is best-effort:
84
+ a missing file, a missing key, or corrupt JSON degrades to an empty override map
85
+ rather than throwing.
86
+
87
+ ## The graceful-null contract
88
+
89
+ OpenRouter is opt-in ALONGSIDE native provider auth — never OpenRouter-only (D-08).
90
+ When no catalog is available (no cache, an empty `models[]`, or a `readCatalog` that
91
+ returns null) AND no override applies to the requested tier, `resolve` returns
92
+ `null`. A `null` is not an error: it is the signal that the caller (the router /
93
+ budget-enforcer, wired in 33.6-03) should fall back to the native provider via the
94
+ existing `scripts/lib/tier-resolver.cjs` fallback chain. The adapter NEVER throws —
95
+ an unknown tier, a missing config, a corrupt cache, or garbage options all degrade to
96
+ `null` (or to an override when one applies). This keeps OpenRouter a strictly
97
+ additive capability: turning it off, or having it fail to fetch, can never break a
98
+ resolution that would have succeeded natively.
@@ -0,0 +1,26 @@
1
+ # OpenRouter — Catalog-Derived Price Snapshot
2
+
3
+ **Phase 33.6 (v1.33.6).** This file is a **catalog-derived snapshot** of OpenRouter per-model prices — it is **generated from** `.design/cache/openrouter-models.json` (the dynamic catalog fetched by `scripts/lib/openrouter/catalog-fetcher.cjs`), **not** a hand-maintained authority. The **live source of truth is the dynamic catalog**; this table is a derived, illustrative view that can go stale between catalog fetches.
4
+
5
+ Unlike the per-runtime tables under `reference/prices/` (Phase 26 D-08, hand-curated authority with provenance), OpenRouter's prices live in the upstream `/models` response and are refreshed on the 24h TTL. To inspect the current resolved prices, run `/gdd:openrouter-status` or read the cache directly. For the tier→model resolution heuristic see `reference/openrouter-tier-mapping.md`.
6
+
7
+ OpenRouter quotes prices **per token** (USD), for `prompt` (input) and `completion` (output) separately.
8
+
9
+ ## Representative sample (per token, USD)
10
+
11
+ Derived from the fixture catalog at `test/fixtures/baselines/phase-33-6/openrouter-catalog.json` (a snapshot mirror of the cache shape). Actual live prices come from the catalog at fetch time.
12
+
13
+ | model id | prompt $/tok | completion $/tok |
14
+ |----------|--------------|------------------|
15
+ | `anthropic/claude-opus-4-7` | 0.000015 | 0.000075 |
16
+ | `anthropic/claude-sonnet-4-7` | 0.000003 | 0.000015 |
17
+ | `meta-llama/llama-3.1-70b-instruct` | 0.00000052 | 0.00000075 |
18
+ | `meta-llama/llama-3.1-8b-instruct` | 0.00000002 | 0.00000005 |
19
+ | `qwen/qwen-2.5-72b-instruct` | 0.00000038 | 0.0000004 |
20
+
21
+ ## Notes
22
+
23
+ - **Derived view, not authority.** Do not hand-edit prices here to "fix" cost math — fix the catalog fetch instead. This file documents the *shape* and *source* of OpenRouter pricing for the registry round-trip and for human reference.
24
+ - **Per-token vs per-1M.** The native runtime tables (`reference/prices/<runtime>.md`) quote `input_per_1m` / `output_per_1m`; OpenRouter's catalog quotes per-token. Multiply by 1,000,000 to compare (e.g. `anthropic/claude-opus-4-7` ≈ $15 input / $75 output per 1M tokens).
25
+ - **Cost telemetry.** When a model is resolved via the OpenRouter adapter, the cost row tags `provider: openrouter` (Phase 33.6-03, SC#6) — see `scripts/lib/budget-enforcer.cjs#buildCostEventPayload`.
26
+ - **Drift.** The authority-watcher diffs the catalog weekly and surfaces `deprecated`/`withdrawn` models matching a configured `openrouter_tier_overrides` pin (SC#8) — see `scripts/lib/authority-watcher/index.cjs#diffOpenRouterCatalog`.
@@ -874,6 +874,20 @@
874
874
  "type": "heuristic",
875
875
  "phase": 33.5,
876
876
  "description": "Phase 33.5 static security audit of GDD's shipped runtime surface (hooks/scripts/sdk/bin) — outbound-network call sites, secret-handling sites, and external-input surfaces; human-readable companion to scripts/security/outbound-allowlist.json (the canonical active-egress allowlist the 33.5-04 scan-outbound-network.cjs gate consumes) and reference/gdd-threat-model.md."
877
+ },
878
+ {
879
+ "name": "openrouter-tier-mapping",
880
+ "path": "reference/openrouter-tier-mapping.md",
881
+ "type": "heuristic",
882
+ "phase": 33.6,
883
+ "description": "Phase 33.6 OpenRouter tier-mapping heuristic — maps GDD opus/sonnet/haiku onto OpenRouter catalog ids via closed-vs-open + pricing buckets (high/medium/low), with the .design/config.json#openrouter_tier_overrides escape hatch (override wins) and graceful-null → native fallback."
884
+ },
885
+ {
886
+ "name": "prices-openrouter",
887
+ "path": "reference/prices.openrouter.md",
888
+ "type": "data",
889
+ "phase": 33.6,
890
+ "description": "Phase 33.6 catalog-derived OpenRouter price sub-table — per-model prompt/completion $/tok snapshot of .design/cache/openrouter-models.json; derived view, the dynamic catalog is the source of truth (D-11 registry round-trip)."
877
891
  }
878
892
  ]
879
893
  }