@hegemonart/get-design-done 1.25.0 → 1.27.0

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.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +96 -0
  4. package/README.md +12 -6
  5. package/SKILL.md +3 -0
  6. package/agents/README.md +89 -0
  7. package/agents/design-reflector.md +43 -0
  8. package/agents/gdd-intel-updater.md +34 -1
  9. package/hooks/budget-enforcer.ts +143 -4
  10. package/package.json +1 -1
  11. package/reference/model-prices.md +40 -19
  12. package/reference/peer-cli-capabilities.md +151 -0
  13. package/reference/peer-protocols.md +266 -0
  14. package/reference/prices/antigravity.md +21 -0
  15. package/reference/prices/augment.md +21 -0
  16. package/reference/prices/claude.md +42 -0
  17. package/reference/prices/cline.md +23 -0
  18. package/reference/prices/codebuddy.md +21 -0
  19. package/reference/prices/codex.md +25 -0
  20. package/reference/prices/copilot.md +21 -0
  21. package/reference/prices/cursor.md +21 -0
  22. package/reference/prices/gemini.md +25 -0
  23. package/reference/prices/kilo.md +21 -0
  24. package/reference/prices/opencode.md +23 -0
  25. package/reference/prices/qwen.md +25 -0
  26. package/reference/prices/trae.md +23 -0
  27. package/reference/prices/windsurf.md +21 -0
  28. package/reference/registry.json +121 -1
  29. package/reference/runtime-models.md +446 -0
  30. package/reference/schemas/runtime-models.schema.json +123 -0
  31. package/scripts/install.cjs +8 -0
  32. package/scripts/lib/bandit-router.cjs +214 -7
  33. package/scripts/lib/budget-enforcer.cjs +514 -0
  34. package/scripts/lib/cost-arbitrage.cjs +294 -0
  35. package/scripts/lib/event-stream/index.ts +14 -1
  36. package/scripts/lib/event-stream/types.ts +125 -1
  37. package/scripts/lib/install/installer.cjs +188 -11
  38. package/scripts/lib/install/parse-runtime-models.cjs +267 -0
  39. package/scripts/lib/install/runtimes.cjs +101 -0
  40. package/scripts/lib/peer-cli/acp-client.cjs +375 -0
  41. package/scripts/lib/peer-cli/adapters/codex.cjs +101 -0
  42. package/scripts/lib/peer-cli/adapters/copilot.cjs +79 -0
  43. package/scripts/lib/peer-cli/adapters/cursor.cjs +78 -0
  44. package/scripts/lib/peer-cli/adapters/gemini.cjs +81 -0
  45. package/scripts/lib/peer-cli/adapters/qwen.cjs +72 -0
  46. package/scripts/lib/peer-cli/asp-client.cjs +587 -0
  47. package/scripts/lib/peer-cli/broker-lifecycle.cjs +406 -0
  48. package/scripts/lib/peer-cli/registry.cjs +434 -0
  49. package/scripts/lib/peer-cli/spawn-cmd.cjs +149 -0
  50. package/scripts/lib/runtime-detect.cjs +96 -0
  51. package/scripts/lib/session-runner/index.ts +215 -0
  52. package/scripts/lib/session-runner/types.ts +60 -0
  53. package/scripts/lib/tier-resolver.cjs +311 -0
  54. package/scripts/validate-frontmatter.ts +297 -2
  55. package/skills/peer-cli-add/SKILL.md +170 -0
  56. package/skills/peer-cli-customize/SKILL.md +110 -0
  57. package/skills/peers/SKILL.md +101 -0
  58. package/skills/router/SKILL.md +51 -2
