@aiwg/cli 2026.10.6 → 2026.10.7

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.
Files changed (84) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/decision.js +3 -2
  18. package/dist/src/cli/handlers/effect.js +4 -84
  19. package/dist/src/cli/handlers/guided-setup.js +1 -1
  20. package/dist/src/cli/handlers/init.js +1 -1
  21. package/dist/src/cli/handlers/install.js +6 -0
  22. package/dist/src/cli/handlers/marketplace.js +15 -4
  23. package/dist/src/cli/handlers/setup.js +24 -20
  24. package/dist/src/cli/handlers/use.js +57 -15
  25. package/dist/src/cli/prompt-utils.js +73 -0
  26. package/dist/src/config/gitignore.js +4 -0
  27. package/dist/src/config/workspace.js +2 -1
  28. package/dist/src/decision/adapters/jev.js +20 -31
  29. package/dist/src/decision/ask.js +6 -1
  30. package/dist/src/decision/benchmark.js +19 -6
  31. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  32. package/dist/src/decision/qualification/load-staged.js +47 -10
  33. package/dist/src/decision/qualification/runner.js +7 -3
  34. package/dist/src/discovery/interview.js +10 -5
  35. package/dist/src/discovery/plan-apply.js +26 -30
  36. package/dist/src/discovery/plan-validation.js +2 -2
  37. package/dist/src/effects/host.js +91 -0
  38. package/dist/src/extensions/commands/definitions.js +2 -2
  39. package/dist/src/extensions/project-local-gitignore.js +2 -0
  40. package/dist/src/issues/cli.js +16 -5
  41. package/dist/src/issues/live.js +44 -12
  42. package/dist/src/marketplace/exchange.js +18 -6
  43. package/dist/src/marketplace/provenance.js +3 -2
  44. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  45. package/dist/src/mcp/cli.mjs +17 -90
  46. package/dist/src/mcp/grok-build-config.mjs +2 -1
  47. package/dist/src/mcp/muse-native-config.mjs +2 -1
  48. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  49. package/dist/src/memory/project-registry.js +2 -1
  50. package/dist/src/mission-protocol/worker-status.js +91 -1
  51. package/dist/src/models/worker-installation.d.mts +1 -1
  52. package/dist/src/models/worker-installation.mjs +65 -4
  53. package/dist/src/packages/adapters/git.js +99 -23
  54. package/dist/src/packages/adapters/gitea.js +8 -27
  55. package/dist/src/packages/adapters/github.js +7 -25
  56. package/dist/src/packages/registry.js +4 -4
  57. package/dist/src/process/cli-command.d.mts +5 -0
  58. package/dist/src/process/cli-command.mjs +42 -0
  59. package/dist/src/serve/worker-invocation.js +10 -0
  60. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  61. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  62. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  63. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  64. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  65. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  66. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  67. package/dist/src/tracker/project-inputs.js +2 -0
  68. package/dist/src/update/notifier.mjs +2 -4
  69. package/package.json +1 -1
  70. package/tools/agents/providers/base.mjs +5 -1
  71. package/tools/agents/providers/claude.mjs +5 -2
  72. package/tools/agents/providers/codex.mjs +4 -1
  73. package/tools/agents/providers/context-write.mjs +28 -0
  74. package/tools/agents/providers/copilot.mjs +5 -2
  75. package/tools/agents/providers/hermes.mjs +5 -2
  76. package/tools/agents/providers/omp.mjs +3 -1
  77. package/tools/agents/providers/rule-delivery.mjs +29 -12
  78. package/tools/agents/providers/warp.mjs +4 -1
  79. package/tools/agents/providers/windsurf.mjs +6 -3
  80. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  81. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  82. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  83. package/tools/ralph-external/process-monitor.mjs +42 -4
  84. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -1,3 +1,4 @@
1
+ import { ContextWriteJournal } from '../../smiths/context-pipeline/context-file-safety.js';
1
2
  import { trackTemporaryDirectory } from '../../utils/temporary-directory.js';
