agent-orchestrator-kit 0.10.0 → 0.11.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.
@@ -20,6 +20,53 @@ function addNullable(a, b) {
20
20
  return (a ?? 0) + (b ?? 0);
21
21
  }
22
22
 
23
+ export function cursorSpendFingerprint(row) {
24
+ if (!row || typeof row !== 'object') return null;
25
+ const input = numOrNull(row.inputTokens);
26
+ const output = numOrNull(row.outputTokens);
27
+ if (input == null && output == null) return null;
28
+ const model = String(row.model || row.modelId || '');
29
+ const cache = numOrNull(row.cacheReadTokens) ?? 0;
30
+ return `${model}|${input ?? 0}|${output ?? 0}|${cache}`;
31
+ }
32
+
33
+ function preferCursorSource(previous, next) {
34
+ if (!previous) return next;
35
+ if (!next) return previous;
36
+ if (next.event === 'stop' && previous.event !== 'stop') return next;
37
+ if (previous.event === 'stop' && next.event !== 'stop') return previous;
38
+ const prevAt = Date.parse(previous.at);
39
+ const nextAt = Date.parse(next.at);
40
+ if (Number.isFinite(nextAt) && Number.isFinite(prevAt) && nextAt !== prevAt) {
41
+ return nextAt > prevAt ? next : previous;
42
+ }
43
+ return (next.totalTokens ?? 0) >= (previous.totalTokens ?? 0) ? next : previous;
44
+ }
45
+
46
+ function stripCursorCollectMeta(record) {
47
+ if (!record || typeof record !== 'object') return record;
48
+ const { event, ...rest } = record;
49
+ return rest;
50
+ }
51
+
52
+ export function dedupeCursorSources(sources) {
53
+ const best = new Map();
54
+ const rest = [];
55
+ for (const src of sources || []) {
56
+ if (!src || src.platform !== 'cursor') {
57
+ rest.push(src);
58
+ continue;
59
+ }
60
+ const fp = cursorSpendFingerprint(src);
61
+ if (!fp) {
62
+ rest.push(src);
63
+ continue;
64
+ }
65
+ best.set(fp, preferCursorSource(best.get(fp), src));
66
+ }
67
+ return [...rest, ...[...best.values()].map(stripCursorCollectMeta)];
68
+ }
69
+
23
70
  function emptyPlatform(source = 'none') {
24
71
  return {
25
72
  inputTokens: null,
@@ -501,7 +548,130 @@ function collectAmpCli(ctx) {
501
548
 
502
549
  export const CURSOR_USAGE_FILE_REL = join('.agents', 'spend', 'cursor-usage.jsonl');
503
550
 
504
- function collectCursor({ cwd, windowStart, windowEnd, existing, notes }) {
551
+ function loadCursorUsageById(cwd) {
552
+ const filePath = join(cwd, CURSOR_USAGE_FILE_REL);
553
+ const bestById = new Map();
554
+ if (!existsSync(filePath)) return bestById;
555
+ let text;
556
+ try {
557
+ text = readFileSync(filePath, 'utf-8');
558
+ } catch {
559
+ return bestById;
560
+ }
561
+ for (const line of text.split('\n')) {
562
+ if (!line.trim()) continue;
563
+ let row;
564
+ try {
565
+ row = JSON.parse(line);
566
+ } catch {
567
+ continue;
568
+ }
569
+ if (!row || typeof row !== 'object') continue;
570
+ const id = row.id == null || row.id === '' ? null : String(row.id);
571
+ if (!id) continue;
572
+ const inputTokens = numOrNull(row.inputTokens);
573
+ const outputTokens = numOrNull(row.outputTokens);
574
+ if (inputTokens == null && outputTokens == null) continue;
575
+ const totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
576
+ const previous = bestById.get(id);
577
+ const previousTotal = previous
578
+ ? (numOrNull(previous.inputTokens) ?? 0) + (numOrNull(previous.outputTokens) ?? 0)
579
+ : -1;
580
+ if (!previous || totalTokens >= previousTotal) bestById.set(id, row);
581
+ }
582
+ return bestById;
583
+ }
584
+
585
+ export function attachCursorEstimates(sources, cwd) {
586
+ const byId = loadCursorUsageById(cwd);
587
+ let changed = false;
588
+ for (const src of sources || []) {
589
+ if (!src || src.platform !== 'cursor') continue;
590
+ const row = src.id ? byId.get(String(src.id)) : null;
591
+ if (row) {
592
+ const cache = numOrNull(row.cacheReadTokens);
593
+ if (src.cacheReadTokens == null && cache != null) {
594
+ src.cacheReadTokens = cache;
595
+ changed = true;
596
+ }
597
+ if (!src.model && (row.model || row.modelId)) {
598
+ src.model = row.model || row.modelId;
599
+ changed = true;
600
+ }
601
+ }
602
+ const described = describeCursorCostEstimate({
603
+ model: src.model,
604
+ inputTokens: src.inputTokens,
605
+ outputTokens: src.outputTokens,
606
+ cacheReadTokens: src.cacheReadTokens,
607
+ totalTokens: src.totalTokens,
608
+ });
609
+ if (!described) continue;
610
+ if (src.costUsdEstimated !== described.usd) {
611
+ src.costUsdEstimated = described.usd;
612
+ changed = true;
613
+ }
614
+ if (src.costSource !== described.costSource) {
615
+ src.costSource = described.costSource;
616
+ changed = true;
617
+ }
618
+ }
619
+ return changed;
620
+ }
621
+
622
+ export function enrichMetricsCursorEstimates(metrics, cwd) {
623
+ let changed = false;
624
+ for (const session of metrics.sessions || []) {
625
+ const deduped = dedupeCursorSources(session.sources || []);
626
+ if (deduped.length !== (session.sources || []).length) {
627
+ session.sources = deduped;
628
+ changed = true;
629
+ } else {
630
+ session.sources = deduped;
631
+ }
632
+ if (attachCursorEstimates(session.sources || [], cwd)) changed = true;
633
+ let estimated = null;
634
+ for (const src of session.sources || []) {
635
+ estimated = addNullable(estimated, numOrNull(src.costUsdEstimated));
636
+ }
637
+ if (estimated != null && session.costUsdEstimated !== estimated) {
638
+ session.costUsdEstimated = estimated;
639
+ changed = true;
640
+ }
641
+ if (!session.spendSource || session.spendSource === 'adapter' || session.spendSource === 'unreported') {
642
+ let inputTokens = null;
643
+ let outputTokens = null;
644
+ let totalTokens = null;
645
+ for (const src of session.sources || []) {
646
+ inputTokens = addNullable(inputTokens, numOrNull(src.inputTokens));
647
+ outputTokens = addNullable(outputTokens, numOrNull(src.outputTokens));
648
+ totalTokens = addNullable(totalTokens, numOrNull(src.totalTokens));
649
+ }
650
+ if (session.inputTokens !== inputTokens) {
651
+ session.inputTokens = inputTokens;
652
+ changed = true;
653
+ }
654
+ if (session.outputTokens !== outputTokens) {
655
+ session.outputTokens = outputTokens;
656
+ changed = true;
657
+ }
658
+ if (session.totalTokens !== totalTokens) {
659
+ session.totalTokens = totalTokens;
660
+ changed = true;
661
+ }
662
+ }
663
+ if (
664
+ session.spendSource === 'unreported'
665
+ && (session.inputTokens != null || session.totalTokens != null || (session.sources || []).length)
666
+ ) {
667
+ session.spendSource = 'adapter';
668
+ changed = true;
669
+ }
670
+ }
671
+ return changed;
672
+ }
673
+
674
+ function collectCursor({ cwd, windowStart, windowEnd, existing, existingSources, notes, env, cursorConversationId }) {
505
675
  const filePath = join(cwd, CURSOR_USAGE_FILE_REL);
506
676
  if (!existsSync(filePath)) {
507
677
  notes.push('cursor: usage file missing (spend hook not installed or no turns recorded yet)');
@@ -514,8 +684,16 @@ function collectCursor({ cwd, windowStart, windowEnd, existing, notes }) {
514
684
  notes.push('cursor: cannot read usage file');
515
685
  return [];
516
686
  }
687
+ const filterId = String((env && env.CURSOR_CONVERSATION_ID) || cursorConversationId || '').trim();
688
+ const existingFingerprints = new Set();
689
+ for (const src of existingSources || []) {
690
+ const fp = cursorSpendFingerprint(src);
691
+ if (fp) existingFingerprints.add(fp);
692
+ }
517
693
  // stop / afterAgentResponse / loop follow-ups may write the same generation_id
518
694
  // several times with cumulative turn totals; keep the largest record per id.
695
+ // Cursor sometimes omits generation_id, so stop + afterAgentResponse get two
696
+ // ids for one turn — collapse those by token fingerprint.
519
697
  const bestById = new Map();
520
698
  for (const line of text.split('\n')) {
521
699
  if (!line.trim()) continue;
@@ -529,10 +707,18 @@ function collectCursor({ cwd, windowStart, windowEnd, existing, notes }) {
529
707
  const id = row.id == null || row.id === '' ? null : String(row.id);
530
708
  if (!id) continue;
531
709
  if (existing.has(id)) continue;
710
+ if (filterId) {
711
+ const rowConversationId = row.conversationId == null || row.conversationId === ''
712
+ ? ''
713
+ : String(row.conversationId).trim();
714
+ if (rowConversationId !== filterId) continue;
715
+ }
532
716
  if (!inWindow(row.at, windowStart, windowEnd)) continue;
533
717
  const inputTokens = numOrNull(row.inputTokens);
534
718
  const outputTokens = numOrNull(row.outputTokens);
535
719
  if (inputTokens == null && outputTokens == null) continue;
720
+ const fp = cursorSpendFingerprint(row);
721
+ if (fp && existingFingerprints.has(fp)) continue;
536
722
  const model = row.model || row.modelId;
537
723
  const cacheReadTokens = numOrNull(row.cacheReadTokens);
538
724
  const described = describeCursorCostEstimate({
@@ -554,12 +740,18 @@ function collectCursor({ cwd, windowStart, windowEnd, existing, notes }) {
554
740
  costUsdEstimated: described?.usd ?? null,
555
741
  costSource: described?.costSource ?? null,
556
742
  });
743
+ record.event = row.event || null;
557
744
  const previous = bestById.get(id);
558
745
  if (!previous || (record.totalTokens ?? 0) >= (previous.totalTokens ?? 0)) {
559
746
  bestById.set(id, record);
560
747
  }
561
748
  }
562
- return [...bestById.values()];
749
+ const bestByFingerprint = new Map();
750
+ for (const record of bestById.values()) {
751
+ const fp = cursorSpendFingerprint(record) || record.id;
752
+ bestByFingerprint.set(fp, preferCursorSource(bestByFingerprint.get(fp), record));
753
+ }
754
+ return [...bestByFingerprint.values()].map(stripCursorCollectMeta);
563
755
  }
564
756
 
565
757
  function aggregate(sources) {
@@ -622,15 +814,18 @@ export function collectSpend(options = {}) {
622
814
  ? options.platforms.filter((name) => PLATFORMS.includes(name))
623
815
  : PLATFORMS;
624
816
  const run = (name) => !wanted.length || wanted.includes(name);
817
+ const existingSources = Array.isArray(options.existingSources) ? options.existingSources : [];
625
818
  const ctx = {
626
819
  cwd,
627
820
  windowStart,
628
821
  windowEnd,
629
822
  existing,
823
+ existingSources,
630
824
  env,
631
825
  homedir,
632
826
  notes,
633
827
  ampThreadId: options.ampThreadId,
828
+ cursorConversationId: options.cursorConversationId,
634
829
  exportAmpThread: options.exportAmpThread,
635
830
  listAmpThreads: options.listAmpThreads,
636
831
  usageAmpThread: options.usageAmpThread,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-orchestrator-kit",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",
@@ -20,8 +20,8 @@ Agents (local or cloud) write session artifacts only to git-tracked paths — ne
20
20
 
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
- 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`, `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).
23
+ 2. Fill `## Metrics` before running persist. Required keys: `platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`. Use `unknown` for unknown numbers — never invent `0`. Do not set `spend_source: self-report` when tokens are `unknown`. `--model` / `model` is the LLM product id (example `cursor-grok-4.6-xhigh-fast`); family `cursor-grok-4.6` is only a fallback; the CLI takes the product id from hook sources when they exist; Closed role MAY have a sentence after `—`; metrics stores the canonical token. `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-xhigh-fast`, `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.
@@ -146,8 +146,8 @@ Archive is one deterministic CLI call — `npx agent-orchestrator-kit archive <n
146
146
 
147
147
  **End of each session (HARD STOP — you are NOT done):**
148
148
  1. Write `openspec/changes/<name>/handoff.md` in the parent using the template below, including `## Metrics`.
149
- 2. Fill `## Metrics` (`platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`) before persist. Use `unknown` when a value is missing — never invent `0`.
150
- 3. Run `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` and require exit 0. `--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 or rewrite `## Metrics`. Optional `--platform cursor|claude|amp` or `AOK_PLATFORM`. Optional `--collect` also runs local spend adapters. The same command 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. The CLI appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md` (the git canon), upserts Memory JSON with an absolute path (`Decision:*` is a file→Memory mirror only), and prints the expanded self-contained prompt on stdout. Spawn `session-handoff` in persist mode ONLY if this CLI step failed.
149
+ 2. Fill `## Metrics` (`platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`) before persist. Use `unknown` for unknown numbers — never invent `0`. Do not set `spend_source: self-report` when tokens are `unknown`. `--model` / `model` is the LLM product id (example `cursor-grok-4.6-xhigh-fast`); family `cursor-grok-4.6` is only a fallback; the CLI takes the product id from hook sources when they exist; Closed role MAY have a sentence after `—`; metrics stores the canonical token.
150
+ 3. Run `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` and require exit 0. `--model` is the LLM product id of this chat (`claude-opus-5`, `claude-fable-5`, `gpt-5.6-sol`, `cursor-grok-4.6-xhigh-fast`) — 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 or rewrite `## Metrics`. Optional `--platform cursor|claude|amp` or `AOK_PLATFORM`. Optional `--collect` also runs local spend adapters. The same command 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. The CLI appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md` (the git canon), upserts Memory JSON with an absolute path (`Decision:*` is a file→Memory mirror only), and prints the expanded self-contained prompt on stdout. Spawn `session-handoff` in persist mode ONLY if this CLI step failed.
151
151
  4. If Memory MCP tools are available, mirror `Change:<name>`, `Handoff:<name>`, and new `Decision:<topic>` entities in one call — optional; its absence never blocks closing.
152
152
  5. Paste the CLI stdout as one fenced next-session prompt. First line is `/opsx:<next> <name>`; body uses `project.agent_language`; keep Done/Decisions/Blocked/spawn/HARD STOP complete. No banner. Do not emit a thin “read Memory” stub.
153
153
  6. Do not start the next phase in this chat. If apply, include build/lint status in the persisted Done section.
@@ -228,7 +228,7 @@ The Prompt section is overwritten by `npx agent-orchestrator-kit handoff <name>`
228
228
 
229
229
  Before specialist work, the parent MUST restore context in order: honor the pasted `/opsx:*` command; run `npx agent-orchestrator-kit handoff --restore` (the CLI briefing is canonical — no separate Memory MCP read step); if the CLI failed, read `openspec/changes/<name>/handoff.md`; spawn `session-handoff` in restore mode ONLY when both failed. Missing Memory MCP never blocks a session. With one active change, free-form “continue” uses `Handoff.next_command` instead of asking for the phase. Amp spawns any needed subagent as an isolated `subagent-*` skill.
230
230
 
231
- Before declaring a session closed, the parent MUST, in order: (1) write `openspec/changes/<name>/handoff.md` itself including `## Metrics` (keys `platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`; `unknown` when missing), (2) run `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` (exit 0) — NEVER pass a Closed role or subagent name as `--model`; the parent SHOULD still pass `--model` and MUST NOT guess tokens; spend flags override session totals only and do not rewrite `## Metrics`; optional `--platform`; optional `--collect` for local adapters; the same CLI works in Cursor, Claude Code, and Amp and MUST NOT require Cursor SDK, Claude `/cost`, or Amp billing as a required step; this CLI appends `decisions.md` and mirrors `Decision:*` file→Memory; spawn `session-handoff` persist ONLY if this CLI step failed, (3) paste the CLI stdout prompt whose first line is `/opsx:<next> <name>`. Memory MCP mirroring is an optional single call. Never write Memory back into `decisions.md`. The prompt has no `NEXT_SESSION_PROMPT` label, uses `project.agent_language`, and MUST be self-contained (Done, Decisions, Blocked, attach, spawn, HARD STOP) so the next thread can run if Memory MCP is ignored. Never start the next phase in the current chat. Write session artifacts only to git-tracked paths (never `/tmp`, never gitignored caches). If runtime is cloud: after persist, commit → push → `npx agent-orchestrator-kit handoff <name> --cloud-check` with exit 0; closing without that is an incomplete handoff.
231
+ Before declaring a session closed, the parent MUST, in order: (1) write `openspec/changes/<name>/handoff.md` itself including `## Metrics` (keys `platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`). Use `unknown` for unknown numbers — never invent `0`. Do not set `spend_source: self-report` when tokens are `unknown`. `--model` / `model` is the LLM product id (example `cursor-grok-4.6-xhigh-fast`); family `cursor-grok-4.6` is only a fallback; the CLI takes the product id from hook sources when they exist; Closed role MAY have a sentence after `—`; metrics stores the canonical token. (2) run `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` (exit 0) — NEVER pass a Closed role or subagent name as `--model`; the parent SHOULD still pass `--model` and MUST NOT guess tokens; spend flags override session totals only and do not rewrite `## Metrics`; optional `--platform`; optional `--collect` for local adapters; the same CLI works in Cursor, Claude Code, and Amp and MUST NOT require Cursor SDK, Claude `/cost`, or Amp billing as a required step; this CLI appends `decisions.md` and mirrors `Decision:*` file→Memory; spawn `session-handoff` persist ONLY if this CLI step failed, (3) paste the CLI stdout prompt whose first line is `/opsx:<next> <name>`. Memory MCP mirroring is an optional single call. Never write Memory back into `decisions.md`. The prompt has no `NEXT_SESSION_PROMPT` label, uses `project.agent_language`, and MUST be self-contained (Done, Decisions, Blocked, attach, spawn, HARD STOP) so the next thread can run if Memory MCP is ignored. Never start the next phase in the current chat. Write session artifacts only to git-tracked paths (never `/tmp`, never gitignored caches). If runtime is cloud: after persist, commit → push → `npx agent-orchestrator-kit handoff <name> --cloud-check` with exit 0; closing without that is an incomplete handoff.
232
232
 
233
233
  | Entity | Required fields |
234
234
  |--------|-----------------|
@@ -22,8 +22,8 @@ Use when the parent's restore failed (CLI restore and handoff.md both unavailabl
22
22
  Use when the parent's persist failed (`npx agent-orchestrator-kit handoff <name>` did not exit 0). A session is not closed until persist succeeds.
23
23
 
24
24
  1. Write or update `openspec/changes/<name>/handoff.md` with every required section: Closed role, Change, Done, Decisions, Blocked, Next command, Next role, Attach, Subagents to spawn, Constraints, Runtime, Metrics.
25
- 2. Fill `## Metrics` (`platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`) before persist. Use `unknown` when a value is missing. The section is the agent's self-report; the CLI does not overwrite it with resolved values.
26
- 3. Run `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` and require exit 0. `--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. Optional `--platform cursor|claude|amp` or `AOK_PLATFORM`. Optional `--collect` also runs local spend adapters. The same command 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. This appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md` (git canon), upserts `.cursor/memory.json` using an absolute path (`Decision:*` mirrors that file, never the reverse), and prints the expanded next-session prompt on stdout. Cloud sessions pass `--runtime cloud` (or `AOK_RUNTIME` / `AOK_AGENT_ID`).
25
+ 2. Fill `## Metrics` (`platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`) before persist. Use `unknown` for unknown numbers — never invent `0`. Do not set `spend_source: self-report` when tokens are `unknown`. `--model` / `model` is the LLM product id (example `cursor-grok-4.6-xhigh-fast`); family `cursor-grok-4.6` is only a fallback; the CLI takes the product id from hook sources when they exist; Closed role MAY have a sentence after `—`; metrics stores the canonical token. The CLI does not overwrite `## Metrics` with resolved values; `metrics.json` records what landed.
26
+ 3. Run `npx agent-orchestrator-kit handoff <name> --model <llm-product-id>` and require exit 0. `--model` is the LLM product id of this chat (`claude-opus-5`, `claude-fable-5`, `gpt-5.6-sol`, `cursor-grok-4.6-xhigh-fast`) — 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. Optional `--platform cursor|claude|amp` or `AOK_PLATFORM`. Optional `--collect` also runs local spend adapters. The same command 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. This appends non-empty Decisions into append-only `openspec/changes/<name>/decisions.md` (git canon), upserts `.cursor/memory.json` using an absolute path (`Decision:*` mirrors that file, never the reverse), and prints the expanded next-session prompt on stdout. Cloud sessions pass `--runtime cloud` (or `AOK_RUNTIME` / `AOK_AGENT_ID`).
27
27
  4. If Memory MCP tools are available, also create/update `Change:<name>`, `Handoff:<name>`, and each `Decision:<topic>` to match `decisions.md`. MCP failure is not a blocker after the CLI succeeds.
28
28
  5. Put the CLI stdout prompt (first line `/opsx:…`) into **Next prompt** unchanged. Do not shorten it. Do not add a banner.
29
29
  6. If runtime is cloud: after persist, commit and push `openspec/changes/<name>/`, then `npx agent-orchestrator-kit handoff <name> --cloud-check` (exit 0 required). Closing without this is an incomplete handoff. The CLI never runs `git commit` / `git push`.
@@ -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` 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.
12
+ 3. Fill `## Metrics` in the change `handoff.md` only when reporting Archiver-specific numbers. Use `unknown` for unknown numbers — never invent `0`. Do not set `spend_source: self-report` when tokens are `unknown`. `--model` / `model` is the LLM product id (example `cursor-grok-4.6-xhigh-fast`); family `cursor-grok-4.6` is only a fallback; the CLI takes the product id from hook sources when they exist; Closed role MAY have a sentence after `—`; metrics stores the canonical token. 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