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.
@@ -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
- if (value == null || value === '') return NaN;
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({ id, platform, model, inputTokens, outputTokens, costUsd, ampCredits, at }) {
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
- return {
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
- if (!thread || typeof thread !== 'object') continue;
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: row.model || row.modelId,
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 ctx = { cwd, windowStart, windowEnd, existing, env, homedir, notes };
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
- try {
470
- sources = sources.concat(collectClaude(ctx));
471
- } catch {
472
- notes.push('claude: adapter failed');
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
- try {
475
- sources = sources.concat(collectAmp(ctx));
476
- } catch {
477
- notes.push('amp: adapter failed');
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
- try {
480
- sources = sources.concat(collectCursor(ctx));
481
- } catch {
482
- notes.push('cursor: adapter failed');
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
- return { sources, byPlatform, byModel, notes };
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.7.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. Collect runs only with `--collect`; if `spend.costUsd` is `null` — stderr warning, not a gate.
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 (`Architect`, `Implementer`, `Explorer`) or a subagent name (`spec-architect`, `session-handoff`) as `--model`. The parent SHOULD still pass `--model`. The parent MUST NOT guess tokens. `--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` also runs local spend adapters (Claude JSONL, Amp threads, Cursor spend hook file). The same `npx agent-orchestrator-kit handoff <name>` works in Cursor, Claude Code, and Amp and MUST NOT require Cursor SDK, a Claude `/cost` parser, or an Amp billing API as a required step. `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).
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` (Archiver self-report: platform, model, tokens, cost_usd, amp_credits, spend_source; use `unknown` when missing) before running archive.
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