agent-orchestrator-kit 0.13.0 → 0.14.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 +8 -0
- package/README.md +16 -3
- package/bin/agent-orchestrator.js +335 -115
- package/bin/claude-cost-estimate.js +51 -0
- package/bin/spend-collect.js +76 -13
- package/package.json +1 -1
- package/templates/.agents/commands/opsx-propose.md +6 -0
- package/templates/.agents/commands/opsx-review.md +35 -11
- package/templates/.agents/rules/session-handoff.mdc +2 -2
- package/templates/.agents/skills/agent-orchestration/SKILL.md +5 -4
- package/templates/.agents/skills/openspec-propose/SKILL.md +6 -0
- package/templates/.agents/subagents/openspec-guide.md +3 -2
- package/templates/.agents/subagents/session-handoff.md +2 -1
- package/templates/.agents/subagents/spec-architect.md +6 -1
- package/templates/.agents/subagents/spec-archiver.md +2 -1
- package/templates/.agents/subagents/spec-reviewer.md +5 -1
- package/templates/AGENTS.md +1 -1
- package/templates/scripts/cursor-spend-collect.cjs +129 -67
|
@@ -303,11 +303,13 @@ function dedupeCursorSources(sources) {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
function leftoverWindowEnd(metrics, last) {
|
|
306
|
-
if (metrics.pending && metrics.pending.startedAt) return metrics.pending.startedAt;
|
|
307
306
|
if (!last || !last.endedAt) return null;
|
|
308
307
|
const end = Date.parse(last.endedAt);
|
|
309
308
|
if (!Number.isFinite(end)) return null;
|
|
310
|
-
|
|
309
|
+
const grace = new Date(end + CURSOR_LEFTOVER_GRACE_MS).toISOString();
|
|
310
|
+
const pending = metrics.pending && metrics.pending.startedAt;
|
|
311
|
+
if (!pending || last.threadId) return pending || grace;
|
|
312
|
+
return Date.parse(pending) < Date.parse(grace) ? pending : grace;
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
function leftoverEndExclusive(metrics) {
|
|
@@ -317,9 +319,7 @@ function leftoverEndExclusive(metrics) {
|
|
|
317
319
|
function existingIds(metrics) {
|
|
318
320
|
const ids = new Set();
|
|
319
321
|
for (const session of metrics.sessions || []) {
|
|
320
|
-
for (const
|
|
321
|
-
if (src && src.id != null && src.id !== '') ids.add(String(src.id));
|
|
322
|
-
}
|
|
322
|
+
for (const id of session.sourceIds || []) ids.add(String(id));
|
|
323
323
|
}
|
|
324
324
|
return ids;
|
|
325
325
|
}
|
|
@@ -417,7 +417,7 @@ function sourceTotals(sources) {
|
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
function looksOverridden(session) {
|
|
420
|
-
const fromSources = sourceTotals(session.
|
|
420
|
+
const fromSources = sourceTotals(session.byModel || []);
|
|
421
421
|
return ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd'].some((key) => {
|
|
422
422
|
const sessionVal = numOrNull(session[key]);
|
|
423
423
|
const sourceVal = numOrNull(fromSources[key]);
|
|
@@ -489,13 +489,7 @@ function recompute(metrics) {
|
|
|
489
489
|
phase.endedAt = session.endedAt;
|
|
490
490
|
}
|
|
491
491
|
for (const spendKey of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
492
|
-
const
|
|
493
|
-
let value = fromSession;
|
|
494
|
-
if (value == null) {
|
|
495
|
-
let sum = null;
|
|
496
|
-
for (const src of session.sources || []) sum = addNullable(sum, numOrNull(src[spendKey]));
|
|
497
|
-
value = sum;
|
|
498
|
-
}
|
|
492
|
+
const value = numOrNull(session[spendKey]);
|
|
499
493
|
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
500
494
|
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
501
495
|
}
|
|
@@ -508,27 +502,21 @@ function recompute(metrics) {
|
|
|
508
502
|
}
|
|
509
503
|
phases[key] = phase;
|
|
510
504
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
const bucket = byPlatform[platform];
|
|
517
|
-
bucket.inputTokens = addNullable(bucket.inputTokens, numOrNull(src.inputTokens));
|
|
518
|
-
bucket.outputTokens = addNullable(bucket.outputTokens, numOrNull(src.outputTokens));
|
|
519
|
-
bucket.totalTokens = addNullable(bucket.totalTokens, numOrNull(src.totalTokens));
|
|
520
|
-
bucket.costUsd = addNullable(bucket.costUsd, numOrNull(src.costUsd));
|
|
521
|
-
bucket.ampCredits = addNullable(bucket.ampCredits, numOrNull(src.ampCredits));
|
|
522
|
-
bucket.costUsdEstimated = addNullable(bucket.costUsdEstimated, numOrNull(src.costUsdEstimated));
|
|
523
|
-
if (platform === 'claude') bucket.source = 'claude-jsonl';
|
|
524
|
-
else if (platform === 'amp') bucket.source = 'amp-thread';
|
|
525
|
-
else if (platform === 'cursor') bucket.source = 'cursor-hook';
|
|
505
|
+
const platform = session.platform;
|
|
506
|
+
if (platform && byPlatform[platform]) {
|
|
507
|
+
const bucket = byPlatform[platform];
|
|
508
|
+
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'ampCredits', 'costUsdEstimated']) {
|
|
509
|
+
bucket[key] = addNullable(bucket[key], numOrNull(session[key]));
|
|
526
510
|
}
|
|
511
|
+
if ((session.sourceIds || []).length) bucket.source = platform === 'cursor' ? 'cursor-hook' : `${platform}-jsonl`;
|
|
512
|
+
}
|
|
513
|
+
const modelRows = (session.byModel || []).length ? session.byModel : [session];
|
|
514
|
+
for (const src of modelRows) {
|
|
527
515
|
if (src.model) {
|
|
528
|
-
const modelKey = `${src.model}::${src.platform || ''}`;
|
|
516
|
+
const modelKey = `${src.model}::${src.platform || session.platform || ''}`;
|
|
529
517
|
const row = byModel.get(modelKey) || {
|
|
530
518
|
model: src.model,
|
|
531
|
-
platform: src.platform || null,
|
|
519
|
+
platform: src.platform || session.platform || null,
|
|
532
520
|
inputTokens: null,
|
|
533
521
|
outputTokens: null,
|
|
534
522
|
totalTokens: null,
|
|
@@ -545,11 +533,6 @@ function recompute(metrics) {
|
|
|
545
533
|
byModel.set(modelKey, row);
|
|
546
534
|
}
|
|
547
535
|
}
|
|
548
|
-
const sessionCostUsd = numOrNull(session.costUsd);
|
|
549
|
-
if (sessionSourceCostUsd == null && sessionCostUsd != null) {
|
|
550
|
-
const billedBucket = session.platform && byPlatform[session.platform];
|
|
551
|
-
if (billedBucket) billedBucket.costUsd = addNullable(billedBucket.costUsd, sessionCostUsd);
|
|
552
|
-
}
|
|
553
536
|
}
|
|
554
537
|
|
|
555
538
|
if (firstStart && lastEnd) {
|
|
@@ -583,7 +566,6 @@ function incomingCursorSources(cwd, existing, fingerprints, windowStart, windowE
|
|
|
583
566
|
const filterId = String(filterConversationId || '').trim();
|
|
584
567
|
const bestById = new Map();
|
|
585
568
|
for (const [id, row] of byId) {
|
|
586
|
-
if (existing.has(id)) continue;
|
|
587
569
|
if (filterId) {
|
|
588
570
|
const rowConversationId = row.conversationId == null || row.conversationId === ''
|
|
589
571
|
? ''
|
|
@@ -631,14 +613,7 @@ function incomingCursorSources(cwd, existing, fingerprints, windowStart, windowE
|
|
|
631
613
|
}
|
|
632
614
|
|
|
633
615
|
function existingFingerprints(metrics) {
|
|
634
|
-
|
|
635
|
-
for (const session of metrics.sessions || []) {
|
|
636
|
-
for (const src of session.sources || []) {
|
|
637
|
-
const fp = cursorSpendFingerprint(src);
|
|
638
|
-
if (fp) set.add(fp);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return set;
|
|
616
|
+
return new Set();
|
|
642
617
|
}
|
|
643
618
|
|
|
644
619
|
function sessionHasSpendNumbers(session) {
|
|
@@ -661,7 +636,7 @@ function sessionSpendFrozen(session) {
|
|
|
661
636
|
|
|
662
637
|
function syncAdapterSessionTotals(session) {
|
|
663
638
|
if (sessionSpendFrozen(session)) return false;
|
|
664
|
-
const totals = sourceTotals(session.
|
|
639
|
+
const totals = sourceTotals(session.byModel || []);
|
|
665
640
|
let changed = false;
|
|
666
641
|
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
667
642
|
if (session[key] !== totals[key]) {
|
|
@@ -669,7 +644,7 @@ function syncAdapterSessionTotals(session) {
|
|
|
669
644
|
changed = true;
|
|
670
645
|
}
|
|
671
646
|
}
|
|
672
|
-
if ((session.
|
|
647
|
+
if ((session.sourceIds || []).length > 0 && session.spendSource !== 'adapter') {
|
|
673
648
|
session.spendSource = 'adapter';
|
|
674
649
|
changed = true;
|
|
675
650
|
}
|
|
@@ -677,21 +652,12 @@ function syncAdapterSessionTotals(session) {
|
|
|
677
652
|
}
|
|
678
653
|
|
|
679
654
|
function enrichMetrics(metrics, cwd) {
|
|
680
|
-
const byId = loadCursorUsageById(cwd);
|
|
681
655
|
let changed = false;
|
|
682
656
|
for (const session of metrics.sessions || []) {
|
|
683
|
-
const deduped = dedupeCursorSources(session.sources || []);
|
|
684
|
-
if (deduped.length !== (session.sources || []).length) {
|
|
685
|
-
session.sources = deduped;
|
|
686
|
-
changed = true;
|
|
687
|
-
} else {
|
|
688
|
-
session.sources = deduped;
|
|
689
|
-
}
|
|
690
|
-
if (attachCursorEstimates(session.sources || [], byId)) changed = true;
|
|
691
657
|
if (syncAdapterSessionTotals(session)) changed = true;
|
|
692
658
|
if (
|
|
693
659
|
session.spendSource === 'unreported'
|
|
694
|
-
&& (session.inputTokens != null || session.totalTokens != null || (session.
|
|
660
|
+
&& (session.inputTokens != null || session.totalTokens != null || (session.sourceIds || []).length)
|
|
695
661
|
) {
|
|
696
662
|
session.spendSource = 'adapter';
|
|
697
663
|
changed = true;
|
|
@@ -700,6 +666,101 @@ function enrichMetrics(metrics, cwd) {
|
|
|
700
666
|
return changed;
|
|
701
667
|
}
|
|
702
668
|
|
|
669
|
+
function compactModelRows(sources) {
|
|
670
|
+
const rows = new Map();
|
|
671
|
+
for (const source of sources || []) {
|
|
672
|
+
if (!source) continue;
|
|
673
|
+
const model = source.model == null || source.model === '' ? null : String(source.model);
|
|
674
|
+
const key = `${source.platform || ''}::${model || ''}`;
|
|
675
|
+
const row = rows.get(key) || {
|
|
676
|
+
model,
|
|
677
|
+
platform: source.platform || null,
|
|
678
|
+
inputTokens: null,
|
|
679
|
+
outputTokens: null,
|
|
680
|
+
totalTokens: null,
|
|
681
|
+
costUsd: null,
|
|
682
|
+
costUsdEstimated: null,
|
|
683
|
+
};
|
|
684
|
+
for (const field of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd']) {
|
|
685
|
+
row[field] = addNullable(row[field], numOrNull(source[field]));
|
|
686
|
+
}
|
|
687
|
+
row.costUsdEstimated = roundUsd4(addNullable(row.costUsdEstimated, numOrNull(source.costUsdEstimated)));
|
|
688
|
+
if (source.costSource) row.costSource = source.costSource;
|
|
689
|
+
rows.set(key, row);
|
|
690
|
+
}
|
|
691
|
+
return [...rows.values()];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function mergeModelRows(current, incoming) {
|
|
695
|
+
return compactModelRows([...(current || []), ...(incoming || [])]);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function normalizeMetricsV2(metrics) {
|
|
699
|
+
metrics.version = 2;
|
|
700
|
+
metrics.sessions = Array.isArray(metrics.sessions) ? metrics.sessions : [];
|
|
701
|
+
for (const session of metrics.sessions) {
|
|
702
|
+
if (Array.isArray(session.sources)) {
|
|
703
|
+
const bestById = new Map();
|
|
704
|
+
for (const source of session.sources) {
|
|
705
|
+
if (!source || source.id == null || source.id === '') continue;
|
|
706
|
+
const id = String(source.id);
|
|
707
|
+
const previous = bestById.get(id);
|
|
708
|
+
if (!previous || (numOrNull(source.totalTokens) ?? 0) >= (numOrNull(previous.totalTokens) ?? 0)) bestById.set(id, source);
|
|
709
|
+
}
|
|
710
|
+
const compactSources = [...bestById.values()].map((source) => ({
|
|
711
|
+
...source,
|
|
712
|
+
model: source.model || session.model || null,
|
|
713
|
+
platform: source.platform || session.platform || null,
|
|
714
|
+
}));
|
|
715
|
+
session.sourceIds = [];
|
|
716
|
+
session.sourceTotals = {};
|
|
717
|
+
for (const source of compactSources) {
|
|
718
|
+
if (!source || source.id == null || source.id === '') continue;
|
|
719
|
+
const id = String(source.id);
|
|
720
|
+
if (!session.sourceIds.includes(id)) session.sourceIds.push(id);
|
|
721
|
+
const total = numOrNull(source.totalTokens);
|
|
722
|
+
if (total != null) session.sourceTotals[id] = Math.max(numOrNull(session.sourceTotals[id]) ?? 0, total);
|
|
723
|
+
}
|
|
724
|
+
session.byModel = compactModelRows(compactSources);
|
|
725
|
+
if (!session.platform && compactSources[0] && compactSources[0].platform) session.platform = compactSources[0].platform;
|
|
726
|
+
if (!session.model && compactSources[0] && compactSources[0].model) session.model = compactSources[0].model;
|
|
727
|
+
delete session.sources;
|
|
728
|
+
} else {
|
|
729
|
+
session.sourceIds = Array.isArray(session.sourceIds) ? [...new Set(session.sourceIds.map(String))] : [];
|
|
730
|
+
session.sourceTotals = session.sourceTotals && typeof session.sourceTotals === 'object' ? session.sourceTotals : {};
|
|
731
|
+
session.byModel = Array.isArray(session.byModel) ? session.byModel : [];
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return metrics;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function mergeIncomingCompact(session, incoming) {
|
|
738
|
+
session.sourceIds = Array.isArray(session.sourceIds) ? session.sourceIds : [];
|
|
739
|
+
session.sourceTotals = session.sourceTotals && typeof session.sourceTotals === 'object' ? session.sourceTotals : {};
|
|
740
|
+
const deltaSources = [];
|
|
741
|
+
for (const source of incoming || []) {
|
|
742
|
+
const id = String(source.id);
|
|
743
|
+
const previous = numOrNull(session.sourceTotals[id]);
|
|
744
|
+
const next = numOrNull(source.totalTokens);
|
|
745
|
+
if (!session.sourceIds.includes(id)) session.sourceIds.push(id);
|
|
746
|
+
if (next != null) session.sourceTotals[id] = Math.max(previous ?? 0, next);
|
|
747
|
+
if (previous == null || next == null) {
|
|
748
|
+
deltaSources.push(source);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const delta = Math.max(0, next - previous);
|
|
752
|
+
if (!delta) continue;
|
|
753
|
+
const ratio = next > 0 ? delta / next : 0;
|
|
754
|
+
const copy = { ...source };
|
|
755
|
+
for (const field of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
756
|
+
const value = numOrNull(copy[field]);
|
|
757
|
+
if (value != null) copy[field] = field.includes('Usd') ? roundUsd4(value * ratio) : Math.round(value * ratio);
|
|
758
|
+
}
|
|
759
|
+
deltaSources.push(copy);
|
|
760
|
+
}
|
|
761
|
+
session.byModel = mergeModelRows(session.byModel, compactModelRows(deltaSources));
|
|
762
|
+
}
|
|
763
|
+
|
|
703
764
|
function backfillMetricsFile(cwd, filePath) {
|
|
704
765
|
if (!existsSync(filePath)) return;
|
|
705
766
|
let metrics;
|
|
@@ -709,18 +770,14 @@ function backfillMetricsFile(cwd, filePath) {
|
|
|
709
770
|
return;
|
|
710
771
|
}
|
|
711
772
|
if (!metrics || typeof metrics !== 'object') return;
|
|
773
|
+
const schemaChanged = metrics.version !== 2 || (metrics.sessions || []).some((session) => Array.isArray(session.sources));
|
|
774
|
+
normalizeMetricsV2(metrics);
|
|
712
775
|
const sessions = Array.isArray(metrics.sessions) ? metrics.sessions : [];
|
|
713
776
|
if (!sessions.length) return;
|
|
714
|
-
let collapsed = false;
|
|
715
|
-
for (const session of sessions) {
|
|
716
|
-
const next = dedupeCursorSources(session.sources || []);
|
|
717
|
-
if (next.length !== (session.sources || []).length) collapsed = true;
|
|
718
|
-
session.sources = next;
|
|
719
|
-
}
|
|
720
777
|
const last = sessions[sessions.length - 1];
|
|
721
778
|
const byId = loadCursorUsageById(cwd);
|
|
722
779
|
const leftoverEnd = leftoverWindowEnd(metrics, last);
|
|
723
|
-
const incoming = last.endedAt && leftoverEnd
|
|
780
|
+
const incoming = (last.endedAt && leftoverEnd
|
|
724
781
|
? incomingCursorSources(
|
|
725
782
|
cwd,
|
|
726
783
|
existingIds(metrics),
|
|
@@ -731,11 +788,16 @@ function backfillMetricsFile(cwd, filePath) {
|
|
|
731
788
|
leftoverEndExclusive(metrics),
|
|
732
789
|
last.threadId,
|
|
733
790
|
)
|
|
734
|
-
: []
|
|
791
|
+
: []).filter((source) => {
|
|
792
|
+
const previous = numOrNull(last.sourceTotals && last.sourceTotals[source.id]);
|
|
793
|
+
return previous == null || (numOrNull(source.totalTokens) ?? 0) > previous;
|
|
794
|
+
});
|
|
735
795
|
if (incoming.length) {
|
|
736
|
-
|
|
796
|
+
mergeIncomingCompact(last, incoming);
|
|
797
|
+
if (!last.platform) last.platform = 'cursor';
|
|
798
|
+
if (!last.model && incoming[0].model) last.model = incoming[0].model;
|
|
737
799
|
if (!sessionSpendFrozen(last)) {
|
|
738
|
-
const totals = sourceTotals(last.
|
|
800
|
+
const totals = sourceTotals(last.byModel);
|
|
739
801
|
last.inputTokens = totals.inputTokens;
|
|
740
802
|
last.outputTokens = totals.outputTokens;
|
|
741
803
|
last.totalTokens = totals.totalTokens;
|
|
@@ -745,7 +807,7 @@ function backfillMetricsFile(cwd, filePath) {
|
|
|
745
807
|
}
|
|
746
808
|
}
|
|
747
809
|
const enriched = enrichMetrics(metrics, cwd);
|
|
748
|
-
if (!incoming.length && !enriched && !
|
|
810
|
+
if (!incoming.length && !enriched && !schemaChanged) return;
|
|
749
811
|
metrics.updatedAt = new Date().toISOString();
|
|
750
812
|
recompute(metrics);
|
|
751
813
|
writeFileSync(filePath, `${JSON.stringify(metrics, null, 2)}\n`);
|