2
3
  /**
3
4
  * Use Command Handler
@@ -1675,6 +1676,11 @@ export async function countPiAgentFiles(target, agentsPath) {
1675
1676
  // and additive passes can describe the same destination more than once.
1676
1677
  let usePreviewActions;
1677
1678
  // Recursive framework/addon/provider passes share the boundary's resolved policy.
1679
+ async function generateDeploymentContextFiles(options) {
1680
+ const result = await generateContextFiles(options);
1681
+ useDeployment?.contextFiles.push(result);
1682
+ return result;
1683
+ }
1678
1684
  let useDeployment;
1679
1685
  function useDeploymentArgs(args) {
1680
1686
  const forwarded = [...args];
@@ -2118,7 +2124,7 @@ async function generateGlobalProjectContext(opts) {
2118
2124
  skills: userPaths.skills,
2119
2125
  behaviors: userPaths.behaviors,
2120
2126
  });
2121
- await generateContextFiles({
2127
+ await generateDeploymentContextFiles({
2122
2128
  provider: opts.provider,
2123
2129
  projectPath: opts.projectPath,
2124
2130
  sections,
@@ -2181,7 +2187,7 @@ async function ensurePostDeployPhases(opts) {
2181
2187
  behaviors: paths.behaviors,
2182
2188
  });
2183
2189
  try {
2184
- await generateContextFiles({
2190
+ await generateDeploymentContextFiles({
2185
2191
  provider: opts.provider,
2186
2192
  projectPath: opts.projectPath,
2187
2193
  sections,
@@ -2194,8 +2200,8 @@ async function ensurePostDeployPhases(opts) {
2194
2200
  },
2195
2201
  });
2196
2202
  }
2197
- catch {
2198
- // The shared verifier reports context or provider-wiring failures.
2203
+ catch (error) {
2204
+ throw new Error(`Context generation failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2199
2205
  }
2200
2206
  }
2201
2207
  async function deploySourceDirectory(opts) {
@@ -2544,11 +2550,40 @@ export class UseHandler {
2544
2550
  useDeployment = {
2545
2551
  version: previousDeployment?.version ?? resolveDeployVersion(ctx.frameworkRoot || await getFrameworkRoot()),
2546
2552
  skipContextFiles: previousDeployment?.skipContextFiles || ctx.args.includes('--no-context-files'),
2553
+ contextFiles: previousDeployment?.contextFiles ?? [],
2547
2554
  };
2555
+ const journal = previousDeployment ? undefined : new ContextWriteJournal();
2548
2556
  try {
2549
- return await this.executeDeployment(ctx);
2557
+ let result;
2558
+ try {
2559
+ result = await this.executeDeployment(ctx);
2560
+ }
2561
+ catch (error) {
2562
+ result = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
2563
+ }
2564
+ if (journal) {
2565
+ const rollback = result.exitCode !== 0 ? journal.rollback() : undefined;
2566
+ if (ctx.args.includes('--json')) {
2567
+ let structured;
2568
+ try {
2569
+ structured = JSON.parse(result.message || '{}');
2570
+ }
2571
+ catch {
2572
+ structured = { exitCode: result.exitCode, message: result.message };
2573
+ }
2574
+ structured.contextFiles = useDeployment.contextFiles;
2575
+ if (rollback)
2576
+ structured.contextRollback = rollback;
2577
+ result = { ...result, message: JSON.stringify(structured, null, 2), rawOutput: true };
2578
+ }
2579
+ else if (rollback?.conflicts.length) {
2580
+ result.message = [result.message, ...rollback.conflicts.map(conflict => `Context rollback conflict: ${conflict.path}: ${conflict.reason}`)].filter(Boolean).join('\n');
2581
+ }
2582
+ }
2583
+ return result;
2550
2584
  }
2551
2585
  finally {
2586
+ journal?.dispose();
2552
2587
  useDeployment = previousDeployment;
2553
2588
  }
2554
2589
  }
@@ -2639,6 +2674,9 @@ export class UseHandler {
2639
2674
  try {
2640
2675
  coreResult = await this.executeCore({ ...ctx, args: coreArgs });
2641
2676
  }
2677
+ catch (error) {
2678
+ coreResult = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
2679
+ }
2642
2680
  finally {
2643
2681
  usePreviewActions = previousPreview;
2644
2682
  if (json || dryRun) {
@@ -2664,12 +2702,18 @@ export class UseHandler {
2664
2702
  else {
2665
2703
  if (coreResult.exitCode === 0 && !dryRun && !VALID_FRAMEWORKS.includes(requestedBundle)) {
2666
2704
  for (const provider of providers) {
2667
- await ensurePostDeployPhases({
2668
- frameworkRoot,
2669
- projectPath: projectDir,
2670
- provider,
2671
- args: remainingArgs,
2672
- });
2705
+ try {
2706
+ await ensurePostDeployPhases({
2707
+ frameworkRoot,
2708
+ projectPath: projectDir,
2709
+ provider,
2710
+ args: remainingArgs,
2711
+ });
2712
+ }
2713
+ catch (error) {
2714
+ coreResult = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
2715
+ break;
2716
+ }
2673
2717
  }
2674
2718
  }
2675
2719
  const providerResults = [];
@@ -4377,7 +4421,7 @@ export class UseHandler {
4377
4421
  skills: paths.skills,
4378
4422
  behaviors: paths.behaviors,
4379
4423
  });
4380
- const ctxResult = await generateContextFiles({
4424
+ const ctxResult = await generateDeploymentContextFiles({
4381
4425
  provider: provider,
4382
4426
  projectPath: target,
4383
4427
  sections,
@@ -4431,10 +4475,8 @@ export class UseHandler {
4431
4475
  }
4432
4476
  }
4433
4477
  catch (err) {
4434
- // Non-fatal: context-pipeline emission must not block deployment.
4435
- // Operator can re-run aiwg use to retry.
4436
4478
  const msg = err instanceof Error ? err.message : String(err);
4437
- console.error(`Warning: context-pipeline emission failed: ${msg}`);
4479
+ return { exitCode: 1, message: `Context generation failed: ${msg}` };
4438
4480
  }
4439
4481
  }
4440
4482
  span.end('use:complete', { framework });
@@ -88,7 +88,16 @@ export function askWithTimeout(rl, prompt, parse, fallback, fallbackLabel, timeo
88
88
  const cleanup = () => {
89
89
  clearTimeout(timer);
90
90
  signal?.removeEventListener('abort', onAbort);
91
+ rl.removeListener('close', onClose);
91
92
  };
93
+ const onClose = () => {
94
+ if (settled)
95
+ return;
96
+ settled = true;
97
+ cleanup();
98
+ resolve(fallback);
99
+ };
100
+ rl.on('close', onClose);
92
101
  if (signal?.aborted) {
93
102
  // Fire synchronously so the caller's try/catch sees the reject without
94
103
  // awaiting a tick.
@@ -112,6 +121,70 @@ export function askWithTimeout(rl, prompt, parse, fallback, fallbackLabel, timeo
112
121
  export async function askString(rl, prompt, fallback = '', signal) {
113
122
  return askWithTimeout(rl, prompt, (a) => a.trim(), fallback, fallback || '(empty)', promptTimeoutMs(), signal);
114
123
  }
124
+ /** Collect a hidden paste through an empty line, with one overall prompt deadline. */
125
+ export function askMultilineSecret(rl, signal) {
126
+ return new Promise((resolve, reject) => {
127
+ const lines = [];
128
+ let length = 0;
129
+ let settled = false;
130
+ const cleanup = () => {
131
+ clearTimeout(timer);
132
+ signal?.removeEventListener('abort', onAbort);
133
+ rl.removeListener('line', onLine);
134
+ rl.removeListener('close', onClose);
135
+ lines.length = 0;
136
+ };
137
+ const finish = (value) => {
138
+ if (settled)
139
+ return;
140
+ settled = true;
141
+ cleanup();
142
+ resolve(value);
143
+ };
144
+ const onLine = (line) => {
145
+ if (!line) {
146
+ finish(lines.join('\n'));
147
+ return;
148
+ }
149
+ length += line.length + (lines.length ? 1 : 0);
150
+ if (length > 16384) {
151
+ settled = true;
152
+ cleanup();
153
+ reject(new Error('Sensitive input exceeds 16384 characters.'));
154
+ return;
155
+ }
156
+ lines.push(line);
157
+ };
158
+ // A closed terminal or timeout discards incomplete input, never echoes it.
159
+ const onClose = () => finish('');
160
+ const onAbort = () => {
161
+ if (settled)
162
+ return;
163
+ settled = true;
164
+ cleanup();
165
+ try {
166
+ rl.close();
167
+ }
168
+ catch { /* already closed */ }
169
+ reject(signal.reason ?? new Error('Aborted'));
170
+ };
171
+ const timeoutMs = promptTimeoutMs();
172
+ const timer = setTimeout(() => {
173
+ if (settled)
174
+ return;
175
+ ui.warn(` No input completed within ${Math.round(timeoutMs / 1000)}s — discarding incomplete sensitive input`);
176
+ finish('');
177
+ }, timeoutMs);
178
+ timer.unref?.();
179
+ rl.on('line', onLine);
180
+ rl.on('close', onClose);
181
+ if (signal?.aborted) {
182
+ onAbort();
183
+ return;
184
+ }
185
+ signal?.addEventListener('abort', onAbort, { once: true });
186
+ });
187
+ }
115
188
  /**
116
189
  * Prompt for a yes/no answer with timeout and default. Answers starting with
117
190
  * 'y' (case-insensitive) are considered yes; everything else is no. On
@@ -13,6 +13,7 @@ import * as path from 'path';
13
13
  import { execFile } from 'child_process';
14
14
  import { promisify } from 'util';
15
15
  import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
16
+ import { helperEnvironment, HELPER_ENV_ALLOWLIST } from '../process/cli-command.mjs';
16
17
  const execFileAsync = promisify(execFile);
17
18
  const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
18
19
  // ===========================
@@ -94,6 +95,7 @@ async function isGitWorktree(projectRoot) {
94
95
  try {
95
96
  const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
96
97
  cwd: projectRoot,
98
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
97
99
  windowsHide: true,
98
100
  });
99
101
  return stdout.trim() === 'true';
@@ -117,6 +119,7 @@ async function resolveCoveredPatterns(projectRoot, patterns, lines) {
117
119
  try {
118
120
  await execFileAsync('git', ['check-ignore', '--quiet', '--', probePath(pattern)], {
119
121
  cwd: projectRoot,
122
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
120
123
  windowsHide: true,
121
124
  });
122
125
  return true;
@@ -239,6 +242,7 @@ export function dropGitignored(targetDir, files) {
239
242
  }
240
243
  const result = boundedSpawnSync('git', ['check-ignore', '-z', '--stdin'], 'quick', {
241
244
  cwd: targetDir,
245
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
242
246
  input: `${files.join('\0')}\0`,
243
247
  encoding: 'utf8',
244
248
  });
@@ -13,6 +13,7 @@ import { existsSync, statSync } from 'fs';
13
13
  import { homedir } from 'os';
14
14
  import { dirname, isAbsolute, relative, resolve } from 'path';
15
15
  import { promisify } from 'util';
16
+ import { helperEnvironment } from '../process/cli-command.mjs';
16
17
  import { getConfigPath, readAiwgConfig, resolveDelivery, resolveRemoteProvider, resolveRemotes, } from './aiwg-config.js';
17
18
  const execFileAsync = promisify(execFile);
18
19
  function expandHome(value) {
@@ -96,7 +97,7 @@ async function readRemoteUrl(repoPath, remote) {
96
97
  if (remote.includes('://') || /^[^@/\s]+@[^:/\s]+:/.test(remote))
97
98
  return remote;
98
99
  try {
99
- const { stdout } = await execFileAsync('git', ['-C', repoPath, 'remote', 'get-url', remote], { encoding: 'utf8' });
100
+ const { stdout } = await execFileAsync('git', ['-C', repoPath, 'remote', 'get-url', remote], { encoding: 'utf8', env: helperEnvironment() });
100
101
  return stdout.trim() || undefined;
101
102
  }
102
103
  catch {
@@ -44,22 +44,15 @@ export class JevDecisionAdapter {
44
44
  this.region = options.region?.trim() ? options.region : null;
45
45
  this.observeResponseBody = options.observeResponseBody;
46
46
  }
47
- /** Used only for non-2xx error bodies, where there is no separate successful parse to stay consistent with. */
48
- observeResponse(body) {
49
- if (!this.observeResponseBody)
50
- return;
51
- let parsed = {};
52
- try {
53
- parsed = asRecord(parseBoundedJson(body));
54
- }
55
- catch { /* Invalid answers still have bounded body evidence. */ }
56
- let usage = emptyUsage();
47
+ /** An error response may still report billed usage and correlation metadata. */
48
+ parseErrorResponse(body) {
49
+ let parsed;
57
50
  try {
58
- usage = normalizeUsage(parsed.usage);
51
+ parsed = parseBoundedJson(body);
59
52
  }
60
- catch { /* Missing usage is unknown, including on auth errors. */ }
61
- this.observeResponseBody(body, { actualModel: typeof parsed.model === 'string' && parsed.model ? parsed.model : null,
62
- usage });
53
+ catch { /* Error classification does not require valid JSON. */ }
54
+ this.observeParsed(body, parsed);
55
+ return parsed;
63
56
  }
64
57
  /**
65
58
  * Derives the observer's model/usage from a value `parseBoundedJson` has already produced (or `undefined`
@@ -81,6 +74,15 @@ export class JevDecisionAdapter {
81
74
  catch { /* Missing usage is unknown, including on auth errors. */ }
