agent-orchestrator-kit 0.13.0 → 0.14.1

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.
@@ -0,0 +1,51 @@
1
+ function numOrNull(value) {
2
+ if (value == null || value === '') return null;
3
+ const n = Number(value);
4
+ return Number.isFinite(n) ? n : null;
5
+ }
6
+
7
+ const RATES = {
8
+ 'claude-fable-5': { input: 10, cacheRead: 0.25, cacheWrite: 12.5, output: 50 },
9
+ 'claude-opus': { input: 5, cacheRead: 0.5, cacheWrite: 6.25, output: 25 },
10
+ 'claude-sonnet-5': { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 10 },
11
+ 'claude-sonnet-4-6': { input: 3, cacheRead: 0.3, cacheWrite: 3.75, output: 15 },
12
+ 'claude-haiku-4-5': { input: 1, cacheRead: 0.1, cacheWrite: 1.25, output: 5 },
13
+ };
14
+
15
+ function ratesForModel(model) {
16
+ const id = String(model || '').toLowerCase();
17
+ return Object.entries(RATES)
18
+ .sort(([a], [b]) => b.length - a.length)
19
+ .find(([prefix]) => id.startsWith(prefix))?.[1] || null;
20
+ }
21
+
22
+ export function estimateClaudeCostUsd({
23
+ model,
24
+ inputTokens,
25
+ cacheReadTokens,
26
+ cacheCreationTokens,
27
+ outputTokens,
28
+ } = {}) {
29
+ const input = numOrNull(inputTokens);
30
+ const cacheRead = numOrNull(cacheReadTokens);
31
+ const cacheWrite = numOrNull(cacheCreationTokens);
32
+ const output = numOrNull(outputTokens);
33
+ if (input == null && cacheRead == null && cacheWrite == null && output == null) return null;
34
+ const rates = ratesForModel(model);
35
+ const usd = rates
36
+ ? ((input ?? 0) * rates.input
37
+ + (cacheRead ?? 0) * rates.cacheRead
38
+ + (cacheWrite ?? 0) * rates.cacheWrite
39
+ + (output ?? 0) * rates.output) / 1e6
40
+ : (((input ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0)) * 3 + (output ?? 0) * 15) / 1e6;
41
+ return Math.round(usd * 10000) / 10000;
42
+ }
43
+
44
+ export function describeClaudeCostEstimate(args = {}) {
45
+ const usd = estimateClaudeCostUsd(args);
46
+ if (usd == null) return null;
47
+ return {
48
+ usd,
49
+ costSource: ratesForModel(args.model) ? 'api-estimate' : 'api-estimate-fallback',
50
+ };
51
+ }
@@ -1,10 +1,11 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'fs';
2
- import { join, basename } from 'path';
2
+ import { join, basename, sep } from 'path';
3
3
  import { homedir as osHomedir } from 'os';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { listRecentAmpThreadIds } from './session-client.js';
6
6
  import { formatUtcIso, parseFlexibleIso } from './metrics-time.js';
7
7
  import { describeCursorCostEstimate } from './cursor-cost-estimate.js';
8
+ import { describeClaudeCostEstimate } from './claude-cost-estimate.js';
8
9
  import { ampAgentMode, matchAmpUsageModel, parseAmpUsageDetails } from './amp-usage.js';
9
10
 
10
11
  const PLATFORMS = ['cursor', 'claude', 'amp'];
