agent-orchestrator-kit 0.14.1 → 0.15.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.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.15.0] - 2026-09-08
8
+
9
+ ### Added
10
+ - **`costUsdTotal` — one USD figure per change, phase, platform, model, and session.** Each session contributes its billed `costUsd` when present, otherwise its `costUsdEstimated`; sums are rounded to 4 decimals. A change that ran on Amp (billed `$14.48`) plus Claude (`~$5.16`) and Cursor (`~$1.43`) now carries `spend.costUsdTotal: 21.0779` instead of forcing a dashboard to pick `costUsd` and drop the estimated platforms. `costUsd` and `costUsdEstimated` stay separate fields and Amp credits stay out of every USD field. `metrics --summary-json` carries the field; the human `cost:` line prints `$21.08 ($14.48 billed + ~$6.60 est.)`.
11
+
7
12
  ### Fixed
13
+ - **Billed sums are rounded like estimates.** `spendByModel[].costUsd`, `spendByPlatform.*.costUsd`, `phases.*.costUsd`, and `spend.costUsd` stored raw float sums such as `4.4399999999999995` and `0.009000000000000001`; they are now `Math.round(x * 10000) / 10000`, the same rule `costUsdEstimated` already had.
8
14
  - **Cache-split cost estimates survive the session recompute.** `applyCollectedSessionFields` recomputed `byModel[].costUsdEstimated` from `inputTokens` + `outputTokens` alone and overwrote the value the adapters had already produced. Since `inputTokens` includes `cache_read_*` / `cache_creation_*`, every cached token was billed at the full input rate: a real Claude change with 3.5M tokens estimated `$30.11` instead of `$5.16`, and Cursor rows lost their own rate table to the Claude `$3/$15` fallback. The recompute now only fills rows that have no adapter estimate, so ESTIMATE-ALL still covers Amp rows without `Cost:` while `claude-opus-5` with 100k input + 900k cache read + 10k output stays at `$1.20`.
9
15
 
10
16
  ## [0.14.1] - 2026-09-08
package/README.md CHANGED
@@ -74,7 +74,7 @@ npx agent-orchestrator-kit@latest init --profile generic --ci gitlab --spec-veri
74
74
 