82
75
  return { actualModel: typeof record.model === 'string' && record.model ? record.model : null, usage };
83
76
  }
77
+ responseMetadata(parsed, metadata) {
78
+ let body = {};
79
+ try {
80
+ body = asRecord(parsed);
81
+ }
82
+ catch { /* No body correlation metadata. */ }
83
+ const id = metadata.requestId ? null : safeRequestId(body.request_id ?? body.requestId);
84
+ return { ...metadata, ...this.observationMetadataOf(parsed), ...(id ? { requestId: id, requestIdSource: 'body' } : {}) };
85
+ }
84
86
  /** Observes exactly once, from the single parse attempt `parsed` already represents (successful or not). */
85
87
  observeParsed(body, parsed) {
86
88
  this.observeResponseBody?.(body, this.observationMetadataOf(parsed));
@@ -209,22 +211,7 @@ export class JevDecisionAdapter {
209
211
  if (!response.ok) {
210
212
  try {
211
213
  const errorBody = await readBoundedBody(response, signal);
212
- this.observeResponse(errorBody);
213
- if (!metadata.requestId && response.headers.get('content-type')?.includes('json')) {
214
- let parsedError;
215
- try {
216
- parsedError = parseBoundedJson(errorBody);
217
- }
218
- catch {
219
- parsedError = null;
220
- }
221
- if (parsedError && typeof parsedError === 'object' && !Array.isArray(parsedError)) {
222
- const field = parsedError.request_id ?? parsedError.requestId;
223
- const safe = safeRequestId(field);
224
- if (safe)
225
- metadata = { ...metadata, requestId: safe, requestIdSource: 'body' };
226
- }
227
- }
214
+ metadata = this.responseMetadata(this.parseErrorResponse(errorBody), metadata);
228
215
  }
229
216
  catch {
230
217
  if (request.signal.aborted)
@@ -248,6 +235,7 @@ export class JevDecisionAdapter {
248
235
  finally {
249
236
  this.observeParsed(encoded, parsed);
250
237
  }
238
+ metadata = this.responseMetadata(parsed, metadata);
251
239
  if (request.signal.aborted)
252
240
  return externalInterruption(request, 'terminal-response', metadata);
253
241
  if (deadline.aborted)
@@ -353,7 +341,7 @@ export class JevDecisionAdapter {
353
341
  }
354
342
  if (!response.ok) {
355
343
  try {
356
- this.observeResponse(await readBoundedBody(response, signal));
344
+ metadata = this.responseMetadata(this.parseErrorResponse(await readBoundedBody(response, signal)), metadata);
357
345
  }
358
346
  catch {
359
347
  return batchFailure(requests, failure('invalid-output', metadata));
@@ -371,6 +359,7 @@ export class JevDecisionAdapter {
371
359
  finally {
372
360
  this.observeParsed(encoded, parsed);
373
361
  }
362
+ metadata = this.responseMetadata(parsed, metadata);
374
363
  const normalized = normalizeBatchResponse(requests, parsed).map(answer => ({
375
364
  questionId: answer.questionId,
376
365
  observation: { ...answer.observation, ...metadata },
@@ -495,6 +495,11 @@ export async function setupJev(input = {}, options = {}) {
495
495
  next: ['Run aiwg decision ask --question "<q>" --yes-no --json.'],
496
496
  };
497
497
  if (input.verify) {
498
+ // Verify the credential just saved, even when the shell still names an
499
+ // older token. Ordinary ask calls retain their environment precedence.
500
+ const verificationEnv = { ...environment };
501
+ delete verificationEnv.JEV_API_KEY;
502
+ delete verificationEnv.AIWG_DECISION_JEV_API_KEY;
498
503
  const verified = await askDecision({
499
504
  question: 'Is this Jev decision setup verification request well formed?',
500
505
  mode: { kind: 'yes-no' },
@@ -502,7 +507,7 @@ export async function setupJev(input = {}, options = {}) {
502
507
  timeoutMs: 15_000,
503
508
  passes: 1,
504
509
  escalationPasses: 1,
505
- }, options);
510
+ }, { ...options, env: verificationEnv });
506
511
  result.verified = verified.status === 'answered';
507
512
  result.model = verified.model;
508
513
  result.usage = verified.usage;
@@ -57,7 +57,7 @@ export function benchmarkComparison(capture, pricing) {
57
57
  return `| Jev, ${passes} pass${passes === 1 ? '' : 'es'} | ${display(t.input)} | n/a | ${display(t.output)} | ${display(t.input === null || t.output === null ? null : t.input + t.output)} | ${t.latency.toFixed(1)} | ${money(t.cost)} |`;
58
58
  }),
59
59
  `| ${capture.baselineLabel.replace(/[|\r\n]/g, ' ')} | ${display(baseline.input)} | ${display(baseline.cached)} | ${display(baseline.output)} | ${display(baseline.input === null || baseline.output === null ? null : baseline.input + baseline.output)} | ${baseline.latency.toFixed(1)} | ${money(baseline.cost)} |`, '',
60
- capture.rows.some(r => r.baseline.cost || r.jev.some(s => s.cost)) ? 'Captured costs use sourced rates recorded per observation; unknown costs remain unknown.' : pricing ? `Rates: decision.ask.cost.* (${pricing.source ?? 'manual'}, checked ${pricing.checked_at ?? 'unknown'}).` : 'Costs unknown: no explicit decision.ask.cost.* rates.',
60
+ capture.rows.some(r => r.baseline.cost || r.jev.some(s => s.cost)) ? 'Captured costs use provider-reported USD or sourced rates recorded per observation; unknown costs remain unknown.' : pricing ? `Rates: decision.ask.cost.* (${pricing.source ?? 'manual'}, checked ${pricing.checked_at ?? 'unknown'}).` : 'Costs unknown: no explicit decision.ask.cost.* rates.',
61
61
  'Costs use reported usage and explicit pricing assumptions, not subscription invoices.',
62
62
  'Standalone baseline requests do not measure the marginal cost of an inline LLM decision.',
63
63
  ];
@@ -116,6 +116,9 @@ function validCost(sample) {
116
116
  if (sample.cost === undefined)
117
117
  return true;
118
118
  const cost = sample.cost;
119
+ if (cost?.source === 'provider-reported')
120
+ return fields(cost, ['usd', 'price', 'source'])
121
+ && cost.price === null && typeof cost.usd === 'number' && Number.isFinite(cost.usd) && cost.usd >= 0;
119
122
  return fields(cost, ['usd', 'price']) && (cost.price === null ? cost.usd === null
120
123
  : validPrice(cost.price) && cost.price.model === sample.model && cost.usd === tokenCost(sample.usage, cost.price));
121
124
  }
@@ -128,10 +131,18 @@ export async function collectBenchmark(dataset, options) {
128
131
  throw new Error('Positive maxUsd required for live collection.');
129
132
  if (!Number.isInteger(options.passes) || options.passes < 1 || options.passes > MAX_BENCHMARK_PASSES)
130
133
  throw new Error('passes must be 1..20.');
134
+ if (options.jevCapture && options.extendCapture)
135
+ throw new Error('Cannot combine Jev reuse and capture extension.');
131
136
  const cases = splitBenchmarkHoldout(dataset, options.holdout ?? 0.3, options.seed ?? '3291');
132
137
  const prior = options.extendCapture;
133
138
  if (prior)
134
139
  validateBenchmarkCapture(cases, prior);
140
+ const reused = options.jevCapture;
141
+ if (reused) {
142
+ validateBenchmarkCapture(cases, reused);
143
+ if (reused.rows.length !== cases.length || reused.rows.some(row => row.jev.length !== options.passes))
144
+ throw new Error('Reused Jev capture must contain every case with the same pass count.');
145
+ }
135
146
  if (typeof options.baselineLabel !== 'string' || !options.baselineLabel.trim() || options.baselineLabel.length > 200
136
147
  || prior && prior.baselineLabel !== options.baselineLabel)
137
148
  throw new Error('Invalid or changed baseline label.');
@@ -147,7 +158,7 @@ export async function collectBenchmark(dataset, options) {
147
158
  throw new Error('Extension cannot remove existing passes.');
148
159
  const planned = cases.reduce((sum, c) => {
149
160
  const row = rows.find(r => r.id === c.id);
150
- return sum + (row ? 0 : 1) + Math.max(0, options.passes - (row?.jev.length ?? 0));
161
+ return sum + (row ? 0 : 1) + (reused ? 0 : Math.max(0, options.passes - (row?.jev.length ?? 0)));
151
162
  }, 0);
152
163
  if (planned > options.maxCalls)
153
164
  throw new Error(`Insufficient maxCalls: need ${planned}; no providers invoked.`);
@@ -177,7 +188,7 @@ export async function collectBenchmark(dataset, options) {
177
188
  return capture;
178
189
  }
179
190
  const environment = options.env ?? process.env;
180
- if (!options.jev && planned > 0 && cases.some(c => (rows.find(r => r.id === c.id)?.jev.length ?? 0) < options.passes)
191
+ if (!reused && !options.jev && planned > 0 && cases.some(c => (rows.find(r => r.id === c.id)?.jev.length ?? 0) < options.passes)
181
192
  && !(environment.JEV_API_KEY?.trim() || environment.AIWG_DECISION_JEV_API_KEY?.trim()))
182
193
  throw new Error('Live benchmark requires JEV_API_KEY or AIWG_DECISION_JEV_API_KEY in the caller environment.');
183
194
  const jev = options.jev ?? (async (input) => {
@@ -201,8 +212,9 @@ export async function collectBenchmark(dataset, options) {
201
212
  if (!validObservation(sample) || !modeAnswer(c, sample))
202
213
  throw new Error(`Invalid provider observation: ${c.id}`);
203
214
  const price = (baseline ? options.baselinePrices ?? [] : jevPrices).find(p => p.model === sample.model) ?? null;
204
- const usd = price ? tokenCost(sample.usage, price) : null;
205
- sample.cost = { usd, price: structuredClone(price) };
215
+ sample.cost = result.cost?.source === 'provider-reported' && validCost(result)
216
+ ? structuredClone(result.cost) : { usd: price ? tokenCost(sample.usage, price) : null, price: structuredClone(price) };
217
+ const usd = sample.cost.usd;
206
218
  if (usd === null)
207
219
  stopReason = 'unknown-cost';
208
220
  else {
@@ -215,7 +227,8 @@ export async function collectBenchmark(dataset, options) {
215
227
  for (const c of cases) {
216
228
  let row = rows.find(r => r.id === c.id);
217
229
  if (!row) {
218
- row = { id: c.id, baseline: await invoke(c, true), jev: [] };
230
+ row = { id: c.id, baseline: await invoke(c, true),
231
+ jev: structuredClone(reused?.rows.find(r => r.id === c.id).jev ?? []) };
219
232
  rows.push(row);
220
233
  }
221
234
  if (options.onCheckpoint)
@@ -4,7 +4,7 @@ import { matchEvidenceDigest } from '../../gates/stats/index.js';
4
4
  import { CalibrationRegistry, calibrationArtifactDigest } from '../calibration/registry.js';
5
5
  import { validateCaseInventory } from './manifest.js';
6
6
  import { verifyQualificationSplits } from './quality.js';
7
- import { verifyDecisionStagedLoadRecord } from './load.js';
7
+ import { DECISION_STAGED_LOAD_RESULT_VERSIONS, verifyDecisionStagedLoadRecord } from './load.js';
8
8
  import { validateReviewEvaluation } from '../review/validate.js';
9
9
  const range = (prefix, from, to) => Array.from({ length: to - from + 1 }, (_, index) => `${prefix}${String(from + index).padStart(2, '0')}`);
10
10
  export const DECISION_GATE_SUITES = {
@@ -34,7 +34,7 @@ export const DECISION_GATE_SUITES = {
34
34
  export const GATE_ARTIFACT_SCHEMAS = {
35
35
  'immutable-splits': 'decision-binary-benchmark-plan/v2',
36
36
  'calibration-qualified': 'decision-calibration-artifact/v1',
37
- 'load-manifest-qualified': 'decision-load-result/v1',
37
+ 'load-manifest-qualified': 'decision-load-result/v2',
38
38
  'review-decision-recorded': 'decision-qualification-review/v1',
39
39
  };
40
40
  export const GATE_ARTIFACT_FLAGS = Object.keys(GATE_ARTIFACT_SCHEMAS);
@@ -127,7 +127,7 @@ function validCalibration(value, context) {
127
127
  && artifact.metrics.selectiveRisk <= artifact.profile.maximumSelectiveRisk;
128
128
  }
129
129
  function validLoadResult(value, context) {
130
- if (!record(value) || value.schemaVersion !== GATE_ARTIFACT_SCHEMAS['load-manifest-qualified'])
130
+ if (!record(value) || !DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(String(value.schemaVersion)))
131
131
  return false;
132
132
  // Legacy records remain readable, but only completed live v3 evidence bound to
133
133
  // this exact clean source revision can qualify the performance gate.
@@ -177,7 +177,9 @@ export function deriveGateEvidence(manifest, proof = {}) {
177
177
  const ref = manifest.gateArtifacts?.[flag];
178
178
  const versionOk = flag === 'immutable-splits'
179
179
  ? ref !== undefined && IMMUTABLE_SPLIT_VERSIONS.includes(ref.schemaVersion)
180
- : ref !== undefined && ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
180
+ : flag === 'load-manifest-qualified'
181
+ ? ref !== undefined && DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(ref.schemaVersion)
182
+ : ref !== undefined && ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
181
183
  flags[flag] = versionOk && ref !== undefined
182
184
  && typeof ref.artifact === 'string' && ref.artifact.length > 0 && sha256Pattern.test(ref.digest);
183
185
  if (flag === 'load-manifest-qualified') {
@@ -4,7 +4,9 @@ import { JevDecisionAdapter, compileJevQuestion } from '../adapters/jev.js';
4
4
  import { DecisionAdmissionRegistry } from '../admission.js';
5
5
  import { CanonicalJsonByteEstimator } from '../context-plan.js';
6
6
  import { assertContextLiveSource } from '../context-live-qualification.js';
7
+ import { tokenCost } from '../pricing.js';
7
8
  import { admissionControllerLoadDriver, decisionLoadArrivals, compareDecisionLoadBounds, decisionLoadManifestDigest, decisionLoadPolicy, runDecisionLoadHarness, } from './load.js';
9
+ export const DECISION_STAGED_LOAD_RESULT_VERSIONS = ['decision-load-result/v1', 'decision-load-result/v2'];
8
10
  const finite = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
9
11
  const positive = (value) => finite(value) && value > 0;
10
12
  const exactCommit = (value) => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value);
@@ -289,7 +291,9 @@ export async function runDecisionStagedLoad(options) {
289
291
  onObservation: (arrival, attempt, admission, observation) => {
290
292
  const receipt = inFlight.get(`${arrival.ordinal}:${attempt}`);
291
293
  Object.assign(receipt, { settledAtMs: clock.now(), admission: structuredClone(admission), status: observation.status, reason: observation.reason,
292
- usage: observation.usage, ...(observation.httpStatus === undefined ? {} : { httpStatus: observation.httpStatus }) });
294
+ usage: observation.usage, ...(observation.httpStatus === undefined ? {} : { httpStatus: observation.httpStatus }),
295
+ ...(observation.actualModel ? { servedModel: observation.actualModel } : {}),
296
+ ...(observation.requestId ? { requestId: observation.requestId } : {}) });
293
297
  inFlight.delete(`${arrival.ordinal}:${attempt}`);
294
298
  guard.observe(arrival.providerId, observation, receipt.tokens, receipt.costUsd);
295
299
  },
@@ -335,7 +339,7 @@ export async function runDecisionStagedLoad(options) {
335
339
  host: { hostname: hostname(), cpuClass: `${cpus().length} cores; ${cpus()[0]?.model ?? 'unknown'}`, memoryGiB: totalmem() / 2 ** 30,
336
340
  networkPath: options.networkPath, node: process.version, platform: process.platform, arch: process.arch },
337
341
  pricing: options.pricing, stages, receipts, samples, profileHistory: registry.profileHistory('load-harness'), breakerTransitions: transitions,
338
- usage: guard.usage, stop: guard.stop, startedAtMs: start, elapsedMs: samples.at(-1)?.atMs ?? clock.now() - start });
342
+ reservedUsage: guard.usage, stop: guard.stop, startedAtMs: start, elapsedMs: samples.at(-1)?.atMs ?? clock.now() - start });
339
343
  }
340
344
  /** Token-bucket debit (burst plus refill), matching the admission controller's rate semantics. */
341
345
  function bucketDebit(receipts, capacity, periodMs, amount) {
@@ -400,9 +404,9 @@ export function compareDecisionStagedLoad(record) {
400
404
  }
401
405
  }
402
406
  }
403
- atMost('budget.maxUsd.stopAtFraction', m.budget.maxUsd * m.budget.stopAtFraction, record.usage.usd);
404
- atMost('budget.maxCalls', m.budget.maxCalls, record.usage.calls);
405
- atMost('budget.maxTokens', m.budget.maxTokens, record.usage.tokens);
407
+ atMost('budget.maxUsd.stopAtFraction', m.budget.maxUsd * m.budget.stopAtFraction, record.reservedUsage.usd);
408
+ atMost('budget.maxCalls', m.budget.maxCalls, record.reservedUsage.calls);
409
+ atMost('budget.maxTokens', m.budget.maxTokens, record.reservedUsage.tokens);
406
410
  atMost('budget.maxWallClockSeconds', m.budget.maxWallClockSeconds, record.elapsedMs / 1000);
407
411
  atMost('maximumSampleGapMs', m.bounds.maximumSampleGapMs, maximum(record.samples.slice(1).map((s, i) => s.atMs - record.samples[i].atMs)));
408
412
  const endingAt = record.startedAtMs + record.elapsedMs;
@@ -440,10 +444,29 @@ export function compareDecisionStagedLoad(record) {
440
444
  atMost('stop.maximumCrossPrincipalQuotaViolations', 0, violations);
441
445
  return out;
442
446
  }
447
+ function reportedUsage(receipts, pricing) {
448
+ const sum = (values) => values.reduce((total, value) => total === null || value === null ? null : total + value, 0);
449
+ const inputs = receipts.map(r => r.usage.inputTokens);
450
+ const outputs = receipts.map(r => r.usage.outputTokens);
451
+ const totals = receipts.map(r => r.usage.inputTokens === null || r.usage.outputTokens === null ? null : r.usage.inputTokens + r.usage.outputTokens);
452
+ const inputTokens = sum(inputs);
453
+ const outputTokens = sum(outputs);
454
+ // The request is pinned to the priced model; an explicitly different served model has no applicable rate.
455
+ const costs = receipts.map(r => r.usage.inputTokens === null || r.usage.outputTokens === null
456
+ || (r.servedModel !== undefined && r.servedModel !== pricing.jev_model) ? null
457
+ : tokenCost(r.usage, { inputPerMillionUsd: pricing.jev_input_per_mtok, outputPerMillionUsd: pricing.jev_output_per_mtok }));
458
+ const knownSum = (values) => values.length > 0 && values.every(v => v === null)
459
+ ? null : sum(values.filter(v => v !== null));
460
+ return { calls: receipts.length, inputTokens, outputTokens, tokens: inputTokens === null || outputTokens === null ? null : inputTokens + outputTokens,
461
+ usd: sum(costs), incompleteCalls: costs.filter(cost => cost === null).length,
462
+ knownSubtotal: { inputTokens: knownSum(inputs), outputTokens: knownSum(outputs), tokens: knownSum(totals), usd: knownSum(costs) } };
463
+ }
443
464
  export function decisionStagedLoadResultRecord(input) {
444
465
  const manifest = readDecisionLoadManifest(input.manifestBytes);
445
466
  validateDecisionLoadManifestV3(manifest);
446
- const payload = { ...input, schemaVersion: 'decision-load-result/v1', kind: 'load-manifest-qualified',
467
+ validateDecisionLoadPricing(input.pricing, manifest.model);
468
+ const payload = { ...input, schemaVersion: 'decision-load-result/v2', kind: 'load-manifest-qualified',
469
+ reportedUsage: reportedUsage(input.receipts, input.pricing),
447
470
  manifest, manifestDigest: decisionLoadManifestDigest(input.manifestBytes) };
448
471
  for (const key of ['digest', 'comparisons', 'passed', 'queueWaitDistribution', 'retries'])
449
472
  delete payload[key];
@@ -461,7 +484,7 @@ export function decisionStagedLoadResultRecord(input) {
461
484
  export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
462
485
  try {
463
486
  const r = value;
464
- if (!r || r.schemaVersion !== 'decision-load-result/v1' || r.kind !== 'load-manifest-qualified' || r.mode !== 'staged-provider'
487
+ if (!r || !DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(r.schemaVersion) || r.kind !== 'load-manifest-qualified' || r.mode !== 'staged-provider'
465
488
  || r.execution !== 'live' || r.dirty !== false || !exactCommit(r.sourceCommit) || (sourceCommit !== undefined && sourceCommit !== r.sourceCommit)
466
489
  || r.stop !== null || !r.passed || !finite(r.startedAtMs) || !r.host.networkPath || !r.host.cpuClass || !positive(r.host.memoryGiB))
467
490
  return false;
@@ -512,8 +535,17 @@ export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
512
535
  || !positive(receipt.tokens) || !finite(receipt.costUsd) || !finite(receipt.admittedAtMs) || receipt.settledAtMs < receipt.admittedAtMs
513
536
  || receipt.admission.decision !== 'admit' || receipt.costUsd !== price(receipt.tokens, r.pricing)))
514
537
  return false;
515
- if (r.usage.calls !== r.receipts.length || r.usage.tokens !== r.receipts.reduce((sum, receipt) => sum + receipt.tokens, 0)
516
- || Math.abs(r.usage.usd - r.receipts.reduce((sum, receipt) => sum + receipt.costUsd, 0)) > 1e-9)
538
+ const reservedUsage = r.schemaVersion === 'decision-load-result/v1' ? r.usage : r.reservedUsage;
539
+ if (!reservedUsage || ![reservedUsage.calls, reservedUsage.tokens, reservedUsage.usd].every(finite)
540
+ || reservedUsage.calls !== r.receipts.length || reservedUsage.tokens !== r.receipts.reduce((sum, receipt) => sum + receipt.tokens, 0)
541
+ || Math.abs(reservedUsage.usd - r.receipts.reduce((sum, receipt) => sum + receipt.costUsd, 0)) > 1e-9)
542
+ return false;
543
+ if (r.schemaVersion === 'decision-load-result/v2' && r.receipts.some(receipt => !receipt.usage
544
+ || ![receipt.usage.inputTokens, receipt.usage.outputTokens].every(v => v === null || (Number.isSafeInteger(v) && v >= 0))
545
+ || (receipt.usage.inputTokens ?? 0) + (receipt.usage.outputTokens ?? 0) > receipt.tokens
546
+ || (receipt.usage.costUsd !== null && (!finite(receipt.usage.costUsd) || receipt.usage.costUsd > receipt.costUsd + 1e-12))
547
+ || (receipt.servedModel !== undefined && (typeof receipt.servedModel !== 'string' || !receipt.servedModel))
548
+ || (receipt.requestId !== undefined && (typeof receipt.requestId !== 'string' || !receipt.requestId))))
517
549
  return false;
518
550
  if (!r.samples.every((sample, index) => finite(sample.atMs) && finite(sample.residentMemoryMiB) && finite(sample.heapUsedMiB)
519
551
  && finite(sample.cpuPercentOneCore) && finite(sample.active) && finite(sample.queued) && (index === 0 ? sample.atMs === 0 : sample.atMs >= r.samples[index - 1].atMs
@@ -529,7 +561,12 @@ export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
529
561
  || change?.kind !== 'change' || change.profileVersion !== manifest.profileVersion || change.digest !== profileDigest(manifest.workspace, manifest.providers)
530
562
  || rollback?.kind !== 'rollback' || rollback.profileVersion !== initial.profileVersion || rollback.digest !== initial.digest)
531
563
  return false;
532
- const rebuilt = decisionStagedLoadResultRecord(r);
564
+ const rebuilt = decisionStagedLoadResultRecord({ ...r, reservedUsage });
565
+ if (r.schemaVersion === 'decision-load-result/v1') {
566
+ const { reservedUsage: _, reportedUsage: __, digest: ___, ...legacy } = rebuilt;
567
+ const legacyBody = { ...legacy, schemaVersion: r.schemaVersion };
568
+ return rebuilt.passed && canonicalJson({ ...legacyBody, digest: decisionLoadManifestDigest(canonicalJson(legacyBody)) }) === canonicalJson(r);
569
+ }
533
570
  return rebuilt.passed && canonicalJson(rebuilt) === canonicalJson(r);
534
571
  }
535
572
  catch {