@@ -216,17 +217,31 @@ function collectClaude({ cwd, windowStart, windowEnd, existing, env, homedir, no
216
217
  notes.push('claude: project folder missing');
217
218
  return sources;
218
219
  }
219
- let files;
220
+ let entries;
220
221
  try {
221
- files = readdirSync(projectDir).filter((name) => name.endsWith('.jsonl'));
222
+ entries = readdirSync(projectDir, { withFileTypes: true });
222
223
  } catch {
223
224
  notes.push('claude: cannot read project folder');
224
225
  return sources;
225
226
  }
227
+ const files = entries
228
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
229
+ .map((entry) => join(projectDir, entry.name));
230
+ for (const entry of entries) {
231
+ if (!entry.isDirectory()) continue;
232
+ const subagentsDir = join(projectDir, entry.name, 'subagents');
233
+ if (!existsSync(subagentsDir)) continue;
234
+ try {
235
+ for (const name of readdirSync(subagentsDir)) {
236
+ if (name.endsWith('.jsonl')) files.push(join(subagentsDir, name));
237
+ }
238
+ } catch {}
239
+ }
240
+ const bestById = new Map();
226
241
  for (const file of files) {
227
242
  let text;
228
243
  try {
229
- text = readFileSync(join(projectDir, file), 'utf-8');
244
+ text = readFileSync(file, 'utf-8');
230
245
  } catch {
231
246
  continue;
232
247
  }
@@ -245,9 +260,18 @@ function collectClaude({ cwd, windowStart, windowEnd, existing, env, homedir, no
245
260
  const id = message.id;
246
261
  if (id == null || id === '') continue;
247
262
  if (existing.has(String(id))) continue;
248
- if (row.cwd !== cwd) continue;
263
+ if (row.cwd !== cwd && !String(row.cwd || '').startsWith(`${cwd}${sep}`)) continue;
249
264
  if (!inWindow(row.timestamp, windowStart, windowEnd)) continue;
250
- sources.push(sourceRecord({
265
+ const cacheReadTokens = numOrNull(usage.cache_read_input_tokens ?? usage.cacheReadInputTokens);
266
+ const cacheCreationTokens = numOrNull(usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens);
267
+ const described = describeClaudeCostEstimate({
268
+ model: message.model,
269
+ inputTokens: usage.input_tokens ?? usage.inputTokens,
270
+ cacheReadTokens,
271
+ cacheCreationTokens,
272
+ outputTokens: usage.output_tokens ?? usage.outputTokens,
273
+ });
274
+ const record = sourceRecord({
251
275
  id,
252
276
  platform: 'claude',
253
277
  model: message.model,
@@ -256,9 +280,21 @@ function collectClaude({ cwd, windowStart, windowEnd, existing, env, homedir, no
256
280
  costUsd: claudeCostUsd(row, usage),
257
281
  ampCredits: null,
258
282
  at: row.timestamp,
259
- }));
283
+ cacheReadTokens,
284
+ costUsdEstimated: described?.usd ?? null,
285
+ costSource: described?.costSource ?? null,
286
+ });
287
+ const previous = bestById.get(String(id));
288
+ if (!previous || (record.totalTokens ?? 0) > (previous.totalTokens ?? 0)) {
289
+ bestById.set(String(id), record);
290
+ } else if ((record.totalTokens ?? 0) === (previous.totalTokens ?? 0)) {
291
+ const at = parseTime(record.at);
292
+ const previousAt = parseTime(previous.at);
293
+ if (Number.isFinite(at) && (!Number.isFinite(previousAt) || at < previousAt)) previous.at = record.at;
294
+ }
260
295
  }
261
296
  }
297
+ sources.push(...bestById.values());
262
298
  return sources;
263
299
  }
264
300
 
@@ -409,10 +445,14 @@ export function sourcesFromAmpThread(thread, ctx, fileName = '', via = null) {
409
445
  return sources;
410
446
  }
411
447
 
412
- function collectAmp({ cwd, windowStart, windowEnd, existing, env, homedir, notes }) {
448
+ function collectAmp({ cwd, windowStart, windowEnd, existing, env, homedir, notes, ampThreadId, collectAll }) {
413
449
  const root = ampRoot(env, homedir);
414
450
  const threadsDir = join(root, 'threads');
415
451
  const sources = [];
452
+ if (!ampThreadId && collectAll !== true) {
453
+ notes.push('amp: skipped local threads without thread id');
454
+ return sources;
455
+ }
416
456
  if (!existsSync(threadsDir)) {
417
457
  notes.push('amp: threads folder missing');
418
458
  return sources;
@@ -432,6 +472,8 @@ function collectAmp({ cwd, windowStart, windowEnd, existing, env, homedir, notes
432
472
  } catch {
433
473
  continue;
434
474
  }
475
+ const threadKey = thread && thread.id ? String(thread.id) : basename(file, '.json');
476
+ if (ampThreadId && threadKey !== String(ampThreadId)) continue;
435
477
  sources.push(...sourcesFromAmpThread(thread, ctx, file));
436
478
  }
437
479
  return sources;
@@ -504,10 +546,10 @@ function collectAmpCli(ctx) {
504
546
  if (id && !ids.includes(id)) ids.push(id);
505
547
  };
506
548
  push(ampThreadId);
507
- if (ctx.listRecentAmpThreads !== false) {
549
+ if (ctx.listRecentAmpThreads === true) {
508
550
  push(ampCurrentThreadId(env));
509
551
  }
510
- if (!ids.length && ctx.listRecentAmpThreads !== false) {
552
+ if (!ids.length && ctx.listRecentAmpThreads === true) {
511
553
  for (const id of listRecentAmpThreadIds(ctx)) push(id);
512
554
  }
513
555
  const sources = [];
@@ -529,6 +571,10 @@ function collectAmpCli(ctx) {
529
571
  ...row,
530
572
  model: matchAmpUsageModel(row.model, sourceModels),
531
573
  }));
574
+ const alreadyBilled = ctx.existingThreadIds.has(id) && ctx.rebillThreadId !== id;
575
+ if (alreadyBilled) {
576
+ for (const row of usageModels) row.costUsd = null;
577
+ }
532
578
  if (usage && usage.costUsd != null) {
533
579
  for (const src of extracted) {
534
580
  src.costSource = 'amp-usage';
@@ -537,7 +583,7 @@ function collectAmpCli(ctx) {
537
583
  threads.push({
538
584
  id,
539
585
  agentMode,
540
- costUsd: usage ? numOrNull(usage.costUsd) : null,
586
+ costUsd: usage && !alreadyBilled ? numOrNull(usage.costUsd) : null,
541
587
  inputTokens: usage ? numOrNull(usage.inputTokens) : null,
542
588
  outputTokens: usage ? numOrNull(usage.outputTokens) : null,
543
589
  totalTokens: usage ? numOrNull(usage.totalTokens) : null,
@@ -708,7 +754,6 @@ function collectCursor({ cwd, windowStart, windowEnd, existing, existingSources,
708
754
  if (!row || typeof row !== 'object') continue;
709
755
  const id = row.id == null || row.id === '' ? null : String(row.id);
710
756
  if (!id) continue;
711
- if (existing.has(id)) continue;
712
757
  if (filterId) {
713
758
  const rowConversationId = row.conversationId == null || row.conversationId === ''
714
759
  ? ''
@@ -809,6 +854,9 @@ export function collectSpend(options = {}) {
809
854
  ? [...options.existingSourceIds]
810
855
  : [],
811
856
  );
857
+ const existingSourceTotals = options.existingSourceTotals && typeof options.existingSourceTotals === 'object'
858
+ ? options.existingSourceTotals
859
+ : {};
812
860
  const windowStart = options.windowStart;
813
861
  const windowEnd = options.windowEnd;
814
862
  const notes = [];
@@ -823,6 +871,7 @@ export function collectSpend(options = {}) {
823
871
  windowEnd,
824
872
  existing,
825
873
  existingSources,
874
+ existingSourceTotals,
826
875
  env,
827
876
  homedir,
828
877
  notes,
@@ -834,6 +883,9 @@ export function collectSpend(options = {}) {
834
883
  usageAmpThread: options.usageAmpThread,
835
884
  ampBin: options.ampBin,
836
885
  timeoutMs: options.timeoutMs,
886
+ collectAll: options.collectAll === true || options.platforms == null,
887
+ existingThreadIds: new Set(options.existingThreadIds || []),
888
+ rebillThreadId: options.rebillThreadId || null,
837
889
  };
838
890
  let sources = [];
839
891
  const ampThreads = [];
@@ -867,9 +919,20 @@ export function collectSpend(options = {}) {
867
919
  notes.push('cursor: adapter failed');
868
920
  }
869
921
  }
922
+ sources = sources.filter((source) => {
923
+ if (!source || !existing.has(String(source.id))) return true;
924
+ if (!Object.hasOwn(existingSourceTotals, source.id)) return false;
925
+ return (numOrNull(source.totalTokens) ?? 0) > (numOrNull(existingSourceTotals[source.id]) ?? 0);
926
+ });
870
927
  const { byPlatform, byModel } = aggregate(sources);
871
928
  applyAmpThreadSpend(byPlatform, byModel, ampThreads);
872
- return { sources, byPlatform, byModel, notes, ampThreads };
929
+ const ids = [...new Set(sources.map((source) => String(source.id)))];
930
+ const totals = {};
931
+ for (const source of sources) {
932
+ const id = String(source.id);
933
+ totals[id] = Math.max(numOrNull(totals[id]) ?? 0, numOrNull(source.totalTokens) ?? 0);
934
+ }
935
+ return { sources, ids, totals, byPlatform, byModel, notes, ampThreads };
873
936
  }
874
937
 
875
938
  function applyAmpThreadSpend(byPlatform, byModel, threads) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-orchestrator-kit",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
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",
@@ -35,6 +35,7 @@
35
35
  "files": [
36
36
  "bin/",
37
37
  "templates/",
38
+ "!templates/.cursor",
38
39
  "profiles/",
39
40
  "README.md",
40
41
  "CHANGELOG.md",
@@ -11,6 +11,9 @@ pipeline:
11
11
  max_active_changes: 1
12
12
  archive_after_merge: true
13
13
  task_contract: warn
14
+ # Paths gate-check treats as product code. Widen it when code lives
15
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
16
+ src_glob: "src/"
14
17
 
15
18
  roles:
16
19
  explorer:
@@ -16,6 +16,9 @@ pipeline:
16
16
  archive_after_merge: false
17
17
  quick_mode_enabled: true
18
18
  task_contract: off
19
+ # Paths gate-check treats as product code. Widen it when code lives
20
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
21
+ src_glob: "src/"
19
22
 
20
23
  roles:
21
24
  explorer:
@@ -14,6 +14,9 @@ pipeline:
14
14
  max_active_changes: 1
15
15
  archive_after_merge: true
16
16
  task_contract: warn
17
+ # Paths gate-check treats as product code. Widen it when code lives
18
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
19
+ src_glob: "src/"
17
20
 
18
21
  roles:
19
22
  explorer:
@@ -14,6 +14,9 @@ pipeline:
14
14
  max_active_changes: 1
15
15
  archive_after_merge: true
16
16
  task_contract: warn
17
+ # Paths gate-check treats as product code. Widen it when code lives
18
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
19
+ src_glob: "src/"
17
20
 
18
21
  roles:
19
22
  explorer:
@@ -35,6 +35,12 @@ When ready to implement, run /opsx:apply
35
35
 
36
36
  Each task must be self-contained for a blind implementer — executable without reading design.md. `Files:` paths must exist unless prefixed with `new file:`. Lint: `npx agent-orchestrator-kit gate-check --tasks <name>` (mode via `pipeline.task_contract: warn|strict|off`).
37
37
 
38
+ On re-propose after `review.md` Verdict REQUEST CHANGES, the conductor MUST pass `review.md` (path + verdict + Required Before Apply list) in the `spec-architect` spawn prompt and verify the report addresses every item; the parent MUST NOT itself edit proposal/design/specs/tasks. Exception: the structure-only propose trigger is the exact line
39
+
40
+ **Source:** gate-check
41
+
42
+ plus the absence of `## Checklist`; then fix only those gate-check errors.
43
+
38
44
  **Steps**
39
45
 
40
46
  1. **If no input provided, ask what they want to build**
@@ -38,11 +38,15 @@ npx agent-orchestrator-kit gate-check --review <name>
38
38
 
39
39
  The script runs `openspec validate --strict --type change`, the task-contract lint (Files/Do/Done-when), the `Non-goals` / `Acceptance criteria` proposal sections check, and non-empty ADDED/MODIFIED/REMOVED delta-spec sections check. Add `--json` for a `{pass, errors[]}` report.
40
40
 
41
- **If Tier 1 fails (exit ≠ 0):** do NOT spawn `spec-reviewer` and do NOT read the artifacts. Write `openspec/changes/<name>/review.md` with `Verdict: REQUEST CHANGES` listing the gate-check errors (source: gate-check), output the Request Changes verdict in chat, and go straight to Session Exit.
41
+ **If Tier 1 fails (exit ≠ 0):** do NOT spawn `spec-reviewer` and do NOT read the artifacts. Write `openspec/changes/<name>/review.md` with `Verdict: REQUEST CHANGES` listing the gate-check errors. The T1 file MUST include this exact line:
42
+
43
+ **Source:** gate-check
44
+
45
+ The T1 `review.md` has no `## Checklist` section. This parent-written T1 `review.md` is an **accepted exception** to pipeline-subagents «parent MUST NOT write the verdict». Output the Request Changes verdict in chat. After an accepted RC one line MUST contain `Verdict: REQUEST CHANGES` and `/opsx:propose`. Go straight to Session Exit.
42
46
 
43
47
  ### 3. Tier 2 — spawn the specialist
44
48
 
45
- Only after Tier 1 passes: spawn `spec-reviewer` with the complete change paths, project constraints, and the shortened checklist below. Require `## Subagent report: spec-reviewer`. Do not perform the review in the parent session.
49
+ Only after Tier 1 passes: spawn `spec-reviewer` with the complete change paths, project constraints, and the shortened checklist below. The parent MUST paste the full Tier 2 checklist from this file into the `spec-reviewer` prompt, including the Vue 3 items when `project.stack: vue3`. Before spawn, the parent MUST record whether `openspec/changes/<name>/review.md` already existed, and pass that fact plus the path in the spawn prompt. Require `## Subagent report: spec-reviewer`. Do not perform the review in the parent session.
46
50
 
47
51
  ### 4. Review checklist (Tier 2 — LLM-only)
48
52
 
@@ -68,6 +72,10 @@ Do NOT re-check what Tier 1 already covered (strict validation, contract field p
68
72
  - [ ] Tasks reference concrete component/store paths under `src/`
69
73
  - [ ] No scope creep into unrelated UI refactors
70
74
 
75
+ MUST NOT stop at the first blocking finding. Finish the full LLM checklist and a complete scan of proposal.md, design.md, tasks.md, all delta specs, and referenced main specs/repo paths before writing the verdict. One ✗ still means REQUEST CHANGES, but list every blocking issue of that pass.
76
+
77
+ Re-review MUST scan the same defect class — LLM-only only (another task whose `Do:` is not executable without design.md; another design behaviour with no delta requirement; another proposal↔tasks drift; another referenced heading/path that does not exist). Tier 1 classes NEVER enter this rescan. MUST NOT emit a one-item RC that names only the first leftover.
78
+
71
79
  ### 5. Write and report the verdict
72
80
 
73
81
  #### If all ✓ (or only minor notes):
@@ -110,12 +118,16 @@ Create or update `openspec/changes/<name>/review.md`:
110
118
 
111
119
  On **APPROVE**, `spec-reviewer` also writes `openspec/changes/<name>/apply-notes.md` (≤ 20 lines): critical constraints, pitfalls, what NOT to touch, verification commands. It is the distilled input for `/opsx:apply` and the **second allowed file** next to `review.md`.
112
120
 
113
- For **REQUEST CHANGES**, write only `review.md` with `Verdict: REQUEST CHANGES` and the issues list.
121
+ For **REQUEST CHANGES**, write only `review.md` with `Verdict: REQUEST CHANGES` and the required sections: Checklist (each T2 item ✓/✗), Findings (Blocker / Major / Minor; empty buckets allowed), Required Before Apply (blocking only), Previous findings. Cosmetics stay out of Required Before Apply.
122
+
123
+ The `Previous findings` heading is ALWAYS present after any Tier 2 pass. If no prior `review.md` existed, the body is the literal line `none — first review cycle`. If a prior file existed, each prior Required Before Apply item → `resolved` | `unresolved` plus one-line evidence.
114
124
 
115
125
  `review.md` (always) and `apply-notes.md` (on APPROVE) are the **only files** you may write during review (not `src/`, not `tasks.md` checkboxes).
116
126
 
117
127
  The conductor verifies the subagent's `Status: done`, checks that `review.md` exists with the reported verdict (and `apply-notes.md` on APPROVE), and relays the result without editing them.
118
128
 
129
+ After Tier 2, the conductor MUST reject an RC `review.md` that lacks those headings or has an empty Checklist and MUST NOT rewrite the file; then re-spawn `spec-reviewer` once with the rejection reason and required headings; if the second file is still non-conforming, close with `## Blocked` naming the missing headings and next command `/opsx:review <name>` (a rejected file is not an accepted verdict). NEXT-AFTER-RC applies only to an accepted (schema-conforming) RC.
130
+
119
131
  #### If any ✗:
120
132
 
121
133
  ```
@@ -123,25 +135,37 @@ The conductor verifies the subagent's `Status: done`, checks that `review.md` ex
123
135
 
124
136
  **Change:** <name>
125
137
 
126
- ### Issues Found
138
+ ### Checklist
139
+ - proposal ↔ design ↔ tasks: ✓ or ✗
140
+ - Delta specs cover design: ✓ or ✗
141
+ - No conflicts with main specs: ✓ or ✗
142
+ - No scope creep vs Non-goals: ✓ or ✗
143
+ - Task self-sufficiency: ✓ or ✗
144
+ - Vue 3 items (when `project.stack: vue3`): ✓ or ✗ each
145
+
146
+ ### Findings
147
+
148
+ #### Blocker
149
+ - <or empty>
127
150
 
128
- #### Proposal
129
- - <issue description> — suggestion: <how to fix>
151
+ #### Major
152
+ - <or empty>
130
153
 
131
- #### Tasks
132
- - Task 3 is too vague: "Update the component" — specify which component and what exact change
154
+ #### Minor
155
+ - <or empty>
133
156
 
134
157
  ### Required Before Apply
135
- <list only what must be fixed, not cosmetic>
158
+ - <blocking only>
136
159
 
137
- Fix the above, then re-run `/opsx:review <name>`.
160
+ ### Previous findings
161
+ none — first review cycle
138
162
  ```
139
163
 
140
164
  ---
141
165
 
142
166
  ## Session Exit (HARD STOP)
143
167
 
144
- Close via the canonical Session Exit protocol in `.agents/rules/session-handoff.mdc`. First line of the pasted prompt is the next `/opsx:*` command (`/opsx:apply <name>` only after APPROVE). Do not start the next phase in this chat.
168
+ Close via the canonical Session Exit protocol in `.agents/rules/session-handoff.mdc`. After an accepted REQUEST CHANGES one line contains `Verdict: REQUEST CHANGES` and `/opsx:propose`; after APPROVE `/opsx:apply <name>`. NEXT-AFTER-RC applies only to an accepted (schema-conforming) RC. Do not start the next phase in this chat.
145
169
 
146
170
  ## Guardrails
147
171
 
@@ -20,13 +20,13 @@ 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` 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.
23
+ 2. Fill `## Metrics` before persist: `platform`, `model`, `input_tokens`, `output_tokens`, `cost_usd`, `amp_credits`, `spend_source`. `platform` and product-id `model` are required and never `unknown`; unknown numbers use `unknown`, never invented `0`. Do not set self-report when all numbers are unknown. Put decisions in `## Decisions`; only the CLI writes `decisions.md`.
24
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.
28
28
  7. If runtime is cloud: after persist, `git add openspec/changes/<name>/` → `git commit` → `git push` → `npx agent-orchestrator-kit handoff <name> --cloud-check` (exit 0 required). Closing without this is an incomplete handoff. Persist prints these steps on stderr; the CLI never runs `git commit` / `git push`.
29
- 8. Stop. Next role = new chat.
29
+ 8. Stop. Next role and any out-of-OpenSpec hotfix = new chat. Never run full persist twice; regenerate the prompt only with `handoff <name> --no-metrics`.
30
30
 
31
31
  ## Archive exception
32
32
  `npx agent-orchestrator-kit archive <name>` writes the final `handoff.md` (`next_command: none`) in the archive folder and upserts memory itself. After a successful archive no fenced next-prompt is required — the pipeline is complete.
@@ -109,7 +109,7 @@ Before apply, check `.agents/orchestrator.yaml`:
109
109
  - `require_spec_review: true` → apply MUST find `review.md` with `Verdict: APPROVE` or Approve in session
110
110
  - `require_spec_review: false` → apply allowed directly (mvp / quick mode)
111
111
 
112
- If Request Changes — fix artifacts, re-run `/opsx:review`.
112
+ If Request Changes — run `/opsx:propose <name>` to fix the punch list, then a new `/opsx:review`.
113
113
 
114
114
  This is no longer only a chat convention: `npx agent-orchestrator-kit gate-check` runs in CI (both `agent-verify.yml` fragments) and fails the pipeline if `src/` changed without an approved `review.md` — a forgotten or skipped review is caught at merge time, not just at apply time. When `require_design_brief: true`, the same command also requires `design-brief.md` (or `Design: none` in `proposal.md`).
115
115
 
@@ -146,11 +146,11 @@ 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` 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.
149
+ 2. Fill `## Metrics` before persist. `platform` and product-id `model` are required and never `unknown`; unknown numbers use `unknown`, never invented `0`. Put decisions in `## Decisions`; only the CLI writes `decisions.md`.
150
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
- 6. Do not start the next phase in this chat. If apply, include build/lint status in the persisted Done section.
153
+ 6. Do not start the next phase in this chat. Never run full persist twice; regenerate the prompt only with `handoff <name> --no-metrics`. Any out-of-OpenSpec hotfix after persist requires a new chat. If apply, include build/lint status in Done.
154
154
 
155
155
  `handoff.md` template:
156
156
 
@@ -257,12 +257,13 @@ Before declaring a session closed, the parent MUST, in order: (1) write `openspe
257
257
  | No archive after merge | Next propose has stale domain specs |
258
258
  | Strong model on lint fixes | 5–10x cost with no quality gain |
259
259
  | Skip Memory MCP / skip `handoff` CLI | Next thread has no context; Amp looks like it “ignored the rules” |
260
+ | one-finding review loop | fragments defects across many propose/review sessions |
260
261
 
261
262
  ## Metrics (health check per change)
262
263
 
263
264
  - Sessions: 4–8 (not 1 marathon, not 20 micro-sessions)
264
265
  - Apply iterations to PR: ≤ 2
265
- - Spec review loops: ≤ 1
266
+ - Spec review discovery loops: ≤ 2 (optional Tier 1 structural RC plus one semantic Tier 2 RC; a confirmation APPROVE after an exhaustive propose does not count as a discovery loop)
266
267
  - Tasks rework: ≤ 10%
267
268
 
268
269
  If apply iterations > 2 → problem is in Architect or Reviewer, not Implementer.
@@ -35,6 +35,12 @@ When ready to implement, run /opsx:apply
35
35
 
36
36
  Each task must be self-contained for a blind implementer — executable without reading design.md. `Files:` paths must exist unless prefixed with `new file:`. Lint: `npx agent-orchestrator-kit gate-check --tasks <name>` (mode via `pipeline.task_contract: warn|strict|off`).
37
37
 
38
+ On re-propose after `review.md` Verdict REQUEST CHANGES, the conductor MUST pass `review.md` (path + verdict + Required Before Apply list) in the `spec-architect` spawn prompt and verify the report addresses every item; the parent MUST NOT itself edit proposal/design/specs/tasks. Exception: the structure-only propose trigger is the exact line
39
+
40
+ **Source:** gate-check
41
+
42
+ plus the absence of `## Checklist`; then fix only those gate-check errors.
43
+
38
44
  **Steps**
39
45
 
40
46
  1. **If no clear input provided, ask what they want to build**
@@ -15,8 +15,9 @@ On every invocation:
15
15
  4. Map what you find to the correct next command:
16
16
  - No `proposal.md` yet → `/opsx:propose <name>`
17
17
  - `require_design_brief: true`, UI-touching change, no `design-brief.md`, no `Design: none` in `proposal.md` → `/opsx:design <name>`
18
- - `proposal.md` exists but no `review.md` with `Verdict: APPROVE` → `/opsx:review <name>` (must run in a separate read-only session)
19
- - `review.md` says APPROVE but `tasks.md` has unchecked `- [ ]` items → `/opsx:apply <name>`
18
+ - `proposal.md` exists but no `review.md` → `/opsx:review <name>` (must run in a separate read-only session)
19
+ - `review.md` contains `Verdict: REQUEST CHANGES` → `/opsx:propose <name>`
20
+ - `review.md` has `Verdict: APPROVE` but `tasks.md` has unchecked `- [ ]` items → `/opsx:apply <name>`
20
21
  - All tasks `[x]` and review approved → ready to archive, suggest `/opsx:archive <name>` (or note that GitLab/GitHub CI auto-archives after merge if `archive_after_merge: true`)
21
22
  5. If a CI gate (`gate-check`, `verify-openspec-pr`) is failing, reproduce the check locally (`npx agent-orchestrator-kit gate-check <name>`, `npm run verify:openspec:pr`) and quote the exact failing reason from its output — don't guess.
22
23
  6. If `pipeline.max_active_changes` is exceeded, say so explicitly and name which changes are over the limit.
@@ -22,7 +22,7 @@ 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` 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.
25
+ 2. Fill `## Metrics` before persist. `platform` and product-id `model` are required and never `unknown`; unknown numbers use `unknown`, never invented `0`. Put decisions in `## Decisions`; only the CLI writes `decisions.md`.
26
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.
@@ -33,6 +33,7 @@ Use when the parent's persist failed (`npx agent-orchestrator-kit handoff <name>
33
33
  - Write session artifacts only to git-tracked paths (never `/tmp`, never gitignored caches).
34
34
  - Do NOT edit `src/`, tests, main specs, `tasks.md` checkboxes, or phase artifacts (`proposal.md`, `review.md`, `design-brief.md`) except `handoff.md`.
35
35
  - Do NOT start the next OpenSpec phase.
36
+ - Never run full persist twice. Regenerate a prompt only with `handoff <name> --no-metrics`; after persist, move any next-role or out-of-OpenSpec work to a new chat.
36
37
  - Do NOT return a thin prompt. The next thread must be able to run if Memory MCP is ignored.
37
38
  - Stop as blocked when the change name or next command cannot be resolved.
38
39
 
@@ -11,7 +11,12 @@ Workflow:
11
11
  2. Create or update `proposal.md`, `design.md`, `specs/<capability>/spec.md`, and `tasks.md` using the repository's OpenSpec schema and conventions.
12
12
  3. Keep requirements testable: each requirement uses SHALL/MUST language and includes concrete scenarios.
13
13
  4. Make tasks ordered, independently verifiable, and traceable to the design and delta specs. Every task MUST follow the task contract: indented `Files:` (existing paths, or `new file:` prefix for new ones), `Do:` (concrete change, no vague wording like "as needed" / "if necessary" / "as appropriate"), and `Done-when:` (verifiable condition or command). Each task must be self-contained for a blind implementer without reading design.md.
14
- 5. Report which validation command the conductor should run; do not cross into review or implementation.
14
+ 5. On re-propose after REQUEST CHANGES, the architect MUST read `review.md`, fix every Required Before Apply item, and re-scan the same defect class in proposal.md, design.md, tasks.md, and all delta specs (LLM-only classes only: another task whose `Do:` is not executable without design.md; another design behaviour with no delta requirement; another proposal↔tasks drift; another referenced heading/path that does not exist); do not stop after the listed items; Tier 1 classes NEVER enter this rescan. Exception: the structure-only propose trigger is the exact line
15
+
16
+ **Source:** gate-check
17
+
18
+ plus the absence of `## Checklist`; then fix only those gate-check errors.
19
+ 6. Report which validation command the conductor should run; do not cross into review or implementation.
15
20
 
16
21
  Rules:
17
22
 
@@ -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` 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.
12
+ 3. Fill `## Metrics` with Archiver-only numbers before archive. `platform` and product-id `model` are required and never `unknown`; unknown numbers use `unknown`, never invented `0`. Put decisions in `## Decisions`; only the CLI writes `decisions.md`. Do not copy the apply 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
 
@@ -18,6 +18,7 @@ Rules:
18
18
  - Do NOT edit `src/`, tests, CI, or implementation files.
19
19
  - Do NOT add new features, redesign requirements, or repair incomplete implementation during archive.
20
20
  - Do NOT manually discard delta requirements to make validation pass.
21
+ - Do not run archive/persist twice; after success, stop and move out-of-OpenSpec work to a new chat. Prompt regeneration before archive uses `handoff <name> --no-metrics`.
21
22
  - If archive prerequisites are missing, return `blocked` with the exact unmet gate.
22
23
 
23
24
  Return exactly this report contract:
@@ -13,7 +13,8 @@ Workflow:
13
13
  - conflicts with existing `openspec/specs/` requirements;
14
14
  - scope creep vs proposal Non-goals;
15
15
  - task self-sufficiency: a blind implementer can execute each task from Files/Do/Done-when alone, without design.md.
16
- 3. Write `review.md` with findings ordered by severity and exactly one verdict: `APPROVE` or `REQUEST CHANGES`.
16
+ MUST NOT stop at the first blocking finding. Finish the full LLM checklist and a complete artifact scan of proposal.md, design.md, tasks.md, all delta specs, and referenced main specs/repo paths before writing the verdict. One still means REQUEST CHANGES, but list every blocking issue of that pass.
17
+ 3. MUST read the existing `review.md` before overwriting it. Write `review.md` with findings ordered by severity and exactly one verdict: `APPROVE` or `REQUEST CHANGES`. REQUEST CHANGES and re-review MUST use headings Checklist, Findings (Blocker / Major / Minor), Required Before Apply, Previous findings.
17
18
  4. On APPROVE, also write `apply-notes.md` (≤ 20 lines): critical constraints, pitfalls, what NOT to touch, verification commands. It is the second and last file you may write.
18
19
  5. Approve only when artifacts are implementable without material guessing.
19
20
 
@@ -23,6 +24,9 @@ Rules:
23
24
  - Do NOT implement fixes found during review.
24
25
  - Do NOT substitute for `code-reviewer`; that agent reviews the implementation diff after apply.
25
26
  - Do NOT approve based only on Tier 1 passing; verify semantics and repository references.
27
+ - The `Previous findings` heading is ALWAYS present after any Tier 2 pass. If no prior `review.md` existed, the body is the literal line `none — first review cycle`. If a prior file existed, each prior Required Before Apply item → `resolved` | `unresolved` plus one-line evidence.
28
+ - Cosmetics stay out of Required Before Apply.
29
+ - Later review MUST carry prior Required Before Apply into Previous findings and MUST still scan the same defect class (LLM-only only: another task whose `Do:` is not executable without design.md; another design behaviour with no delta requirement; another proposal↔tasks drift; another referenced heading/path that does not exist). Tier 1 classes NEVER enter this rescan. MUST NOT emit a one-item RC that lists only the first leftover.
26
30
 
27
31
  Return exactly this report contract:
28
32
 
@@ -25,7 +25,7 @@ Routing table, HARD STOP, and CLI forms: `.agents/rules/` (`agent-orchestration`
25
25
 
26
26
  Session Start / Exit are **parent-driven** — canonical protocol in `.agents/rules/session-handoff.mdc`. Start: `status` → `handoff --restore` → `handoff.md` fallback. Exit HARD STOP: parent writes `handoff.md` including `## Metrics` (use `unknown` when a value is missing) → `npx agent-orchestrator-kit handoff <name>` (exit 0; optional `--collect`) → paste the CLI `/opsx:*` prompt. `session-handoff` subagent = fallback only. Do not start the next phase here.
27
27
 
28
- Quality gates: `gate-check --tasks <name>` lints the task contract (Files/Do/Done-when, `pipeline.task_contract: warn|strict|off`); `gate-check --review <name>` is deterministic Tier 1 of review — spec-reviewer (Tier 2) is spawned only after it passes and writes `apply-notes.md` on APPROVE.
28
+ Quality gates: `gate-check --tasks <name>` lints the task contract (Files/Do/Done-when, `pipeline.task_contract: warn|strict|off`); `gate-check --review <name>` is deterministic Tier 1 of review — spec-reviewer (Tier 2) is spawned only after it passes and writes `apply-notes.md` on APPROVE. Spec review discovery loops ≤ 2 (optional Tier 1 structural RC plus one semantic Tier 2 RC; a confirmation APPROVE after an exhaustive propose does not count). Anti-pattern: one-finding review loop — fragments defects across many propose/review sessions.
29
29
 
30
30
  ## Hard rules
31
31
  - One active change (unless mvp profile).
@@ -13,6 +13,9 @@ pipeline:
13
13
  max_active_changes: 1
14
14
  archive_after_merge: true
15
15
  task_contract: warn
16
+ # Paths gate-check treats as product code. Widen it when code lives
17
+ # outside src/ (e.g. "{src,lib,app}/") or the review gate never fires.
18
+ src_glob: "src/"
16
19
 
17
20
  roles:
18
21
  explorer: