agent-orchestrator-kit 0.7.0 → 0.9.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 +24 -0
- package/README.md +18 -7
- package/bin/agent-orchestrator.js +255 -65
- package/bin/amp-usage.js +63 -0
- package/bin/cursor-cost-estimate.js +50 -0
- package/bin/metrics-time.js +99 -0
- package/bin/session-client.js +187 -0
- package/bin/spend-collect.js +253 -48
- package/package.json +1 -1
- package/templates/.agents/commands/opsx-archive.md +1 -1
- package/templates/.agents/rules/session-handoff.mdc +2 -2
- package/templates/.agents/subagents/spec-archiver.md +1 -1
package/bin/spend-collect.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { join, basename } from 'path';
|
|
3
3
|
import { homedir as osHomedir } from 'os';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
import { listRecentAmpThreadIds } from './session-client.js';
|
|
6
|
+
import { formatUtcIso, parseFlexibleIso } from './metrics-time.js';
|
|
7
|
+
import { estimateCursorCostUsd } from './cursor-cost-estimate.js';
|
|
8
|
+
import { ampAgentMode, matchAmpUsageModel, parseAmpUsageDetails } from './amp-usage.js';
|
|
4
9
|
|
|
5
10
|
const PLATFORMS = ['cursor', 'claude', 'amp'];
|
|
6
11
|
|
|
@@ -22,6 +27,7 @@ function emptyPlatform(source = 'none') {
|
|
|
22
27
|
totalTokens: null,
|
|
23
28
|
costUsd: null,
|
|
24
29
|
ampCredits: null,
|
|
30
|
+
costUsdEstimated: null,
|
|
25
31
|
source,
|
|
26
32
|
};
|
|
27
33
|
}
|
|
@@ -57,11 +63,7 @@ function pathsEqual(a, b) {
|
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
function parseTime(value) {
|
|
60
|
-
|
|
61
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
62
|
-
return value < 1e12 ? value * 1000 : value;
|
|
63
|
-
}
|
|
64
|
-
return Date.parse(String(value));
|
|
66
|
+
return parseFlexibleIso(value);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function inWindow(timestamp, windowStart, windowEnd) {
|
|
@@ -78,12 +80,25 @@ function inWindow(timestamp, windowStart, windowEnd) {
|
|
|
78
80
|
return true;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
function sourceRecord({
|
|
83
|
+
function sourceRecord({
|
|
84
|
+
id,
|
|
85
|
+
platform,
|
|
86
|
+
model,
|
|
87
|
+
inputTokens,
|
|
88
|
+
outputTokens,
|
|
89
|
+
costUsd,
|
|
90
|
+
ampCredits,
|
|
91
|
+
at,
|
|
92
|
+
cacheReadTokens,
|
|
93
|
+
costUsdEstimated,
|
|
94
|
+
costSource,
|
|
95
|
+
agentMode,
|
|
96
|
+
}) {
|
|
82
97
|
const input = numOrNull(inputTokens);
|
|
83
98
|
const output = numOrNull(outputTokens);
|
|
84
99
|
let total = null;
|
|
85
100
|
if (input != null || output != null) total = (input ?? 0) + (output ?? 0);
|
|
86
|
-
|
|
101
|
+
const record = {
|
|
87
102
|
id: String(id),
|
|
88
103
|
platform,
|
|
89
104
|
model: model == null || model === '' ? null : String(model),
|
|
@@ -92,8 +107,15 @@ function sourceRecord({ id, platform, model, inputTokens, outputTokens, costUsd,
|
|
|
92
107
|
totalTokens: total,
|
|
93
108
|
costUsd: numOrNull(costUsd),
|
|
94
109
|
ampCredits: numOrNull(ampCredits),
|
|
95
|
-
at: at == null ? null : String(at),
|
|
110
|
+
at: at == null || at === '' ? null : (formatUtcIso(at) || String(at)),
|
|
96
111
|
};
|
|
112
|
+
const cache = numOrNull(cacheReadTokens);
|
|
113
|
+
if (cache != null) record.cacheReadTokens = cache;
|
|
114
|
+
const estimated = numOrNull(costUsdEstimated);
|
|
115
|
+
if (estimated != null) record.costUsdEstimated = estimated;
|
|
116
|
+
if (costSource) record.costSource = String(costSource);
|
|
117
|
+
if (agentMode) record.agentMode = String(agentMode);
|
|
118
|
+
return record;
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
function claudeInputTokens(usage) {
|
|
@@ -309,6 +331,37 @@ function ampInputTokens(usage) {
|
|
|
309
331
|
return has ? sum : null;
|
|
310
332
|
}
|
|
311
333
|
|
|
334
|
+
export function sourcesFromAmpThread(thread, ctx, fileName = '', via = null) {
|
|
335
|
+
const sources = [];
|
|
336
|
+
if (!thread || typeof thread !== 'object') return sources;
|
|
337
|
+
const { cwd, windowStart, windowEnd, existing, env } = ctx;
|
|
338
|
+
if (!ampThreadMatches(thread, cwd, env, fileName)) return sources;
|
|
339
|
+
const threadKey = thread.id ? String(thread.id) : basename(fileName || 'thread', '.json');
|
|
340
|
+
for (const message of ampMessages(thread)) {
|
|
341
|
+
const usage = ampUsage(message);
|
|
342
|
+
if (!usage) continue;
|
|
343
|
+
const rawId = ampId(message);
|
|
344
|
+
if (rawId == null || rawId === '') continue;
|
|
345
|
+
const id = `${threadKey}:${rawId}`;
|
|
346
|
+
if (existing.has(id)) continue;
|
|
347
|
+
if (!inWindow(usage.timestamp, windowStart, windowEnd)) continue;
|
|
348
|
+
const record = sourceRecord({
|
|
349
|
+
id,
|
|
350
|
+
platform: 'amp',
|
|
351
|
+
model: usage.model,
|
|
352
|
+
inputTokens: ampInputTokens(usage),
|
|
353
|
+
outputTokens: usage.outputTokens,
|
|
354
|
+
costUsd: null,
|
|
355
|
+
ampCredits: null,
|
|
356
|
+
at: usage.timestamp,
|
|
357
|
+
agentMode: ampAgentMode(thread),
|
|
358
|
+
});
|
|
359
|
+
if (via) record.via = via;
|
|
360
|
+
sources.push(record);
|
|
361
|
+
}
|
|
362
|
+
return sources;
|
|
363
|
+
}
|
|
364
|
+
|
|
312
365
|
function collectAmp({ cwd, windowStart, windowEnd, existing, env, homedir, notes }) {
|
|
313
366
|
const root = ampRoot(env, homedir);
|
|
314
367
|
const threadsDir = join(root, 'threads');
|
|
@@ -324,6 +377,7 @@ function collectAmp({ cwd, windowStart, windowEnd, existing, env, homedir, notes
|
|
|
324
377
|
notes.push('amp: cannot read threads');
|
|
325
378
|
return sources;
|
|
326
379
|
}
|
|
380
|
+
const ctx = { cwd, windowStart, windowEnd, existing, env, homedir, notes };
|
|
327
381
|
for (const file of files) {
|
|
328
382
|
let thread;
|
|
329
383
|
try {
|
|
@@ -331,34 +385,120 @@ function collectAmp({ cwd, windowStart, windowEnd, existing, env, homedir, notes
|
|
|
331
385
|
} catch {
|
|
332
386
|
continue;
|
|
333
387
|
}
|
|
334
|
-
|
|
335
|
-
if (!ampThreadMatches(thread, cwd, env, file)) continue;
|
|
336
|
-
// messageId values are thread-local counters (1, 3, 5, ...), so a bare id
|
|
337
|
-
// collides across threads; namespace with the thread id for global dedup.
|
|
338
|
-
const threadKey = thread.id ? String(thread.id) : basename(file, '.json');
|
|
339
|
-
for (const message of ampMessages(thread)) {
|
|
340
|
-
const usage = ampUsage(message);
|
|
341
|
-
if (!usage) continue;
|
|
342
|
-
const rawId = ampId(message);
|
|
343
|
-
if (rawId == null || rawId === '') continue;
|
|
344
|
-
const id = `${threadKey}:${rawId}`;
|
|
345
|
-
if (existing.has(id)) continue;
|
|
346
|
-
if (!inWindow(usage.timestamp, windowStart, windowEnd)) continue;
|
|
347
|
-
sources.push(sourceRecord({
|
|
348
|
-
id,
|
|
349
|
-
platform: 'amp',
|
|
350
|
-
model: usage.model,
|
|
351
|
-
inputTokens: ampInputTokens(usage),
|
|
352
|
-
outputTokens: usage.outputTokens,
|
|
353
|
-
costUsd: null,
|
|
354
|
-
ampCredits: null,
|
|
355
|
-
at: usage.timestamp,
|
|
356
|
-
}));
|
|
357
|
-
}
|
|
388
|
+
sources.push(...sourcesFromAmpThread(thread, ctx, file));
|
|
358
389
|
}
|
|
359
390
|
return sources;
|
|
360
391
|
}
|
|
361
392
|
|
|
393
|
+
export function exportAmpThread(threadId, options = {}) {
|
|
394
|
+
const id = threadId == null ? '' : String(threadId).trim();
|
|
395
|
+
if (!id) return null;
|
|
396
|
+
if (typeof options.exportAmpThread === 'function') {
|
|
397
|
+
try {
|
|
398
|
+
return options.exportAmpThread(id);
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const bin = options.ampBin || (options.env && options.env.AOK_AMP_BIN) || 'amp';
|
|
404
|
+
if (bin !== 'amp' && !existsSync(bin)) return null;
|
|
405
|
+
try {
|
|
406
|
+
const out = execFileSync(bin, ['threads', 'export', id], {
|
|
407
|
+
encoding: 'utf-8',
|
|
408
|
+
timeout: options.timeoutMs != null ? Number(options.timeoutMs) : 15000,
|
|
409
|
+
env: options.env || process.env,
|
|
410
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
411
|
+
});
|
|
412
|
+
const parsed = JSON.parse(out);
|
|
413
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function fetchAmpThreadUsage(threadId, options = {}) {
|
|
420
|
+
const id = threadId == null ? '' : String(threadId).trim();
|
|
421
|
+
if (!id) return null;
|
|
422
|
+
if (typeof options.usageAmpThread === 'function') {
|
|
423
|
+
try {
|
|
424
|
+
const injected = options.usageAmpThread(id);
|
|
425
|
+
if (injected == null) return null;
|
|
426
|
+
if (typeof injected === 'string') return parseAmpUsageDetails(injected);
|
|
427
|
+
if (typeof injected === 'object') {
|
|
428
|
+
if (injected.text && injected.costUsd == null) return { ...parseAmpUsageDetails(injected.text), ...injected };
|
|
429
|
+
return injected;
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (typeof options.exportAmpThread === 'function') return null;
|
|
437
|
+
const bin = options.ampBin || (options.env && options.env.AOK_AMP_BIN) || 'amp';
|
|
438
|
+
if (bin !== 'amp' && !existsSync(bin)) return null;
|
|
439
|
+
try {
|
|
440
|
+
const out = execFileSync(bin, ['threads', 'usage', id, '--details'], {
|
|
441
|
+
encoding: 'utf-8',
|
|
442
|
+
timeout: options.timeoutMs != null ? Number(options.timeoutMs) : 25000,
|
|
443
|
+
env: options.env || process.env,
|
|
444
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
445
|
+
});
|
|
446
|
+
return parseAmpUsageDetails(out);
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function collectAmpCli(ctx) {
|
|
453
|
+
const { env, notes, ampThreadId } = ctx;
|
|
454
|
+
const ids = [];
|
|
455
|
+
const push = (value) => {
|
|
456
|
+
const id = value == null ? '' : String(value).trim();
|
|
457
|
+
if (id && !ids.includes(id)) ids.push(id);
|
|
458
|
+
};
|
|
459
|
+
push(ampThreadId);
|
|
460
|
+
push(ampCurrentThreadId(env));
|
|
461
|
+
if (!ids.length) {
|
|
462
|
+
for (const id of listRecentAmpThreadIds(ctx)) push(id);
|
|
463
|
+
}
|
|
464
|
+
const sources = [];
|
|
465
|
+
const threads = [];
|
|
466
|
+
for (const id of ids) {
|
|
467
|
+
const thread = exportAmpThread(id, ctx);
|
|
468
|
+
if (!thread) {
|
|
469
|
+
notes.push(`amp: export failed for ${id}`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const agentMode = ampAgentMode(thread);
|
|
473
|
+
const extracted = sourcesFromAmpThread(thread, ctx, `${id}.json`, 'amp-cli');
|
|
474
|
+
if (!extracted.length) notes.push(`amp: export ${id} had no matching usage`);
|
|
475
|
+
sources.push(...extracted);
|
|
476
|
+
const usage = fetchAmpThreadUsage(id, ctx);
|
|
477
|
+
if (!usage) notes.push(`amp: usage failed for ${id}`);
|
|
478
|
+
const sourceModels = extracted.map((src) => src.model).filter(Boolean);
|
|
479
|
+
const usageModels = (usage && Array.isArray(usage.models) ? usage.models : []).map((row) => ({
|
|
480
|
+
...row,
|
|
481
|
+
model: matchAmpUsageModel(row.model, sourceModels),
|
|
482
|
+
}));
|
|
483
|
+
if (usage && usage.costUsd != null) {
|
|
484
|
+
for (const src of extracted) {
|
|
485
|
+
src.costSource = 'amp-usage';
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
threads.push({
|
|
489
|
+
id,
|
|
490
|
+
agentMode,
|
|
491
|
+
costUsd: usage ? numOrNull(usage.costUsd) : null,
|
|
492
|
+
inputTokens: usage ? numOrNull(usage.inputTokens) : null,
|
|
493
|
+
outputTokens: usage ? numOrNull(usage.outputTokens) : null,
|
|
494
|
+
totalTokens: usage ? numOrNull(usage.totalTokens) : null,
|
|
495
|
+
cacheReadTokens: usage ? numOrNull(usage.cacheReadTokens) : null,
|
|
496
|
+
models: usageModels,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return { sources, threads };
|
|
500
|
+
}
|
|
501
|
+
|
|
362
502
|
export const CURSOR_USAGE_FILE_REL = join('.agents', 'spend', 'cursor-usage.jsonl');
|
|
363
503
|
|
|
364
504
|
function collectCursor({ cwd, windowStart, windowEnd, existing, notes }) {
|
|
@@ -393,15 +533,26 @@ function collectCursor({ cwd, windowStart, windowEnd, existing, notes }) {
|
|
|
393
533
|
const inputTokens = numOrNull(row.inputTokens);
|
|
394
534
|
const outputTokens = numOrNull(row.outputTokens);
|
|
395
535
|
if (inputTokens == null && outputTokens == null) continue;
|
|
536
|
+
const model = row.model || row.modelId;
|
|
537
|
+
const cacheReadTokens = numOrNull(row.cacheReadTokens);
|
|
538
|
+
const estimated = estimateCursorCostUsd({
|
|
539
|
+
model,
|
|
540
|
+
inputTokens,
|
|
541
|
+
outputTokens,
|
|
542
|
+
cacheReadTokens,
|
|
543
|
+
});
|
|
396
544
|
const record = sourceRecord({
|
|
397
545
|
id,
|
|
398
546
|
platform: 'cursor',
|
|
399
|
-
model
|
|
547
|
+
model,
|
|
400
548
|
inputTokens,
|
|
401
549
|
outputTokens,
|
|
402
550
|
costUsd: null,
|
|
403
551
|
ampCredits: null,
|
|
404
552
|
at: row.at,
|
|
553
|
+
cacheReadTokens,
|
|
554
|
+
costUsdEstimated: estimated,
|
|
555
|
+
costSource: estimated != null ? 'api-estimate' : null,
|
|
405
556
|
});
|
|
406
557
|
const previous = bestById.get(id);
|
|
407
558
|
if (!previous || (record.totalTokens ?? 0) >= (previous.totalTokens ?? 0)) {
|
|
@@ -423,8 +574,9 @@ function aggregate(sources) {
|
|
|
423
574
|
bucket.totalTokens = addNullable(bucket.totalTokens, src.totalTokens);
|
|
424
575
|
bucket.costUsd = addNullable(bucket.costUsd, src.costUsd);
|
|
425
576
|
bucket.ampCredits = addNullable(bucket.ampCredits, src.ampCredits);
|
|
577
|
+
bucket.costUsdEstimated = addNullable(bucket.costUsdEstimated, src.costUsdEstimated);
|
|
426
578
|
if (platform === 'claude') bucket.source = 'claude-jsonl';
|
|
427
|
-
else if (platform === 'amp') bucket.source = 'amp-thread';
|
|
579
|
+
else if (platform === 'amp') bucket.source = src.via === 'amp-cli' ? 'amp-cli' : 'amp-thread';
|
|
428
580
|
else bucket.source = 'cursor-hook';
|
|
429
581
|
}
|
|
430
582
|
const model = src.model;
|
|
@@ -438,12 +590,14 @@ function aggregate(sources) {
|
|
|
438
590
|
totalTokens: null,
|
|
439
591
|
costUsd: null,
|
|
440
592
|
ampCredits: null,
|
|
593
|
+
costUsdEstimated: null,
|
|
441
594
|
};
|
|
442
595
|
row.inputTokens = addNullable(row.inputTokens, src.inputTokens);
|
|
443
596
|
row.outputTokens = addNullable(row.outputTokens, src.outputTokens);
|
|
444
597
|
row.totalTokens = addNullable(row.totalTokens, src.totalTokens);
|
|
445
598
|
row.costUsd = addNullable(row.costUsd, src.costUsd);
|
|
446
599
|
row.ampCredits = addNullable(row.ampCredits, src.ampCredits);
|
|
600
|
+
row.costUsdEstimated = addNullable(row.costUsdEstimated, src.costUsdEstimated);
|
|
447
601
|
byModel.set(key, row);
|
|
448
602
|
}
|
|
449
603
|
}
|
|
@@ -464,23 +618,74 @@ export function collectSpend(options = {}) {
|
|
|
464
618
|
const windowStart = options.windowStart;
|
|
465
619
|
const windowEnd = options.windowEnd;
|
|
466
620
|
const notes = [];
|
|
467
|
-
const
|
|
621
|
+
const wanted = Array.isArray(options.platforms)
|
|
622
|
+
? options.platforms.filter((name) => PLATFORMS.includes(name))
|
|
623
|
+
: PLATFORMS;
|
|
624
|
+
const run = (name) => !wanted.length || wanted.includes(name);
|
|
625
|
+
const ctx = {
|
|
626
|
+
cwd,
|
|
627
|
+
windowStart,
|
|
628
|
+
windowEnd,
|
|
629
|
+
existing,
|
|
630
|
+
env,
|
|
631
|
+
homedir,
|
|
632
|
+
notes,
|
|
633
|
+
ampThreadId: options.ampThreadId,
|
|
634
|
+
exportAmpThread: options.exportAmpThread,
|
|
635
|
+
listAmpThreads: options.listAmpThreads,
|
|
636
|
+
usageAmpThread: options.usageAmpThread,
|
|
637
|
+
ampBin: options.ampBin,
|
|
638
|
+
timeoutMs: options.timeoutMs,
|
|
639
|
+
};
|
|
468
640
|
let sources = [];
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
641
|
+
const ampThreads = [];
|
|
642
|
+
if (run('claude')) {
|
|
643
|
+
try {
|
|
644
|
+
sources = sources.concat(collectClaude(ctx));
|
|
645
|
+
} catch {
|
|
646
|
+
notes.push('claude: adapter failed');
|
|
647
|
+
}
|
|
473
648
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
649
|
+
if (run('amp')) {
|
|
650
|
+
if (options.ampCli === true || typeof options.exportAmpThread === 'function') {
|
|
651
|
+
try {
|
|
652
|
+
const cli = collectAmpCli(ctx);
|
|
653
|
+
sources = sources.concat(cli.sources || []);
|
|
654
|
+
if (Array.isArray(cli.threads)) ampThreads.push(...cli.threads);
|
|
655
|
+
} catch {
|
|
656
|
+
notes.push('amp: cli export failed');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
sources = sources.concat(collectAmp(ctx));
|
|
661
|
+
} catch {
|
|
662
|
+
notes.push('amp: adapter failed');
|
|
663
|
+
}
|
|
478
664
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
665
|
+
if (run('cursor')) {
|
|
666
|
+
try {
|
|
667
|
+
sources = sources.concat(collectCursor(ctx));
|
|
668
|
+
} catch {
|
|
669
|
+
notes.push('cursor: adapter failed');
|
|
670
|
+
}
|
|
483
671
|
}
|
|
484
672
|
const { byPlatform, byModel } = aggregate(sources);
|
|
485
|
-
|
|
673
|
+
applyAmpThreadSpend(byPlatform, byModel, ampThreads);
|
|
674
|
+
return { sources, byPlatform, byModel, notes, ampThreads };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function applyAmpThreadSpend(byPlatform, byModel, threads) {
|
|
678
|
+
if (!threads || !threads.length) return;
|
|
679
|
+
let cost = null;
|
|
680
|
+
for (const thread of threads) {
|
|
681
|
+
cost = addNullable(cost, numOrNull(thread.costUsd));
|
|
682
|
+
for (const row of thread.models || []) {
|
|
683
|
+
if (!row.model) continue;
|
|
684
|
+
const key = `${row.model}::amp`;
|
|
685
|
+
const existing = byModel.find((item) => `${item.model}::${item.platform || ''}` === key)
|
|
686
|
+
|| byModel.find((item) => item.platform === 'amp' && item.model === row.model);
|
|
687
|
+
if (existing && row.costUsd != null) existing.costUsd = addNullable(existing.costUsd, row.costUsd);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (cost != null) byPlatform.amp.costUsd = addNullable(byPlatform.amp.costUsd, cost);
|
|
486
691
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -21,7 +21,7 @@ Archive is one CLI call, no phase subagents.
|
|
|
21
21
|
npx agent-orchestrator-kit archive <name> [--sync | --no-sync --force]
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Gates, optional `--sync`, move to `archive/YYYY-MM-DD-<name>`, validate+rollback, final `handoff.md` (`next_command: none`) + memory. A successful `archive` always creates or updates `metrics.json` (`archivedAt`, Archiver session) and prints the change-wide metrics summary.
|
|
24
|
+
Gates, optional `--sync`, move to `archive/YYYY-MM-DD-<name>`, validate+rollback, final `handoff.md` (`next_command: none`) + memory. A successful `archive` always creates or updates `metrics.json` (`archivedAt`, Archiver session), collects the locked client into that session (Cursor hook / Amp usage / Claude JSONL; `--collect` = all adapters), and prints the change-wide metrics summary. If `spend.costUsd` is `null` — stderr warning, not a gate.
|
|
25
25
|
|
|
26
26
|
4. **Show stdout as-is.** On exit ≠ 0, report the gate from stderr and stop — no manual merge/move.
|
|
27
27
|
|
|
@@ -12,7 +12,7 @@ Agents (local or cloud) write session artifacts only to git-tracked paths — ne
|
|
|
12
12
|
## Session Start (before any work)
|
|
13
13
|
1. Honor pasted `/opsx:<phase> <name>` and announce the role.
|
|
14
14
|
2. `npx agent-orchestrator-kit status`
|
|
15
|
-
3. `npx agent-orchestrator-kit handoff --restore` (or `handoff <name> --restore`). The CLI briefing is canonical — it already reads memory.json and handoff.md; accumulated decisions print from git-tracked `openspec/changes/<name>/decisions.md`, not from Memory. No separate Memory MCP read step.
|
|
15
|
+
3. `npx agent-orchestrator-kit handoff --restore` (or `handoff <name> --restore`). The CLI briefing is canonical — it already reads memory.json and handoff.md; accumulated decisions print from git-tracked `openspec/changes/<name>/decisions.md`, not from Memory. No separate Memory MCP read step. Restore also locks the session client (`cursor` / `claude` / `amp`) into `metrics.json` `pending` — persist will follow that client’s spend flow. Override with `--platform` when detection is wrong.
|
|
16
16
|
4. If the restore CLI failed → read `openspec/changes/<name>/handoff.md` directly.
|
|
17
17
|
5. Spawn `session-handoff` in restore mode ONLY if both the CLI and handoff.md are unavailable (Amp: isolated `subagent-session-handoff`). This is a fallback, never a routine step.
|
|
18
18
|
6. Free-form continue/next/«далі» with one active change → execute `Handoff.next_command`.
|
|
@@ -21,7 +21,7 @@ Agents (local or cloud) write session artifacts only to git-tracked paths — ne
|
|
|
21
21
|
## Session Exit (order)
|
|
22
22
|
1. The parent writes `openspec/changes/<name>/handoff.md` itself: Closed role, Change, Done, Decisions, Blocked, Next command, Next role, Attach, Subagents to spawn, Constraints, Runtime, Metrics.
|
|
23
23
|
2. Fill `## Metrics` before running persist. Required keys: `platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`. Use `unknown` when a value is missing — never invent `0`. This self-report is the primary spend source; `metrics.json` records what the CLI resolved.
|
|
24
|
-
3. `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` — require exit 0 (appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md`, upserts absolute-path Memory JSON, records the session into `openspec/changes/<name>/metrics.json`, prints the expanded prompt on stdout). `--model` is the LLM product id of this chat (`claude-opus-5`, `claude-fable-5`, `gpt-5.6-sol`, `cursor-grok-4.6`) — NEVER pass a Closed role
|
|
24
|
+
3. `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` — require exit 0 (appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md`, upserts absolute-path Memory JSON, records the session into `openspec/changes/<name>/metrics.json`, prints the expanded prompt on stdout). `--model` is the LLM product id of this chat (`claude-opus-5`, `claude-fable-5`, `gpt-5.6-sol`, `cursor-grok-4.6`, `accounts/fireworks/models/glm-5p2`) — NEVER pass a Closed role, a subagent name, or an Amp **mode** (`low`, `medium`, `high`, `ultra`) as `--model`. The parent SHOULD still pass `--model`. The parent MUST NOT guess tokens. Persist collects spend for the client locked at restore (Amp: `amp threads export` + local threads; Cursor: hook file; Claude: JSONL). `--input-tokens` / `--output-tokens` / `--total-tokens` / `--cost-usd` override session-level totals only and do not wipe platform maps; they do not rewrite `## Metrics`. Optional `--platform cursor|claude|amp` or `AOK_PLATFORM`. Optional `--collect` runs all three adapters, not only the locked client. The same `npx agent-orchestrator-kit handoff <name>` works in Cursor, Claude Code, and Amp. `decisions.md` is the git canon of change decisions; Memory `Decision:*` is a file→Memory mirror only. Cloud sessions pass `--runtime cloud` (or set `AOK_RUNTIME=cloud` / `AOK_AGENT_ID` in the cloud-agent environment).
|
|
25
25
|
4. Spawn `session-handoff` in persist mode ONLY if step 3 failed (Amp: isolated `subagent-session-handoff`). Fallback, never routine.
|
|
26
26
|
5. Memory MCP is an optional mirror: if tools are available, update `Change:<name>`, `Handoff:<name>`, `Decision:*` in one call; unavailability never blocks closing.
|
|
27
27
|
6. Paste CLI stdout as one fenced block. First line `/opsx:…`. Body uses `project.agent_language`. Self-contained (Done/Decisions/Blocked/spawn/HARD STOP). No banner.
|
|
@@ -9,7 +9,7 @@ Workflow:
|
|
|
9
9
|
|
|
10
10
|
1. Read `.agents/orchestrator.yaml`, the complete change, review verdict, task state, and verification/merge evidence supplied by the conductor.
|
|
11
11
|
2. Refuse to archive unless required review is approved, all tasks are complete, and the configured merge/CI gate is satisfied.
|
|
12
|
-
3. Fill `## Metrics` in the change `handoff.md`
|
|
12
|
+
3. Fill `## Metrics` in the change `handoff.md` only when reporting Archiver-specific numbers (use `unknown` when missing). Do not copy the previous apply session. The CLI auto-collects the locked client into the Archiver session.
|
|
13
13
|
4. Run `npx agent-orchestrator-kit archive <name>` so delta requirements are merged into main specs, the change moves to the dated archive path, and stdout prints the change-wide metrics summary (by phase / by platform / by model).
|
|
14
14
|
5. Run strict validation after the move and report the resulting archive path and modified main specs.
|
|
15
15
|
|