@@ -0,0 +1,514 @@
1
+ // scripts/lib/budget-enforcer.cjs
2
+ //
3
+ // Plan 26-05 — shared cost-computation backend for budget-enforcer.
4
+ //
5
+ // Pure module that takes a `(model_id, runtime_id, token_counts)` triple and
6
+ // returns a USD cost figure by reading the per-runtime price table at
7
+ // `reference/prices/<runtime>.md`. Used by both:
8
+ //
9
+ // 1. `hooks/budget-enforcer.ts` — the Claude Code PreToolUse hook, when
10
+ // the router decision payload carries a `resolved_models[agent]`
11
+ // entry. The hook calls `computeCost({ model_id, runtime, tokens_in,
12
+ // tokens_out, cache_hit })` and writes the resulting figure into the
13
+ // OPT-09 telemetry row.
14
+ // 2. Non-CC code-level mirrors of the budget-enforcer surface — runtime
15
+ // adapters that wrap the same Phase 22 events.jsonl primitives. Same
16
+ // backend means cost numbers are computed identically across runtime
17
+ // hosts; downstream cost-aggregator (Phase 22) can roll up by
18
+ // `runtime` tag with apples-to-apples figures.
19
+ //
20
+ // `.cjs` extension matches Phase 22 primitives. The .ts hook reaches it
21
+ // via `createRequire` — same scheme as `rate-guard.cjs` and
22
+ // `iteration-budget.cjs`.
23
+ //
24
+ // Per-runtime price tables (D-08):
25
+ // - `reference/prices/claude.md` — Anthropic models
26
+ // - `reference/prices/codex.md` — OpenAI Codex (gpt-5 family)
27
+ // - `reference/prices/gemini.md` — Google Gemini 2.5 family
28
+ // - `reference/prices/qwen.md` — Alibaba Qwen 3 family
29
+ // - `reference/prices/<other>.md` — stub-only at v1.26.0; researcher
30
+ // fills with provenance citation in a later cycle.
31
+ //
32
+ // Each price table has the same canonical row shape:
33
+ // | Model | Tier | input_per_1m | output_per_1m | cached_input_per_1m |
34
+ //
35
+ // The parser extracts the markdown table by header signature, so future
36
+ // per-runtime authors can add columns at the right edge without breaking
37
+ // the consumer (forward-compatible).
38
+ //
39
+ // Fallback chain (mirrors tier-resolver D-04 spirit):
40
+ // 1. Runtime price table has the model row → use it.
41
+ // 2. Runtime price table missing OR model row missing → fall back to
42
+ // claude.md and emit `cost_lookup_fallback` event (caller emits;
43
+ // this module surfaces the fact via the return shape).
44
+ // 3. claude.md also missing the model → return null cost + reason.
45
+ //
46
+ // Pure module — no top-level side effects beyond the lazy cache. Never
47
+ // throws on missing/malformed price tables (returns null cost +
48
+ // diagnostic reason); throws ONLY on programmer errors (bad arg shapes).
49
+
50
+ 'use strict';
51
+
52
+ const fs = require('node:fs');
53
+ const path = require('node:path');
54
+
55
+ const REPO_ROOT_GUESS = path.resolve(__dirname, '..', '..');
56
+ const DEFAULT_RUNTIME_ID = 'claude';
57
+ const VALID_TIERS = Object.freeze(['opus', 'sonnet', 'haiku']);
58
+
59
+ /**
60
+ * Parsed price-row shape returned by `parsePriceTable`.
61
+ * Numeric fields are USD per 1M tokens.
62
+ *
63
+ * @typedef {{
64
+ * model: string,
65
+ * tier: string,
66
+ * input_per_1m: number,
67
+ * output_per_1m: number,
68
+ * cached_input_per_1m: number,
69
+ * }} PriceRow
70
+ */
71
+
72
+ /**
73
+ * In-memory cache of parsed price tables, keyed by runtime ID. `null`
74
+ * means we tried and the file was missing/unparseable (so we don't
75
+ * re-read the same broken file every spawn).
76
+ */
77
+ const _cache = new Map();
78
+
79
+ /**
80
+ * Reset the parsed-prices cache. Tests use this after writing fixture
81
+ * price tables to a temp directory; production callers rarely need it.
82
+ */
83
+ function reset() {
84
+ _cache.clear();
85
+ }
86
+
87
+ /**
88
+ * Compute the absolute path to `reference/prices/<runtime>.md`. Honors
89
+ * the `cwd` option for test isolation — defaults to the repo root
90
+ * derived from this module's filesystem location.
91
+ *
92
+ * @param {string} runtime
93
+ * @param {{cwd?: string}} [opts]
94
+ * @returns {string}
95
+ */
96
+ function priceTablePath(runtime, opts) {
97
+ const root = (opts && typeof opts.cwd === 'string' && opts.cwd.length > 0)
98
+ ? opts.cwd
99
+ : REPO_ROOT_GUESS;
100
+ return path.join(root, 'reference', 'prices', `${runtime}.md`);
101
+ }
102
+
103
+ /**
104
+ * Parse a markdown price table by locating the canonical header row and
105
+ * scanning subsequent `|`-delimited rows until the table ends. The
106
+ * header signature is the four required columns in order:
107
+ *
108
+ * `| Model | Tier | input_per_1m | output_per_1m | cached_input_per_1m |`
109
+ *
110
+ * Extra columns at the right are ignored (forward-compatible). Rows
111
+ * missing required columns or carrying placeholder TODO markers
112
+ * (e.g. `<TODO>` in a numeric cell) are skipped silently — the caller
113
+ * sees a null lookup for that model and falls back per the chain.
114
+ *
115
+ * @param {string} markdown
116
+ * @returns {PriceRow[]}
117
+ */
118
+ function parsePriceTable(markdown) {
119
+ if (typeof markdown !== 'string' || markdown.length === 0) return [];
120
+
121
+ const lines = markdown.split(/\r?\n/);
122
+ const rows = [];
123
+
124
+ // Locate header row by scanning for the canonical column signature.
125
+ // We tolerate whitespace + casing variations on the header cells.
126
+ const headerNeedles = ['model', 'tier', 'input_per_1m', 'output_per_1m', 'cached_input_per_1m'];
127
+ let headerIdx = -1;
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const line = lines[i];
130
+ if (!/^\s*\|/.test(line)) continue;
131
+ const cells = line.split('|').map((s) => s.trim().toLowerCase());
132
+ let matched = 0;
133
+ for (const needle of headerNeedles) {
134
+ if (cells.includes(needle)) matched++;
135
+ }
136
+ if (matched === headerNeedles.length) {
137
+ headerIdx = i;
138
+ break;
139
+ }
140
+ }
141
+ if (headerIdx < 0) return [];
142
+
143
+ // Build the cell-index map from the header so column reorderings are
144
+ // tolerated (a column-shuffle reorg is a docs PR not a logic break).
145
+ const headerCells = lines[headerIdx].split('|').map((s) => s.trim().toLowerCase());
146
+ const colIdx = {};
147
+ for (const needle of headerNeedles) {
148
+ colIdx[needle] = headerCells.indexOf(needle);
149
+ }
150
+
151
+ // Skip the separator row (e.g. `|---|---|...|`).
152
+ let i = headerIdx + 1;
153
+ if (i < lines.length && /^\s*\|[\s|:-]+\|\s*$/.test(lines[i])) i++;
154
+
155
+ for (; i < lines.length; i++) {
156
+ const line = lines[i];
157
+ if (!/^\s*\|/.test(line)) break; // table ended
158
+ const cells = line.split('|').map((s) => s.trim());
159
+ // Pull required cells by header index.
160
+ const model = cells[colIdx.model];
161
+ const tier = cells[colIdx.tier];
162
+ const input = Number(cells[colIdx.input_per_1m]);
163
+ const output = Number(cells[colIdx.output_per_1m]);
164
+ const cached = Number(cells[colIdx.cached_input_per_1m]);
165
+ if (typeof model !== 'string' || model.length === 0) continue;
166
+ // Skip TODO/placeholder rows where prices haven't been confirmed.
167
+ if (
168
+ !Number.isFinite(input) ||
169
+ !Number.isFinite(output) ||
170
+ !Number.isFinite(cached)
171
+ ) {
172
+ continue;
173
+ }
174
+ rows.push({
175
+ model,
176
+ tier: typeof tier === 'string' ? tier : 'unknown',
177
+ input_per_1m: input,
178
+ output_per_1m: output,
179
+ cached_input_per_1m: cached,
180
+ });
181
+ }
182
+ return rows;
183
+ }
184
+
185
+ /**
186
+ * Load + cache the parsed price table for a runtime. Returns the empty
187
+ * array (cached) if the file is missing or unparseable, so subsequent
188
+ * calls don't re-read.
189
+ *
190
+ * @param {string} runtime
191
+ * @param {{cwd?: string}} [opts]
192
+ * @returns {PriceRow[]}
193
+ */
194
+ function loadPriceTable(runtime, opts) {
195
+ const key = `${runtime}:${(opts && opts.cwd) || ''}`;
196
+ if (_cache.has(key)) return _cache.get(key);
197
+ const fp = priceTablePath(runtime, opts);
198
+ let rows = [];
199
+ try {
200
+ if (fs.existsSync(fp)) {
201
+ const md = fs.readFileSync(fp, 'utf8');
202
+ rows = parsePriceTable(md);
203
+ }
204
+ } catch {
205
+ rows = [];
206
+ }
207
+ _cache.set(key, rows);
208
+ return rows;
209
+ }
210
+
211
+ /**
212
+ * Find a price row by model ID OR by tier name. Model-ID match takes
213
+ * precedence (exact concrete-model lookup is the resolved_models path);
214
+ * tier match is the back-compat fallback for callers that only know
215
+ * the tier (legacy model_tier_overrides path).
216
+ *
217
+ * @param {PriceRow[]} rows
218
+ * @param {{model_id?: string|null, tier?: string|null}} q
219
+ * @returns {PriceRow | null}
220
+ */
221
+ function findPriceRow(rows, q) {
222
+ if (!Array.isArray(rows) || rows.length === 0) return null;
223
+ const wantModel = q && typeof q.model_id === 'string' && q.model_id.length > 0
224
+ ? q.model_id
225
+ : null;
226
+ const wantTier = q && typeof q.tier === 'string' && q.tier.length > 0
227
+ ? q.tier
228
+ : null;
229
+ if (wantModel !== null) {
230
+ for (const r of rows) {
231
+ if (r.model === wantModel) return r;
232
+ }
233
+ }
234
+ if (wantTier !== null) {
235
+ for (const r of rows) {
236
+ if (r.tier === wantTier) return r;
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+
242
+ /**
243
+ * Apply the OPT-09 estimator formula to a price row + token counts.
244
+ * Cache-hit input rows charge `cached_input_per_1m` instead of
245
+ * `input_per_1m` (consistent with skills/router/SKILL.md D-08).
246
+ *
247
+ * @param {PriceRow} row
248
+ * @param {{tokens_in: number, tokens_out: number, cache_hit?: boolean}} t
249
+ * @returns {number} cost in USD
250
+ */
251
+ function applyFormula(row, t) {
252
+ const tokensIn = Math.max(0, Number(t.tokens_in || 0));
253
+ const tokensOut = Math.max(0, Number(t.tokens_out || 0));
254
+ const inputRate = t.cache_hit === true ? row.cached_input_per_1m : row.input_per_1m;
255
+ return (
256
+ (tokensIn / 1_000_000) * inputRate +
257
+ (tokensOut / 1_000_000) * row.output_per_1m
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Compute the USD cost for a spawn, given a concrete model identifier
263
+ * (the resolved_models path) OR a tier name (the legacy fallback path)
264
+ * plus token counts.
265
+ *
266
+ * Lookup order:
267
+ * 1. The runtime's price table by model_id → use.
268
+ * 2. The runtime's price table by tier → use.
269
+ * 3. The claude price table by model_id → use (fallback).
270
+ * 4. The claude price table by tier → use (fallback).
271
+ * 5. Nothing matched → return { cost_usd: null, reason }.
272
+ *
273
+ * Never throws for missing files / missing rows. Returns `cost_usd: null`
274
+ * + a diagnostic `reason` string the caller can emit on the event stream
275
+ * (`cost_lookup_failed` event) or write into telemetry.
276
+ *
277
+ * @param {object} args
278
+ * @param {string|null} [args.model_id]
279
+ * Concrete model name (e.g. 'gpt-5', 'claude-sonnet-4-7'). Preferred.
280
+ * @param {string|null} [args.tier]
281
+ * Tier name ('opus'|'sonnet'|'haiku'). Used when model_id is absent.
282
+ * @param {string} args.runtime
283
+ * Runtime ID ('claude', 'codex', 'gemini', …). Required.
284
+ * @param {number} args.tokens_in
285
+ * @param {number} args.tokens_out
286
+ * @param {boolean} [args.cache_hit]
287
+ * @param {object} [opts]
288
+ * @param {string} [opts.cwd]
289
+ * Override repo root (tests).
290
+ * @returns {{
291
+ * cost_usd: number|null,
292
+ * model: string|null,
293
+ * tier: string|null,
294
+ * runtime_used: string|null,
295
+ * fallback: boolean,
296
+ * reason: string|null,
297
+ * }}
298
+ */
299
+ function computeCost(args, opts) {
300
+ if (!args || typeof args !== 'object') {
301
+ return {
302
+ cost_usd: null,
303
+ model: null,
304
+ tier: null,
305
+ runtime_used: null,
306
+ fallback: false,
307
+ reason: 'invalid_args',
308
+ };
309
+ }
310
+ const runtime = typeof args.runtime === 'string' && args.runtime.length > 0
311
+ ? args.runtime
312
+ : null;
313
+ if (runtime === null) {
314
+ return {
315
+ cost_usd: null,
316
+ model: null,
317
+ tier: typeof args.tier === 'string' ? args.tier : null,
318
+ runtime_used: null,
319
+ fallback: false,
320
+ reason: 'missing_runtime',
321
+ };
322
+ }
323
+
324
+ const tokens = {
325
+ tokens_in: Number(args.tokens_in || 0),
326
+ tokens_out: Number(args.tokens_out || 0),
327
+ cache_hit: args.cache_hit === true,
328
+ };
329
+ const q = {
330
+ model_id: typeof args.model_id === 'string' && args.model_id.length > 0
331
+ ? args.model_id
332
+ : null,
333
+ tier: typeof args.tier === 'string' && args.tier.length > 0
334
+ ? args.tier
335
+ : null,
336
+ };
337
+
338
+ // Branch 1+2: runtime price table.
339
+ const rows = loadPriceTable(runtime, opts);
340
+ const direct = findPriceRow(rows, q);
341
+ if (direct !== null) {
342
+ return {
343
+ cost_usd: applyFormula(direct, tokens),
344
+ model: direct.model,
345
+ tier: direct.tier,
346
+ runtime_used: runtime,
347
+ fallback: false,
348
+ reason: null,
349
+ };
350
+ }
351
+
352
+ // Branch 3+4: claude fallback (only if not already querying claude).
353
+ if (runtime !== DEFAULT_RUNTIME_ID) {
354
+ const fallbackRows = loadPriceTable(DEFAULT_RUNTIME_ID, opts);
355
+ const fb = findPriceRow(fallbackRows, q);
356
+ if (fb !== null) {
357
+ return {
358
+ cost_usd: applyFormula(fb, tokens),
359
+ model: fb.model,
360
+ tier: fb.tier,
361
+ runtime_used: DEFAULT_RUNTIME_ID,
362
+ fallback: true,
363
+ reason: rows.length === 0 ? 'runtime_table_missing' : 'model_not_in_runtime_table',
364
+ };
365
+ }
366
+ }
367
+
368
+ // Branch 5: nothing matched.
369
+ return {
370
+ cost_usd: null,
371
+ model: null,
372
+ tier: q.tier,
373
+ runtime_used: null,
374
+ fallback: false,
375
+ reason: rows.length === 0 ? 'runtime_table_missing' : 'model_not_found',
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Convenience: build a `cost_recorded` event payload (D-08 shape) from
381
+ * a computeCost result + the spawn metadata. Returned object is the
382
+ * `payload` field for `appendEvent({ type: 'cost_recorded', payload })`.
383
+ * The .ts hook owns the actual `appendEvent()` call (it has the typed
384
+ * event-stream import); .cjs callers (non-CC mirrors) compose this with
385
+ * the JSONL line shape used in tier-resolver.cjs's `emitEvent()`.
386
+ *
387
+ * Phase 27 / Plan 27-08 (D-09) extension — additive: cost rows now
388
+ * optionally carry `runtime_role` ("host" | "peer", default "host" when
389
+ * absent for back-compat) and `peer_id` (set only when
390
+ * `runtime_role === "peer"`). Pre-Phase-27 callers that don't pass these
391
+ * fields get the legacy shape with `runtime_role: "host"` stamped — so
392
+ * the cost-aggregator + reflector cross-runtime arbitrage
393
+ * (`scripts/lib/cost-arbitrage.cjs`) never sees an absent role tag and
394
+ * mixed-role cycle history rolls up correctly without crashing.
395
+ *
396
+ * @param {object} args
397
+ * @param {string} args.runtime
398
+ * @param {string} args.agent
399
+ * @param {string|null} args.model_id
400
+ * @param {string|null} args.tier
401
+ * @param {number} args.tokens_in
402
+ * @param {number} args.tokens_out
403
+ * @param {number|null} args.cost_usd
404
+ * @param {('host'|'peer')} [args.runtime_role]
405
+ * Phase 27. Defaults to `"host"` when absent.
406
+ * @param {string|null} [args.peer_id]
407
+ * Phase 27. The peer-CLI ID when `runtime_role === "peer"` (e.g.
408
+ * `"gemini"`, `"codex"`). Omitted from output when absent or when
409
+ * `runtime_role === "host"`.
410
+ * @returns {object}
411
+ */
412
+ function buildCostEventPayload(args) {
413
+ const role = args && args.runtime_role === 'peer' ? 'peer' : 'host';
414
+ /** @type {Record<string, unknown>} */
415
+ const out = {
416
+ runtime: args.runtime,
417
+ agent: args.agent,
418
+ model_id: args.model_id,
419
+ tier: args.tier,
420
+ tokens_in: Number(args.tokens_in || 0),
421
+ tokens_out: Number(args.tokens_out || 0),
422
+ cost_usd: typeof args.cost_usd === 'number' && Number.isFinite(args.cost_usd)
423
+ ? args.cost_usd
424
+ : null,
425
+ runtime_role: role,
426
+ };
427
+ // peer_id is only meaningful when role === "peer"; we omit it for
428
+ // host rows to keep the legacy on-disk shape stable for cost-aggregator
429
+ // tests that snapshot the line content.
430
+ if (role === 'peer') {
431
+ const pid = args && typeof args.peer_id === 'string' && args.peer_id.length > 0
432
+ ? args.peer_id
433
+ : null;
434
+ out.peer_id = pid;
435
+ }
436
+ return out;
437
+ }
438
+
439
+ /**
440
+ * Phase 27 / Plan 27-08 helper — derive `(runtime_role, peer_id)` from a
441
+ * router decision shape, mirroring `modelFromResolved`'s contract:
442
+ *
443
+ * - When the router decision carries `runtime_role: "peer"` AND
444
+ * `peer_id: "<non-empty-string>"`, return `{ role: 'peer', peer_id }`.
445
+ * - When the decision is absent, malformed, or not flagged as peer-mode,
446
+ * return `{ role: 'host', peer_id: null }`.
447
+ *
448
+ * Plan 27-06's session-runner sets `runtime_role` + `peer_id` on its
449
+ * router-decision payload before invoking the budget-enforcer hook; this
450
+ * helper is the pure backend lookup that the hook (and any non-CC
451
+ * cost-recorder mirror) calls to thread those values into the cost row.
452
+ *
453
+ * Defensive on every shape — never throws on null / wrong type. Returning
454
+ * `host` on every error path keeps the legacy back-compat default
455
+ * everywhere.
456
+ *
457
+ * @param {unknown} routerDecision
458
+ * @returns {{ role: 'host' | 'peer', peer_id: string | null }}
459
+ */
460
+ function roleFromRouterDecision(routerDecision) {
461
+ if (!routerDecision || typeof routerDecision !== 'object') {
462
+ return { role: 'host', peer_id: null };
463
+ }
464
+ const role = routerDecision.runtime_role;
465
+ if (role !== 'peer') return { role: 'host', peer_id: null };
466
+ const pid = routerDecision.peer_id;
467
+ if (typeof pid !== 'string' || pid.length === 0) {
468
+ // peer-flagged but no peer_id is malformed — degrade to host to
469
+ // avoid emitting a half-tagged cost row.
470
+ return { role: 'host', peer_id: null };
471
+ }
472
+ return { role: 'peer', peer_id: pid };
473
+ }
474
+
475
+ /**
476
+ * Resolve a concrete model_id from the router's `resolved_models` map
477
+ * for a given agent. Returns null when:
478
+ * - resolved_models is absent / not an object;
479
+ * - the agent key is missing;
480
+ * - the value is not a non-empty string.
481
+ *
482
+ * Hosts on the resolved_models consumer path (D-07): if this returns
483
+ * non-null, the cost lookup goes through the per-runtime price table by
484
+ * model_id; otherwise the caller falls back to the legacy
485
+ * model_tier_overrides path.
486
+ *
487
+ * @param {unknown} resolvedModels
488
+ * @param {string} agent
489
+ * @returns {string|null}
490
+ */
491
+ function modelFromResolved(resolvedModels, agent) {
492
+ if (!resolvedModels || typeof resolvedModels !== 'object') return null;
493
+ const v = resolvedModels[agent];
494
+ if (typeof v === 'string' && v.length > 0) return v;
495
+ return null;
496
+ }
497
+
498
+ module.exports = {
499
+ computeCost,
500
+ buildCostEventPayload,
501
+ modelFromResolved,
502
+ // Plan 27-08 (D-09): runtime-role + peer-id derivation from router
503
+ // decision. Used by the .ts hook and any non-CC cost-recorder mirror to
504
+ // thread peer-delegation tags into cost.jsonl rows.
505
+ roleFromRouterDecision,
506
+ parsePriceTable,
507
+ loadPriceTable,
508
+ priceTablePath,
509
+ reset,
510
+ VALID_TIERS,
511
+ DEFAULT_RUNTIME_ID,
512
+ // surfaced for tests only — stable API = computeCost + modelFromResolved
513
+ _internal: { findPriceRow, applyFormula },
514
+ };