75
75
  See [Installation](#installation) for profile/CI options.
76
76
 
77
- **🔄 Already have the kit installed? Upgrade to latest (v0.14.1 fixes a `handoff` persist that wiped the Memory graph, syncs the `/opsx:*` commands into Cursor and Claude Code, and stops `status` / `gate-check` from reporting a gate as met when it is not):**
77
+ **🔄 Already have the kit installed? Upgrade to latest (v0.15.0 adds `costUsdTotal`, one USD figure per change / phase / platform / model that sums billed Amp with estimated Cursor and Claude, rounds billed sums like estimates, and keeps cache-split Claude estimates from being re-billed at the full input rate):**
78
78
 
79
79
  ```bash
80
80
  npx agent-orchestrator-kit@latest update
@@ -816,7 +816,7 @@ Schema v2 stores compact `sourceIds`, `sourceTotals`, and `byModel` per session
816
816
  - **Locked client** — `--restore` records `pending.platform` and Amp `pending.threadId` before phase work. Persist follows that client’s flow even if persist runs in another shell (no `AMP_*` / `CURSOR_*`). Amp: `amp threads export` plus `amp threads usage --details` (`AOK_AMP_BIN`) and local `threads/*.json`. Export supplies `model` / tokens / `agentMode`; usage supplies billed `$`. If Amp runs tools over a pipe (`/dev/null`), thread id comes from `amp threads list`, not stale `session.json` `lastThreadId`. When env and Amp parent do not win, restore locks a fresh `session.json` `lastThreadId` as `amp-session-last`. Cursor: spend hook file. Claude: `~/.claude/projects`. `--collect` still runs all three adapters.
817
817
  - **Cursor spend hook (optional)** — Cursor never writes token usage to disk, so the kit can install `scripts/cursor-spend-hook.cjs` plus `.cursor/hooks.json` entries (`stop` / `subagentStop` / `afterAgentResponse`) in `init` / `update` / `sync` / `mcp-setup`: the hook appends each turn's tokens to gitignored `.agents/spend/cursor-usage.jsonl`. After a successful `stop` / `afterAgentResponse` append the hook runs leftover (fail-open, no stdout). Hook and collect resolve the consumer in a multi-root window (not the first cwd with `.agents` or `openspec`). Persist auto-reads that file when the locked client is Cursor. Persist and restore do not self-heal the hook. `sessionEnd` still runs `scripts/cursor-spend-collect.cjs`. Restart Cursor once after the first install. `status` shows a `Spend capture` section. Claude JSONL remains a fallback. Amp web/CLI spend is taken from `amp threads export` (tokens, model, `agentMode`) and `amp threads usage` (billed USD).
818
818
 
819
- Aggregates are recomputed on every write: per-phase totals (`startedAt`, `endedAt`, `leadTimeMs` from that phase’s sessions, `durationMs` = sum of session work time — not `totals.leadTimeMs` and not `endedAt − startedAt`, tokens, `costUsd`, `costUsdEstimated` to 4 decimals, `sessions`, `roles`, `models`) plus overall `totals` (`sessions`, `cloudSessions`, `durationMs` = sum of session work time, `leadTimeMs` = wall clock from first session start to last session end), `spend` (USD only), and separate **by platform** / **by model** tables. Numbers are null-honest: a metric nobody reported stays `null`, never a fake `0`. No single total that adds Amp credits to USD.
819
+ Aggregates are recomputed on every write: per-phase totals (`startedAt`, `endedAt`, `leadTimeMs` from that phase’s sessions, `durationMs` = sum of session work time — not `totals.leadTimeMs` and not `endedAt − startedAt`, tokens, `costUsd`, `costUsdEstimated`, `costUsdTotal` to 4 decimals, `sessions`, `roles`, `models`) plus overall `totals` (`sessions`, `cloudSessions`, `durationMs` = sum of session work time, `leadTimeMs` = wall clock from first session start to last session end), `spend` (USD only), and separate **by platform** / **by model** tables. Numbers are null-honest: a metric nobody reported stays `null`, never a fake `0`. No single total that adds Amp credits to USD. `costUsdTotal` is the one USD figure per change, phase, platform, model, and session: each session contributes its billed `costUsd` when present, otherwise its `costUsdEstimated`, so a change that ran on Amp (billed) plus Cursor and Claude (estimated) sums all three platforms instead of showing only the billed part. Amp credits never enter it. The human `cost:` line prints it as `$21.08 ($14.48 billed + ~$6.60 est.)`.
820
820
 
821
821
  Fill `## Metrics` in `handoff.md` **before** persist (unknown is fine; do not invent `0`):
822
822
 
@@ -843,7 +843,7 @@ npx agent-orchestrator-kit archive add-thing --sync # finalize + the same t
843
843
 
844
844
  #### Для дашбордів
845
845
 
846
- Use only `phases.<phase>.startedAt`, `endedAt`, and `durationMs` for phase boundaries and duration. Use `totals.leadTimeMs` only for the whole change. Git log MUST NOT be used for phase boundaries; the kit does not provide per-phase commit counts. `metrics <name> --summary-json` returns only aggregate `totals`, `phases`, and spend maps, without sessions or commits.
846
+ Use only `phases.<phase>.startedAt`, `endedAt`, and `durationMs` for phase boundaries and duration. Use `totals.leadTimeMs` only for the whole change. Git log MUST NOT be used for phase boundaries; the kit does not provide per-phase commit counts. `metrics <name> --summary-json` returns only aggregate `totals`, `phases`, and spend maps, without sessions or commits. For the headline cost use `spend.costUsdTotal` (and `phases.<phase>.costUsdTotal`, `spendByPlatform.<platform>.costUsdTotal`, `spendByModel[].costUsdTotal`): `costUsd` alone is only the billed part and `costUsdEstimated` alone is only the estimated part, so picking one of them drops every platform that reported the other.
847
847
 
848
848
  Recording is on by default and never a persist/archive/`gate-check` gate; opt out per persist with `--no-metrics`. Persist and archive collect the locked client without `--collect`; `--collect` runs every adapter. Flags (`--model`, `--platform`, `--input-tokens`, …) override session totals in `metrics.json` and do not rewrite the `## Metrics` section.
849
849
 
@@ -1017,6 +1017,11 @@ Phase bounds and non-goals: [`openspec/specs/agentic-factory-roadmap/spec.md`](o
1017
1017
 
1018
1018
  ## Changelog
1019
1019
 
1020
+ ### 0.15.0
1021
+ - **`costUsdTotal`** on `spend`, `phases.*`, `spendByPlatform.*`, `spendByModel[]`, and sessions — billed `costUsd` when present, otherwise `costUsdEstimated`, per session; a change that ran on Amp (billed) plus Cursor and Claude (estimated) now sums all three platforms instead of showing only the billed part; the human `cost:` line prints `$21.08 ($14.48 billed + ~$6.60 est.)` and `--summary-json` carries the field
1022
+ - Billed `costUsd` aggregates are rounded to 4 decimals like estimates (no more `4.4399999999999995` in `spendByModel`)
1023
+ - Cache-split Claude cost estimates survive the session recompute instead of being re-billed at the full input rate (`$30.11` → `$5.16` on a 3.5M-token change)
1024
+
1020
1025
  ### 0.14.1
1021
1026
  - **Memory graph survives `handoff` persist** — JSONL graphs were misread as an aggregate document and returned empty, so each persist overwrote the file with only the current change; all `Decision:*`, other changes, and relations were lost
1022
1027
  - `/opsx:*` commands now sync to `.cursor/commands/` (flat) and `.claude/commands/opsx/` (namespaced), so the documented slash commands exist in both IDEs
@@ -1710,12 +1710,18 @@ function roundUsd4(x) {
1710
1710
  return Math.round(Number(x) * 10000) / 10000;
1711
1711
  }
1712
1712
 
1713
+ function costUsdTotalOf(obj) {
1714
+ const billed = numOrNull(obj && obj.costUsd);
1715
+ if (billed != null) return billed;
1716
+ return numOrNull(obj && obj.costUsdEstimated);
1717
+ }
1718
+
1713
1719
  function metricsFilePath(projectDir, changeName) {
1714
1720
  return join(projectDir, 'openspec', 'changes', changeName, 'metrics.json');
1715
1721
  }
1716
1722
 
1717
1723
  function emptySpendTotals() {
1718
- return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
1724
+ return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null, costUsdTotal: null };
1719
1725
  }
1720
1726
 
1721
1727
  function emptyPlatformSpend(source = 'none') {
@@ -1726,6 +1732,7 @@ function emptyPlatformSpend(source = 'none') {
1726
1732
  costUsd: null,
1727
1733
  ampCredits: null,
1728
1734
  costUsdEstimated: null,
1735
+ costUsdTotal: null,
1729
1736
  source,
1730
1737
  };
1731
1738
  }
@@ -2549,6 +2556,7 @@ function spendTuple(obj) {
2549
2556
  costUsd: numOrNull(obj && obj.costUsd),
2550
2557
  ampCredits: numOrNull(obj && obj.ampCredits),
2551
2558
  costUsdEstimated: numOrNull(obj && obj.costUsdEstimated),
2559
+ costUsdTotal: numOrNull(obj && obj.costUsdTotal) ?? costUsdTotalOf(obj),
2552
2560
  };
2553
2561
  }
2554
2562
 
@@ -2568,6 +2576,7 @@ function addSpendNums(target, nums) {
2568
2576
  target.costUsd = addNullable(target.costUsd, nums.costUsd);
2569
2577
  target.ampCredits = addNullable(target.ampCredits, nums.ampCredits);
2570
2578
  target.costUsdEstimated = addNullable(target.costUsdEstimated, nums.costUsdEstimated);
2579
+ target.costUsdTotal = addNullable(target.costUsdTotal, nums.costUsdTotal);
2571
2580
  }
2572
2581
 
2573
2582
  function recomputeSpendMaps(metrics) {
@@ -2586,6 +2595,7 @@ function recomputeSpendMaps(metrics) {
2586
2595
  costUsd: null,
2587
2596
  ampCredits: null,
2588
2597
  costUsdEstimated: null,
2598
+ costUsdTotal: null,
2589
2599
  };
2590
2600
  addSpendNums(row, nums);
2591
2601
  byModel.set(key, row);
@@ -2606,10 +2616,14 @@ function recomputeSpendMaps(metrics) {
2606
2616
  }
2607
2617
  }
2608
2618
  for (const key of Object.keys(byPlatform)) {
2619
+ byPlatform[key].costUsd = roundUsd4(byPlatform[key].costUsd);
2609
2620
  byPlatform[key].costUsdEstimated = roundUsd4(byPlatform[key].costUsdEstimated);
2621
+ byPlatform[key].costUsdTotal = roundUsd4(byPlatform[key].costUsdTotal);
2610
2622
  }
2611
2623
  for (const row of byModel.values()) {
2624
+ row.costUsd = roundUsd4(row.costUsd);
2612
2625
  row.costUsdEstimated = roundUsd4(row.costUsdEstimated);
2626
+ row.costUsdTotal = roundUsd4(row.costUsdTotal);
2613
2627
  }
2614
2628
  metrics.spendByPlatform = byPlatform;
2615
2629
  metrics.spendByModel = [...byModel.values()];
@@ -2661,6 +2675,13 @@ function recomputeMetricsAggregates(metrics) {
2661
2675
  phase[spendKey] = addNullable(phase[spendKey], value);
2662
2676
  spend[spendKey] = addNullable(spend[spendKey], value);
2663
2677
  }
2678
+ const sessionCostUsdTotal = costUsdTotalOf({
2679
+ costUsd: sessionFieldOrSources(session, 'costUsd'),
2680
+ costUsdEstimated: sessionFieldOrSources(session, 'costUsdEstimated'),
2681
+ });
2682
+ session.costUsdTotal = roundUsd4(sessionCostUsdTotal);
2683
+ phase.costUsdTotal = addNullable(phase.costUsdTotal, sessionCostUsdTotal);
2684
+ spend.costUsdTotal = addNullable(spend.costUsdTotal, sessionCostUsdTotal);
2664
2685
  if (session.role && !phase.agents.includes(session.role)) phase.agents.push(session.role);
2665
2686
  if (session.model && !phase.models.includes(session.model)) phase.models.push(session.model);
2666
2687
  if (Array.isArray(session.models)) {
@@ -2679,9 +2700,13 @@ function recomputeMetricsAggregates(metrics) {
2679
2700
  phase.leadTimeMs = Number.isFinite(startMs) && Number.isFinite(endMs)
2680
2701
  ? Math.max(0, endMs - startMs)
2681
2702
  : null;
2703
+ phase.costUsd = roundUsd4(phase.costUsd);
2682
2704
  phase.costUsdEstimated = roundUsd4(phase.costUsdEstimated);
2705
+ phase.costUsdTotal = roundUsd4(phase.costUsdTotal);
2683
2706
  }
2707
+ spend.costUsd = roundUsd4(spend.costUsd);
2684
2708
  spend.costUsdEstimated = roundUsd4(spend.costUsdEstimated);
2709
+ spend.costUsdTotal = roundUsd4(spend.costUsdTotal);
2685
2710
  metrics.phases = phases;
2686
2711
  metrics.totals = totals;
2687
2712
  metrics.spend = spend;
@@ -2908,7 +2933,8 @@ function formatMetricsCostLine(spend) {
2908
2933
  const estimated = spend && spend.costUsdEstimated;
2909
2934
  if (billed == null && estimated == null) return '—';
2910
2935
  if (billed != null && estimated != null) {
2911
- return `${formatMetricsCost(billed)} billed + ~${formatMetricsCost(estimated)} est.`;
2936
+ const total = spend.costUsdTotal != null ? spend.costUsdTotal : billed + estimated;
2937
+ return `${formatMetricsCost(total)} (${formatMetricsCost(billed)} billed + ~${formatMetricsCost(estimated)} est.)`;
2912
2938
  }
2913
2939
  if (billed != null) return formatMetricsCost(billed);
2914
2940
  return `~${formatMetricsCost(estimated)} est.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-orchestrator-kit",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, conductor subagents, durable session handoff, factory gates and MCP setup, cloud-agent handoff, and optional local Figma PAT setup",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -23,6 +23,12 @@ function roundUsd4(x) {
23
23
  return Math.round(Number(x) * 10000) / 10000;
24
24
  }
25
25
 
26
+ function costUsdTotalOf(obj) {
27
+ const billed = numOrNull(obj && obj.costUsd);
28
+ if (billed != null) return billed;
29
+ return numOrNull(obj && obj.costUsdEstimated);
30
+ }
31
+
26
32
  function timestampMs(value) {
27
33
  if (value == null || value === '') return NaN;
28
34
  const ms = Date.parse(value);
@@ -435,6 +441,7 @@ function emptyPlatform(source = 'none') {
435
441
  costUsd: null,
436
442
  ampCredits: null,
437
443
  costUsdEstimated: null,
444
+ costUsdTotal: null,
438
445
  source,
439
446
  };
440
447
  }
@@ -442,7 +449,7 @@ function emptyPlatform(source = 'none') {
442
449
  function recompute(metrics) {
443
450
  const phases = {};
444
451
  const totals = { sessions: 0, durationMs: null, leadTimeMs: null, cloudSessions: 0 };
445
- const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
452
+ const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null, costUsdTotal: null };
446
453
  const byPlatform = {
447
454
  cursor: emptyPlatform(),
448
455
  claude: emptyPlatform(),
@@ -477,6 +484,7 @@ function recompute(metrics) {
477
484
  totalTokens: null,
478
485
  costUsd: null,
479
486
  costUsdEstimated: null,
487
+ costUsdTotal: null,
480
488
  agents: [],
481
489
  models: [],
482
490
  };
@@ -493,6 +501,10 @@ function recompute(metrics) {
493
501
  phase[spendKey] = addNullable(phase[spendKey], value);
494
502
  spend[spendKey] = addNullable(spend[spendKey], value);
495
503
  }
504
+ const sessionCostUsdTotal = costUsdTotalOf(session);
505
+ session.costUsdTotal = roundUsd4(sessionCostUsdTotal);
506
+ phase.costUsdTotal = addNullable(phase.costUsdTotal, sessionCostUsdTotal);
507
+ spend.costUsdTotal = addNullable(spend.costUsdTotal, sessionCostUsdTotal);
496
508
  if (session.role && !phase.agents.includes(session.role)) phase.agents.push(session.role);
497
509
  if (session.model && !phase.models.includes(session.model)) phase.models.push(session.model);
498
510
  if (Array.isArray(session.models)) {
@@ -508,6 +520,7 @@ function recompute(metrics) {
508
520
  for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'ampCredits', 'costUsdEstimated']) {
509
521
  bucket[key] = addNullable(bucket[key], numOrNull(session[key]));
510
522
  }
523
+ bucket.costUsdTotal = addNullable(bucket.costUsdTotal, sessionCostUsdTotal);
511
524
  if ((session.sourceIds || []).length) bucket.source = platform === 'cursor' ? 'cursor-hook' : `${platform}-jsonl`;
512
525
  }
513
526
  const modelRows = (session.byModel || []).length ? session.byModel : [session];
@@ -523,6 +536,7 @@ function recompute(metrics) {
523
536
  costUsd: null,
524
537
  ampCredits: null,
525
538
  costUsdEstimated: null,
539
+ costUsdTotal: null,
526
540
  };
527
541
  row.inputTokens = addNullable(row.inputTokens, numOrNull(src.inputTokens));
528
542
  row.outputTokens = addNullable(row.outputTokens, numOrNull(src.outputTokens));
@@ -530,6 +544,7 @@ function recompute(metrics) {
530
544
  row.costUsd = addNullable(row.costUsd, numOrNull(src.costUsd));
531
545
  row.ampCredits = addNullable(row.ampCredits, numOrNull(src.ampCredits));
532
546
  row.costUsdEstimated = addNullable(row.costUsdEstimated, numOrNull(src.costUsdEstimated));
547
+ row.costUsdTotal = addNullable(row.costUsdTotal, costUsdTotalOf(src));
533
548
  byModel.set(modelKey, row);
534
549
  }
535
550
  }
@@ -544,14 +559,22 @@ function recompute(metrics) {
544
559
  phase.leadTimeMs = Number.isFinite(startMs) && Number.isFinite(endMs)
545
560
  ? Math.max(0, endMs - startMs)
546
561
  : null;
562
+ phase.costUsd = roundUsd4(phase.costUsd);
547
563
  phase.costUsdEstimated = roundUsd4(phase.costUsdEstimated);
564
+ phase.costUsdTotal = roundUsd4(phase.costUsdTotal);
548
565
  }
566
+ spend.costUsd = roundUsd4(spend.costUsd);
549
567
  spend.costUsdEstimated = roundUsd4(spend.costUsdEstimated);
568
+ spend.costUsdTotal = roundUsd4(spend.costUsdTotal);
550
569
  for (const bucket of Object.values(byPlatform)) {
570
+ bucket.costUsd = roundUsd4(bucket.costUsd);
551
571
  bucket.costUsdEstimated = roundUsd4(bucket.costUsdEstimated);
572
+ bucket.costUsdTotal = roundUsd4(bucket.costUsdTotal);
552
573
  }
553
574
  for (const row of byModel.values()) {
575
+ row.costUsd = roundUsd4(row.costUsd);
554
576
  row.costUsdEstimated = roundUsd4(row.costUsdEstimated);
577
+ row.costUsdTotal = roundUsd4(row.costUsdTotal);
555
578
  }
556
579
  metrics.phases = phases;
557
580
  metrics.totals = totals;