@xerg/cli 0.4.0 → 0.5.1

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.
package/dist/index.js CHANGED
@@ -1280,6 +1280,7 @@ function hydrateAuditSummary(summary) {
1280
1280
  opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
1281
1281
  spendByDay: summary.spendByDay ?? [],
1282
1282
  wasteByDay: summary.wasteByDay ?? [],
1283
+ recommendations: summary.recommendations ?? [],
1283
1284
  notes: summary.notes ?? [],
1284
1285
  pricingCoverage: summary.pricingCoverage ?? null,
1285
1286
  cursorUsage: summary.cursorUsage ?? null
@@ -1416,6 +1417,7 @@ function buildCursorUsageFindings(runs) {
1416
1417
  summary,
1417
1418
  scope: "global",
1418
1419
  scopeId: "all",
1420
+ scopeLabel: "Cursor usage",
1419
1421
  costImpactUsd: cacheImpactUsd,
1420
1422
  details: {
1421
1423
  cacheReadShare: round3(cacheReadShare),
@@ -1443,6 +1445,7 @@ function buildCursorUsageFindings(runs) {
1443
1445
  summary: `Max mode accounts for ${(maxModeSpendShare * 100).toFixed(0)}% of billed spend across ${maxModeCalls.length} paid row${maxModeCalls.length === 1 ? "" : "s"}. This is a strong candidate for splitting work between premium and standard passes.`,
1444
1446
  scope: "global",
1445
1447
  scopeId: "all",
1448
+ scopeLabel: "Cursor usage",
1446
1449
  costImpactUsd: round3(maxModeSpendUsd * 0.2),
1447
1450
  details: {
1448
1451
  maxModeSpendUsd: round3(maxModeSpendUsd),
@@ -1502,6 +1505,7 @@ function buildFindings(runs) {
1502
1505
  summary: `${retryCandidates.length} failed call${retryCandidates.length === 1 ? "" : "s"} were followed by additional work, making their spend pure retry overhead.`,
1503
1506
  scope: "global",
1504
1507
  scopeId: "all",
1508
+ scopeLabel: "workspace",
1505
1509
  costImpactUsd: round4(retryCost),
1506
1510
  details: {
1507
1511
  failedCallCount: retryCandidates.length
@@ -1529,7 +1533,8 @@ function buildFindings(runs) {
1529
1533
  title: `Workflow "${run2.workflow}" ran beyond efficient loop bounds`,
1530
1534
  summary: `This run reached ${maxIteration} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,
1531
1535
  scope: "run",
1532
- scopeId: run2.id,
1536
+ scopeId: run2.workflow,
1537
+ scopeLabel: run2.workflow,
1533
1538
  costImpactUsd: round4(loopCost),
1534
1539
  details: {
1535
1540
  workflow: run2.workflow,
@@ -1566,6 +1571,7 @@ function buildFindings(runs) {
1566
1571
  summary: `Xerg found ${outlierRuns.length} run${outlierRuns.length === 1 ? "" : "s"} in this workflow with input token volume far above the workflow average.`,
1567
1572
  scope: "workflow",
1568
1573
  scopeId: workflow,
1574
+ scopeLabel: workflow,
1569
1575
  costImpactUsd: round4(outlierCost),
1570
1576
  details: {
1571
1577
  workflow,
@@ -1590,6 +1596,7 @@ function buildFindings(runs) {
1590
1596
  summary: "This workflow name looks like a recurring heartbeat or monitoring loop. Review whether the cadence and model tier are justified.",
1591
1597
  scope: "workflow",
1592
1598
  scopeId: workflow,
1599
+ scopeLabel: workflow,
1593
1600
  costImpactUsd: round4(idleCost),
1594
1601
  details: {
1595
1602
  workflow
@@ -1611,6 +1618,7 @@ function buildFindings(runs) {
1611
1618
  summary: "An expensive model is being used on a workflow that looks operationally simple. Treat this as an A/B test candidate, not proven waste.",
1612
1619
  scope: "workflow",
1613
1620
  scopeId: workflow,
1621
+ scopeLabel: workflow,
1614
1622
  costImpactUsd: round4(spend * 0.3),
1615
1623
  details: {
1616
1624
  workflow,
@@ -1703,6 +1711,247 @@ function estimateCostUsd(provider, model, inputTokens, outputTokens) {
1703
1711
  return Number((inputCost + outputCost).toFixed(8));
1704
1712
  }
1705
1713
 
1714
+ // ../core/src/recommendations.ts
1715
+ var PRIORITY_RANK = {
1716
+ fix_now: 2,
1717
+ test_next: 1,
1718
+ watch: 0
1719
+ };
1720
+ var CONFIDENCE_RANK = {
1721
+ high: 2,
1722
+ medium: 1,
1723
+ low: 0
1724
+ };
1725
+ function roundCurrency(value) {
1726
+ return Number(value.toFixed(6));
1727
+ }
1728
+ function roundPct(value) {
1729
+ return Number(value.toFixed(4));
1730
+ }
1731
+ function formatScopeLabel(finding) {
1732
+ if (finding.scope === "global") {
1733
+ return "workspace";
1734
+ }
1735
+ return finding.scopeLabel?.trim() || finding.scopeId;
1736
+ }
1737
+ function normalizeRecommendationScope(finding) {
1738
+ if (finding.scope === "global") {
1739
+ return "workspace";
1740
+ }
1741
+ return "workflow";
1742
+ }
1743
+ function stableScopeKeyForFinding(finding) {
1744
+ if (finding.scope === "global") {
1745
+ return "workspace";
1746
+ }
1747
+ return finding.scopeId;
1748
+ }
1749
+ function resolveScopeId(finding, scope) {
1750
+ return scope === "workspace" ? "workspace" : stableScopeKeyForFinding(finding);
1751
+ }
1752
+ function dedupKeyForFinding(finding) {
1753
+ const scope = normalizeRecommendationScope(finding);
1754
+ return `${finding.kind}:${scope}:${stableScopeKeyForFinding(finding)}`;
1755
+ }
1756
+ function recommendationIdForFinding(finding, implementationSurface) {
1757
+ const scope = normalizeRecommendationScope(finding);
1758
+ return sha1(
1759
+ `rec:v2:${finding.kind}:${scope}:${stableScopeKeyForFinding(finding)}:${implementationSurface}`
1760
+ );
1761
+ }
1762
+ function compareRecommendations(left, right) {
1763
+ const priorityDelta = PRIORITY_RANK[right.priorityBucket] - PRIORITY_RANK[left.priorityBucket];
1764
+ if (priorityDelta !== 0) {
1765
+ return priorityDelta;
1766
+ }
1767
+ if (right.estimatedSavingsUsd !== left.estimatedSavingsUsd) {
1768
+ return right.estimatedSavingsUsd - left.estimatedSavingsUsd;
1769
+ }
1770
+ const confidenceDelta = CONFIDENCE_RANK[right.confidence] - CONFIDENCE_RANK[left.confidence];
1771
+ if (confidenceDelta !== 0) {
1772
+ return confidenceDelta;
1773
+ }
1774
+ return left.title.localeCompare(right.title);
1775
+ }
1776
+ var templatesByKind = {
1777
+ "retry-waste": {
1778
+ priorityBucket: "fix_now",
1779
+ implementationSurface: "retry_policy",
1780
+ category: "structural_efficiency",
1781
+ severity: "high",
1782
+ effort: "low",
1783
+ titleFn: (finding) => `Reduce retry waste in ${formatScopeLabel(finding)}`,
1784
+ summaryFn: (finding) => `${finding.summary} This is confirmed retry overhead, so it is a fix-now issue rather than an experiment.`,
1785
+ whereToChangeFn: (finding) => `Reduce retries or add exponential backoff in the retry wrapper for ${formatScopeLabel(finding)}.`,
1786
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push` against the same source. Retry waste should drop materially on the next audit.",
1787
+ actionsFn: () => [
1788
+ "Lower max retry count.",
1789
+ "Add exponential backoff if none exists.",
1790
+ "Log or alert when retries hit the cap."
1791
+ ]
1792
+ },
1793
+ "loop-waste": {
1794
+ priorityBucket: "fix_now",
1795
+ implementationSurface: "loop_guard",
1796
+ category: "structural_efficiency",
1797
+ severity: "high",
1798
+ effort: "medium",
1799
+ titleFn: (finding) => `Cap loop depth in ${formatScopeLabel(finding)}`,
1800
+ summaryFn: (finding) => `${finding.summary} This is confirmed loop waste and should be fixed before chasing lower-confidence opportunities.`,
1801
+ whereToChangeFn: (finding) => `Cap iteration depth or add an early-exit guard in the loop controller for ${formatScopeLabel(finding)}.`,
1802
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push`. Loop waste and call volume should fall on the next audit.",
1803
+ actionsFn: () => [
1804
+ "Add a hard iteration cap.",
1805
+ "Add a no-progress exit.",
1806
+ "Emit a warning when the cap is hit."
1807
+ ]
1808
+ },
1809
+ "context-outlier": {
1810
+ priorityBucket: "test_next",
1811
+ implementationSurface: "prompt_builder",
1812
+ category: "context_hygiene",
1813
+ severity: "medium",
1814
+ effort: "medium",
1815
+ titleFn: (finding) => `Trim context in ${formatScopeLabel(finding)}`,
1816
+ summaryFn: (finding) => `${finding.summary} Treat this as a reversible optimization test rather than proven waste.`,
1817
+ whereToChangeFn: (finding) => `Trim input context in the prompt builder for ${formatScopeLabel(finding)}.`,
1818
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push`. Input tokens and context-related spend should fall.",
1819
+ actionsFn: () => [
1820
+ "Drop stale context blocks.",
1821
+ "Summarize long histories.",
1822
+ "Cap prompt size for this workflow."
1823
+ ]
1824
+ },
1825
+ "idle-spend": {
1826
+ priorityBucket: "test_next",
1827
+ implementationSurface: "scheduler",
1828
+ category: "cadence_activity",
1829
+ severity: "medium",
1830
+ effort: "low",
1831
+ titleFn: (finding) => `Review cadence for ${formatScopeLabel(finding)}`,
1832
+ summaryFn: (finding) => `${finding.summary} This is usually a scheduling decision, so validate it by lowering or gating activity instead of rewriting the workflow.`,
1833
+ whereToChangeFn: (finding) => `Lower cadence or move to event-driven triggers for ${formatScopeLabel(finding)}.`,
1834
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push`. Idle spend per day should drop.",
1835
+ actionsFn: () => [
1836
+ "Reduce poll frequency.",
1837
+ "Gate runs behind a real trigger.",
1838
+ "Disable runs during dead windows."
1839
+ ]
1840
+ },
1841
+ "candidate-downgrade": {
1842
+ priorityBucket: "test_next",
1843
+ implementationSurface: "model_routing",
1844
+ category: "model_fit",
1845
+ severity: "low",
1846
+ effort: "low",
1847
+ titleFn: (finding) => `Evaluate a cheaper model for ${formatScopeLabel(finding)}`,
1848
+ summaryFn: (finding) => `${finding.summary} This is an A/B candidate: spend may fall, but quality needs to be checked before rollout.`,
1849
+ whereToChangeFn: (finding) => `Re-map ${formatScopeLabel(finding)} to a cheaper model in the routing layer.`,
1850
+ validationPlanFn: () => "A/B the cheaper model, then rerun `xerg audit --compare --push`. Confirm spend drops without a quality regression.",
1851
+ actionsFn: () => [
1852
+ "Try a cheaper model on this workflow.",
1853
+ "Compare quality on a labeled sample.",
1854
+ "Roll out if acceptable."
1855
+ ]
1856
+ },
1857
+ "cache-carryover": {
1858
+ priorityBucket: "test_next",
1859
+ implementationSurface: "user_behavior",
1860
+ category: "context_hygiene",
1861
+ severity: "medium",
1862
+ effort: "low",
1863
+ titleFn: () => "Reset or summarize long Cursor chats",
1864
+ summaryFn: (finding) => `${finding.summary} This is about session behavior rather than code structure, so the intervention is to reset context more aggressively.`,
1865
+ whereToChangeFn: () => "Reset or summarize long Cursor chats instead of continuing the same session.",
1866
+ validationPlanFn: () => "Change session behavior, then push a new Cursor usage audit with `--compare --push`. Cache-read share should fall.",
1867
+ actionsFn: () => [
1868
+ "Summarize context into a short recall note.",
1869
+ "Start a fresh chat.",
1870
+ "Carry forward only the recall note and necessary facts."
1871
+ ]
1872
+ },
1873
+ "max-mode-concentration": {
1874
+ priorityBucket: "test_next",
1875
+ implementationSurface: "user_behavior",
1876
+ category: "model_fit",
1877
+ severity: "medium",
1878
+ effort: "low",
1879
+ titleFn: () => "Reserve max mode for the hardest Cursor turns",
1880
+ summaryFn: (finding) => `${finding.summary} The likely fix is changing mode habits or escalation rules, not modifying a prompt builder.`,
1881
+ whereToChangeFn: () => "Reserve max mode for the hardest Cursor turns; default to standard mode.",
1882
+ validationPlanFn: () => "Change your mode defaults, then push a new Cursor usage audit with `--compare --push`. Max-mode spend share should fall.",
1883
+ actionsFn: () => [
1884
+ "Start in standard mode.",
1885
+ "Escalate only when standard fails.",
1886
+ "Review any auto-escalation habit."
1887
+ ]
1888
+ }
1889
+ };
1890
+ var unknownTemplate = {
1891
+ priorityBucket: "watch",
1892
+ implementationSurface: "other",
1893
+ category: "other",
1894
+ severity: "low",
1895
+ effort: "medium",
1896
+ titleFn: (finding) => `Review ${finding.title}`,
1897
+ summaryFn: (finding) => finding.summary,
1898
+ whereToChangeFn: (finding) => `Review ${formatScopeLabel(finding)} with your operator.`,
1899
+ validationPlanFn: () => "Ship any change, then rerun `xerg audit --compare --push` to confirm impact.",
1900
+ actionsFn: () => [
1901
+ "Investigate the finding details.",
1902
+ "Decide whether to act.",
1903
+ "Re-audit after the change."
1904
+ ]
1905
+ };
1906
+ function buildSingleRecommendation(summary, finding) {
1907
+ const template = templatesByKind[finding.kind] ?? unknownTemplate;
1908
+ const scope = normalizeRecommendationScope(finding);
1909
+ const scopeId = resolveScopeId(finding, scope);
1910
+ const scopeLabel = formatScopeLabel(finding);
1911
+ const estimatedSavingsPct = summary.totalSpendUsd === 0 ? 0 : roundPct(Math.min(Math.max(finding.costImpactUsd / summary.totalSpendUsd, 0), 1));
1912
+ return {
1913
+ id: recommendationIdForFinding(finding, template.implementationSurface),
1914
+ findingId: finding.id,
1915
+ kind: finding.kind,
1916
+ title: template.titleFn(finding),
1917
+ summary: template.summaryFn(finding),
1918
+ priorityBucket: template.priorityBucket,
1919
+ recommendedOrder: 0,
1920
+ implementationSurface: template.implementationSurface,
1921
+ category: template.category,
1922
+ severity: template.severity,
1923
+ estimatedSavingsUsd: roundCurrency(finding.costImpactUsd),
1924
+ estimatedSavingsPct,
1925
+ confidence: finding.confidence,
1926
+ effort: template.effort,
1927
+ scope,
1928
+ scopeId,
1929
+ scopeLabel,
1930
+ whereToChange: template.whereToChangeFn(finding),
1931
+ validationPlan: template.validationPlanFn(finding),
1932
+ actions: template.actionsFn(finding)
1933
+ };
1934
+ }
1935
+ function buildRecommendations(summary) {
1936
+ const deduped = /* @__PURE__ */ new Map();
1937
+ for (const finding of summary.findings) {
1938
+ const recommendation = buildSingleRecommendation(summary, finding);
1939
+ const key = dedupKeyForFinding(finding);
1940
+ const existing = deduped.get(key);
1941
+ if (!existing) {
1942
+ deduped.set(key, recommendation);
1943
+ continue;
1944
+ }
1945
+ if (recommendation.estimatedSavingsUsd > existing.estimatedSavingsUsd || recommendation.estimatedSavingsUsd === existing.estimatedSavingsUsd && CONFIDENCE_RANK[recommendation.confidence] > CONFIDENCE_RANK[existing.confidence]) {
1946
+ deduped.set(key, recommendation);
1947
+ }
1948
+ }
1949
+ return [...deduped.values()].sort(compareRecommendations).map((recommendation, index) => ({
1950
+ ...recommendation,
1951
+ recommendedOrder: index + 1
1952
+ }));
1953
+ }
1954
+
1706
1955
  // ../core/src/report/timeseries.ts
1707
1956
  function round5(value) {
1708
1957
  return Number(value.toFixed(6));
@@ -1856,7 +2105,7 @@ function buildAuditSummary(input) {
1856
2105
  const generatedAt = isoNow();
1857
2106
  const spendByDay = buildSpendByDay(input.runs);
1858
2107
  const observedDays = buildObservedUtcDayRange(input.runs);
1859
- return {
2108
+ const summary = {
1860
2109
  auditId: sha1(
1861
2110
  `${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
1862
2111
  ),
@@ -1900,6 +2149,7 @@ function buildAuditSummary(input) {
1900
2149
  spendByDay,
1901
2150
  wasteByDay: buildWasteByDay(input.wasteAttributions, observedDays, wasteSpendUsd),
1902
2151
  findings: input.findings,
2152
+ recommendations: [],
1903
2153
  notes: [
1904
2154
  "Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
1905
2155
  "Opportunity findings are directional recommendations, not proven waste."
@@ -1907,6 +2157,8 @@ function buildAuditSummary(input) {
1907
2157
  sourceFiles: input.sources,
1908
2158
  dbPath: input.dbPath
1909
2159
  };
2160
+ summary.recommendations = buildRecommendations(summary);
2161
+ return summary;
1910
2162
  }
1911
2163
 
1912
2164
  // ../core/src/runtime.ts
@@ -3169,109 +3421,6 @@ async function auditCursorUsageCsv(options) {
3169
3421
  return summary;
3170
3422
  }
3171
3423
 
3172
- // ../core/src/recommendations.ts
3173
- var templatesByKind = {
3174
- "retry-waste": {
3175
- actionType: "other",
3176
- titleFn: () => "Add retry backoff or reduce retry attempts",
3177
- descriptionFn: (f) => `${f.summary} Consider adding exponential backoff or reducing the maximum retry count to eliminate this overhead.`,
3178
- suggestedChangeFn: (f) => ({
3179
- strategy: "exponential-backoff",
3180
- maxRetries: 3,
3181
- failedCallCount: f.details.failedCallCount
3182
- })
3183
- },
3184
- "loop-waste": {
3185
- actionType: "other",
3186
- titleFn: (f) => `Cap iteration depth for ${extractWorkflow(f)}`,
3187
- descriptionFn: (f) => `${f.summary} Adding an iteration limit or early-exit condition would prevent runaway loops from burning spend.`,
3188
- suggestedChangeFn: (f) => ({
3189
- strategy: "iteration-cap",
3190
- suggestedMaxIterations: 5,
3191
- observedMaxIteration: f.details.maxIteration
3192
- })
3193
- },
3194
- "context-outlier": {
3195
- actionType: "prompt-trim",
3196
- titleFn: (f) => `Trim context for ${extractWorkflow(f)}`,
3197
- descriptionFn: (f) => `${f.summary} Reducing input token volume to near the workflow baseline would lower cost proportionally.`,
3198
- suggestedChangeFn: (f) => ({
3199
- strategy: "context-reduction",
3200
- averageInputTokens: f.details.averageInputTokens
3201
- })
3202
- },
3203
- "candidate-downgrade": {
3204
- actionType: "model-switch",
3205
- titleFn: (f) => `Evaluate cheaper model for ${extractWorkflow(f)}`,
3206
- descriptionFn: (f) => `${f.summary} This is an A/B test candidate \u2014 try a cheaper model on this workflow and compare quality.`,
3207
- suggestedChangeFn: () => ({
3208
- strategy: "model-downgrade",
3209
- candidates: ["claude-3-haiku", "gpt-4o-mini"]
3210
- })
3211
- },
3212
- "idle-spend": {
3213
- actionType: "other",
3214
- titleFn: (f) => `Review cadence for ${extractWorkflow(f)}`,
3215
- descriptionFn: (f) => `${f.summary} Consider reducing polling frequency or switching to an event-driven approach.`,
3216
- suggestedChangeFn: () => ({
3217
- strategy: "cadence-review"
3218
- })
3219
- },
3220
- "cache-carryover": {
3221
- actionType: "prompt-trim",
3222
- titleFn: () => "Summarize and reset long Cursor chats",
3223
- descriptionFn: (f) => `${f.summary} Create a compact recall summary, start a fresh chat, and carry forward only the facts the model actually needs.`,
3224
- suggestedChangeFn: (f) => ({
3225
- strategy: "conversation-reset",
3226
- cacheReadShare: f.details.cacheReadShare,
3227
- totalCacheReadTokens: f.details.totalCacheReadTokens
3228
- })
3229
- },
3230
- "max-mode-concentration": {
3231
- actionType: "model-switch",
3232
- titleFn: () => "Reserve max mode for the hardest Cursor turns",
3233
- descriptionFn: (f) => `${f.summary} Try a two-pass workflow: standard mode first, then escalate only the prompts that truly need max mode.`,
3234
- suggestedChangeFn: (f) => ({
3235
- strategy: "tiered-routing",
3236
- maxModeSpendShare: f.details.maxModeSpendShare,
3237
- maxModeCallCount: f.details.maxModeCallCount
3238
- })
3239
- }
3240
- };
3241
- function extractWorkflow(finding) {
3242
- const details = finding.details;
3243
- return details.workflow || finding.scopeId || "this workflow";
3244
- }
3245
- function buildSingleRecommendation(finding) {
3246
- const template = templatesByKind[finding.kind];
3247
- if (template) {
3248
- return {
3249
- id: sha1(`rec:${finding.id}:${template.actionType}`),
3250
- findingId: finding.id,
3251
- kind: finding.kind,
3252
- title: template.titleFn(finding),
3253
- description: template.descriptionFn(finding),
3254
- estimatedSavingsUsd: finding.costImpactUsd,
3255
- confidence: finding.confidence,
3256
- actionType: template.actionType,
3257
- suggestedChange: template.suggestedChangeFn?.(finding)
3258
- };
3259
- }
3260
- return {
3261
- id: sha1(`rec:${finding.id}:other`),
3262
- findingId: finding.id,
3263
- kind: finding.kind,
3264
- title: `Review: ${finding.title}`,
3265
- description: finding.summary,
3266
- estimatedSavingsUsd: finding.costImpactUsd,
3267
- confidence: finding.confidence,
3268
- actionType: "other"
3269
- };
3270
- }
3271
- function buildRecommendations(summary) {
3272
- return summary.findings.map(buildSingleRecommendation);
3273
- }
3274
-
3275
3424
  // ../core/src/report/render.ts
3276
3425
  function formatUsd(value) {
3277
3426
  return new Intl.NumberFormat("en-US", {
@@ -3330,16 +3479,6 @@ function renderTaxonomyBlock(summary) {
3330
3479
  function topFinding(summary, classification) {
3331
3480
  return summary.findings.filter((finding) => finding.classification === classification).sort((left, right) => right.costImpactUsd - left.costImpactUsd)[0];
3332
3481
  }
3333
- function topSavingsTest(summary) {
3334
- return summary.findings.filter((finding) => finding.classification === "opportunity").sort((left, right) => {
3335
- const leftPriority = left.kind === "candidate-downgrade" ? 1 : 0;
3336
- const rightPriority = right.kind === "candidate-downgrade" ? 1 : 0;
3337
- if (leftPriority !== rightPriority) {
3338
- return rightPriority - leftPriority;
3339
- }
3340
- return right.costImpactUsd - left.costImpactUsd;
3341
- })[0] ?? null;
3342
- }
3343
3482
  function renderFindingList(findings, emptyLabel) {
3344
3483
  if (findings.length === 0) {
3345
3484
  return [`- ${emptyLabel}`];
@@ -3348,6 +3487,33 @@ function renderFindingList(findings, emptyLabel) {
3348
3487
  return `- ${finding.title}: ${formatUsd(finding.costImpactUsd)} (${finding.confidence})`;
3349
3488
  });
3350
3489
  }
3490
+ function renderActionQueueRow(label, recommendation) {
3491
+ if (!recommendation) {
3492
+ return [`${label}`, "- none"];
3493
+ }
3494
+ return [
3495
+ label,
3496
+ `- ${recommendation.title} [${recommendation.scopeLabel}]: ${formatUsd(recommendation.estimatedSavingsUsd)} (${recommendation.severity})`
3497
+ ];
3498
+ }
3499
+ function renderActionQueue(summary) {
3500
+ const fixNow = summary.recommendations.find(
3501
+ (recommendation) => recommendation.priorityBucket === "fix_now"
3502
+ );
3503
+ const testNext = summary.recommendations.find(
3504
+ (recommendation) => recommendation.priorityBucket === "test_next"
3505
+ );
3506
+ const watch = summary.recommendations.find(
3507
+ (recommendation) => recommendation.priorityBucket === "watch"
3508
+ );
3509
+ return [
3510
+ "## Action queue",
3511
+ ...renderActionQueueRow("Fix now", fixNow),
3512
+ ...renderActionQueueRow("Test next", testNext),
3513
+ ...renderActionQueueRow("Watch", watch),
3514
+ "How to validate: `xerg audit --compare --push`"
3515
+ ];
3516
+ }
3351
3517
  function describeSpendDelta(delta) {
3352
3518
  return `${delta.key} (${formatUsdDelta(delta.deltaSpendUsd)})`;
3353
3519
  }
@@ -3570,6 +3736,8 @@ function renderCursorTerminalSummary(summary) {
3570
3736
  "## Findings",
3571
3737
  ...renderFindingList(summary.findings, "none detected"),
3572
3738
  "",
3739
+ ...renderActionQueue(summary),
3740
+ "",
3573
3741
  ...renderCursorCompareBlock(summary),
3574
3742
  ...summary.comparison ? [""] : [],
3575
3743
  "## Notes",
@@ -3613,6 +3781,8 @@ function renderCursorMarkdownSummary(summary) {
3613
3781
  ...summary.findings.slice(0, 10).map((finding) => {
3614
3782
  return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3615
3783
  }),
3784
+ "",
3785
+ ...renderActionQueue(summary),
3616
3786
  ...summary.comparison ? ["", ...renderCursorCompareBlock(summary)] : [],
3617
3787
  "",
3618
3788
  "## Notes",
@@ -3627,7 +3797,6 @@ function renderTerminalSummary(summary) {
3627
3797
  const opportunityFindings = summary.findings.filter(
3628
3798
  (finding) => finding.classification === "opportunity"
3629
3799
  );
3630
- const topSavings = topSavingsTest(summary);
3631
3800
  const topWaste = topFinding(summary, "waste");
3632
3801
  return [
3633
3802
  "# Xerg audit",
@@ -3656,11 +3825,8 @@ function renderTerminalSummary(summary) {
3656
3825
  "## Opportunities",
3657
3826
  ...renderFindingList(opportunityFindings, "none detected"),
3658
3827
  "",
3659
- "## First savings test",
3660
- ...topSavings ? [
3661
- `- Start with ${topSavings.title}: ${formatUsd(topSavings.costImpactUsd)} of potential impact`,
3662
- `- Why this test first: ${topSavings.summary}`
3663
- ] : ["- No savings test surfaced yet"],
3828
+ ...renderActionQueue(summary),
3829
+ "",
3664
3830
  ...topWaste ? [`- Confirmed leak to close first: ${topWaste.title}`] : ["- Confirmed leak to close first: none"],
3665
3831
  ...summary.spendByWorkflow[0] ? [`- Workflow to inspect first: ${summary.spendByWorkflow[0].key}`] : ["- Workflow to inspect first: none"],
3666
3832
  "",
@@ -3697,7 +3863,9 @@ function renderMarkdownSummary(summary) {
3697
3863
  "## Findings",
3698
3864
  ...summary.findings.slice(0, 10).map((finding) => {
3699
3865
  return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3700
- })
3866
+ }),
3867
+ "",
3868
+ ...renderActionQueue(summary)
3701
3869
  ];
3702
3870
  if (summary.comparison) {
3703
3871
  const comparison = summary.comparison;
@@ -3714,7 +3882,7 @@ function renderMarkdownSummary(summary) {
3714
3882
  }
3715
3883
 
3716
3884
  // ../schemas/dist/index.js
3717
- var AUDIT_PUSH_PAYLOAD_VERSION = 1;
3885
+ var AUDIT_PUSH_PAYLOAD_VERSION = 2;
3718
3886
 
3719
3887
  // ../core/src/wire.ts
3720
3888
  function toWireFinding(finding) {
@@ -3766,6 +3934,7 @@ function toWirePayload(summary, meta) {
3766
3934
  spendByDay: summary.spendByDay,
3767
3935
  wasteByDay: summary.wasteByDay,
3768
3936
  findings: summary.findings.map(toWireFinding),
3937
+ recommendations: summary.recommendations,
3769
3938
  notes: summary.notes,
3770
3939
  comparison: summary.comparison ? toWireComparison(summary.comparison) : null
3771
3940
  },
@@ -5134,11 +5303,10 @@ ${errorMessages}`);
5134
5303
  summaries.push({ name: source.name, source, summary });
5135
5304
  }
5136
5305
  if (options.json) {
5137
- const output = summaries.length === 1 ? { ...summaries[0].summary, recommendations: buildRecommendations(summaries[0].summary) } : {
5306
+ const output = summaries.length === 1 ? summaries[0].summary : {
5138
5307
  sources: summaries.map((s) => ({
5139
5308
  name: s.name,
5140
- ...s.summary,
5141
- recommendations: buildRecommendations(s.summary)
5309
+ ...s.summary
5142
5310
  }))
5143
5311
  };
5144
5312
  process.stdout.write(`${JSON.stringify(output, null, 2)}
@@ -5216,9 +5384,7 @@ function renderOutput(summary, options) {
5216
5384
  return;
5217
5385
  }
5218
5386
  if (options.json) {
5219
- const recommendations = buildRecommendations(summary);
5220
- const output = { ...summary, recommendations };
5221
- process.stdout.write(`${JSON.stringify(output, null, 2)}
5387
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}
5222
5388
  `);
5223
5389
  return;
5224
5390
  }
@@ -5823,6 +5989,7 @@ function renderRailwayDoctorReport(report) {
5823
5989
  import { existsSync as existsSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
5824
5990
  import { dirname as dirname3, join as join8 } from "path";
5825
5991
  var HOSTED_MCP_URL = "https://mcp.xerg.ai/mcp";
5992
+ var MCP_SERVER_NAME = "xerg";
5826
5993
  async function runMcpSetupCommand() {
5827
5994
  await runMcpSetupFlow();
5828
5995
  }
@@ -5865,6 +6032,11 @@ async function runMcpSetupFlow() {
5865
6032
  value: "claude-code",
5866
6033
  description: "Project-scoped Claude Code MCP config"
5867
6034
  },
6035
+ {
6036
+ name: "Codex",
6037
+ value: "codex",
6038
+ description: "Codex config.toml snippet"
6039
+ },
5868
6040
  {
5869
6041
  name: "Other",
5870
6042
  value: "other",
@@ -5876,6 +6048,14 @@ async function runMcpSetupFlow() {
5876
6048
  await handleCursorSetup(snippet, config);
5877
6049
  return;
5878
6050
  }
6051
+ if (client === "codex") {
6052
+ process.stdout.write(`${buildCodexMcpConfig(config)}
6053
+ `);
6054
+ process.stderr.write(
6055
+ "Add this to `~/.codex/config.toml`, then restart Codex so it loads the Xerg MCP tools.\n"
6056
+ );
6057
+ return;
6058
+ }
5879
6059
  process.stdout.write(`${snippet}
5880
6060
  `);
5881
6061
  if (client === "claude-code") {
@@ -5913,7 +6093,7 @@ async function handleCursorSetup(snippet, config) {
5913
6093
  function buildHostedMcpConfig(config) {
5914
6094
  return {
5915
6095
  mcpServers: {
5916
- xerg: {
6096
+ [MCP_SERVER_NAME]: {
5917
6097
  type: "http",
5918
6098
  url: HOSTED_MCP_URL,
5919
6099
  headers: {
@@ -5923,6 +6103,19 @@ function buildHostedMcpConfig(config) {
5923
6103
  }
5924
6104
  };
5925
6105
  }
6106
+ function buildCodexMcpConfig(config) {
6107
+ return [
6108
+ `[mcp_servers.${MCP_SERVER_NAME}]`,
6109
+ "enabled = true",
6110
+ `url = ${tomlString(HOSTED_MCP_URL)}`,
6111
+ "",
6112
+ `[mcp_servers.${MCP_SERVER_NAME}.http_headers]`,
6113
+ `Authorization = ${tomlString(`Bearer ${config.apiKey}`)}`
6114
+ ].join("\n");
6115
+ }
6116
+ function tomlString(value) {
6117
+ return JSON.stringify(value);
6118
+ }
5926
6119
  function writeCursorConfig(filePath, config) {
5927
6120
  mkdirSync6(dirname3(filePath), { recursive: true });
5928
6121
  let parsed = {};
@@ -6259,7 +6452,7 @@ Notes:
6259
6452
  function renderMcpSetupHelp(commandPrefix) {
6260
6453
  return `${formatCommand("mcp-setup", commandPrefix)}
6261
6454
 
6262
- Generate hosted MCP client configuration for Cursor, Claude Code, or another MCP client.
6455
+ Generate hosted MCP client configuration for Cursor, Claude Code, Codex, or another MCP client.
6263
6456
 
6264
6457
  Usage:
6265
6458
  ${formatCommand("mcp-setup", commandPrefix)}
@@ -6268,6 +6461,7 @@ Notes:
6268
6461
  - Interactive in v1 because client selection is prompt-driven
6269
6462
  - Uses the hosted MCP endpoint at https://mcp.xerg.ai/mcp
6270
6463
  - Can write a project-scoped Cursor config when .cursor/ already exists
6464
+ - Prints a Codex config.toml snippet when Codex is selected
6271
6465
  - Local audits and compare stay available even if you skip hosted MCP setup
6272
6466
 
6273
6467
  -h, --help Show help