@hecer/yoke 1.20.0 → 1.22.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.
Files changed (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +6 -1
  5. package/bench/analyze-codex-comparison.mjs +90 -17
  6. package/bench/compare-codex.mjs +159 -36
  7. package/bench/result-schema.mjs +132 -0
  8. package/canon/manifest.yaml +1 -1
  9. package/dist/agents/pi-telemetry.js +2 -1
  10. package/dist/agents/process-streams.js +12 -64
  11. package/dist/agents/provider-selection.js +12 -0
  12. package/dist/agents/telemetry.js +52 -52
  13. package/dist/change/inbox.js +8 -3
  14. package/dist/check/command.js +69 -17
  15. package/dist/check/delivery.js +121 -0
  16. package/dist/cli.js +86 -8
  17. package/dist/code-intelligence/budgets.js +138 -0
  18. package/dist/code-intelligence/contracts.js +2 -0
  19. package/dist/code-intelligence/coordinator.js +156 -84
  20. package/dist/code-intelligence/evidence.js +87 -34
  21. package/dist/code-intelligence/mcp-client.js +10 -2
  22. package/dist/code-intelligence/mcp-server.js +10 -10
  23. package/dist/dashboard/analytics.js +5 -3
  24. package/dist/goals/command.js +183 -53
  25. package/dist/goals/usage.js +87 -0
  26. package/dist/loop/candidate-cleanup.js +47 -17
  27. package/dist/loop/candidates.js +17 -11
  28. package/dist/loop/cleanup.js +100 -0
  29. package/dist/loop/dispatcher.js +89 -26
  30. package/dist/loop/failure.js +104 -0
  31. package/dist/loop/gate-snapshot.js +19 -0
  32. package/dist/loop/git.js +1 -1
  33. package/dist/loop/loop.js +100 -66
  34. package/dist/loop/parallel-adapters.js +22 -4
  35. package/dist/loop/parallel-command.js +32 -5
  36. package/dist/loop/recovery.js +23 -5
  37. package/dist/loop/reporter.js +21 -4
  38. package/dist/loop/run-command.js +102 -48
  39. package/dist/loop/runner.js +5 -4
  40. package/dist/loop/worker.js +145 -91
  41. package/dist/observability/history.js +1 -1
  42. package/dist/observability/invocation.js +42 -0
  43. package/dist/observability/usage.js +17 -0
  44. package/dist/prd/command.js +20 -7
  45. package/dist/prd/decompose.js +5 -2
  46. package/dist/retrofit/command.js +22 -0
  47. package/dist/retrofit/config.js +26 -1
  48. package/dist/retrofit/gitignore.js +2 -0
  49. package/dist/retrofit/planners/claude.js +4 -4
  50. package/dist/retrofit/wsl.js +23 -3
  51. package/dist/routing/attempts.js +241 -0
  52. package/dist/routing/capability.js +13 -9
  53. package/dist/routing/optimization.js +73 -0
  54. package/dist/routing/registry.js +7 -1
  55. package/dist/routing/router.js +280 -127
  56. package/dist/setup/command.js +54 -6
  57. package/dist/smoke/command.js +302 -74
  58. package/docs/BENCHMARK-MANIFEST.md +131 -0
  59. package/docs/CODE-INTELLIGENCE.md +43 -1
  60. package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
  61. package/docs/DELIVERY-JOURNEYS.md +199 -0
  62. package/docs/ECONOMIC-ROUTING.md +180 -0
  63. package/docs/GOALS.md +61 -4
  64. package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
  65. package/docs/parallel-execution.md +37 -9
  66. package/gemini-extension.json +1 -1
  67. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { realpathSync } from 'node:fs';
5
5
  import { checkProject, checkExitCode, protectAcceptance } from './check/command.js';
6
6
  import { registerProject, listProjects, unregisterProject } from './dashboard/registry.js';
7
7
  import { startDashboard } from './dashboard/server.js';
8
- import { createProjectGoal, readProjectGoal, runProjectGoal, pauseProjectGoal, goalHandoff, budgetProjectGoal, bindProjectGoal } from './goals/command.js';
8
+ import { createProjectGoal, readProjectGoal, runProjectGoal, pauseProjectGoal, goalHandoff, budgetProjectGoal, bindProjectGoal, assessProjectGoal } from './goals/command.js';
9
9
  import { validateCanon } from './canon/validate.js';
10
10
  import { runRetrofit } from './retrofit/command.js';
11
11
  import { setLoopEnabled, loopStatus, runLoopCommand } from './loop/run-command.js';
@@ -17,7 +17,7 @@ import { runNew } from './new/command.js';
17
17
  import { runPrdDraft, runPrdCheck } from './prd/command.js';
18
18
  import { runPrdAssess } from './prd/assess.js';
19
19
  import { runPrdDecompose } from './prd/decompose.js';
20
- import { runLoopCleanup } from './loop/cleanup.js';
20
+ import { runLoopCleanup, pruneWorktrees, pruneFleetWorktrees, listWorktrees, listFleetWorktrees } from './loop/cleanup.js';
21
21
  import { runFlowSmoke } from './smoke/command.js';
22
22
  import { maybeNotifyUpdate, currentYokeVersion } from './update/check.js';
23
23
  import { runUpgrade } from './update/upgrade.js';
@@ -62,6 +62,22 @@ export function runDesignScan(targetDir, opts) {
62
62
  console.log(`${label} — ✓`);
63
63
  return 0;
64
64
  }
65
+ export function parseKeyValueFlags(args, flagName) {
66
+ const result = {};
67
+ for (const arg of args) {
68
+ if (arg.startsWith(`--${flagName}=`)) {
69
+ const raw = arg.slice(flagName.length + 3);
70
+ const eqIdx = raw.indexOf(':');
71
+ if (eqIdx > 0) {
72
+ const key = raw.slice(0, eqIdx).trim();
73
+ const val = raw.slice(eqIdx + 1).trim();
74
+ if (key && val)
75
+ result[key] = val;
76
+ }
77
+ }
78
+ }
79
+ return result;
80
+ }
65
81
  export function parseQualityFlags(args) {
66
82
  const quality = args.includes('--quality') ? true : args.includes('--no-quality') ? false : undefined;
67
83
  const qualityUnbounded = args.includes('--quality-unbounded') || undefined;
@@ -185,9 +201,16 @@ export function main(argv) {
185
201
  console.error('Invalid --model-provider (expected deepseek,kimi)');
186
202
  return 1;
187
203
  }
204
+ const runnerModel = rest.find(a => a.startsWith('--runner-model='))?.slice('--runner-model='.length);
205
+ const runnerReasoning = rest.find(a => a.startsWith('--runner-reasoning='))?.slice('--runner-reasoning='.length);
206
+ const agentModels = parseKeyValueFlags(rest, 'model');
207
+ const agentReasoning = parseKeyValueFlags(rest, 'reasoning');
208
+ const cleanWorktrees = rest.includes('--clean-worktrees');
188
209
  return runSetup(targetDir, {
189
210
  modelProviders: modelProviders,
190
211
  host: hostArg, agents, runner: runnerArg,
212
+ runnerModel, runnerReasoning, agentModels, agentReasoning, cleanWorktrees,
213
+ configureModels: rest.includes('--configure-models'),
191
214
  codeGraph: graphArg,
192
215
  codeIntelligence: intelligenceArg,
193
216
  loop, routing, decisionPolicy: policyArg,
@@ -218,6 +241,47 @@ export function main(argv) {
218
241
  return 2;
219
242
  }
220
243
  }
244
+ case 'worktrees': {
245
+ const sub = rest[0] ?? 'list';
246
+ const targetDir = rest.slice(1).find(a => !a.startsWith('-')) ?? '.';
247
+ const all = rest.includes('--all');
248
+ const force = rest.includes('--force');
249
+ if (sub === 'list') {
250
+ if (all) {
251
+ const fleet = listFleetWorktrees();
252
+ for (const [name, list] of Object.entries(fleet)) {
253
+ console.log(`[${name}] ${list.length} worktree(s):`);
254
+ for (const wt of list)
255
+ console.log(` ${wt}`);
256
+ }
257
+ return 0;
258
+ }
259
+ const list = listWorktrees(targetDir);
260
+ console.log(`Worktrees in ${targetDir} (${list.length}):`);
261
+ for (const wt of list)
262
+ console.log(` ${wt}`);
263
+ return 0;
264
+ }
265
+ if (sub === 'prune') {
266
+ if (all) {
267
+ const fleet = pruneFleetWorktrees({ force });
268
+ let totalRemoved = 0;
269
+ for (const [name, res] of Object.entries(fleet)) {
270
+ if (res.removed.length > 0 || res.failed.length > 0) {
271
+ console.log(`[${name}] Removed: ${res.removed.length}, Failed: ${res.failed.length}`);
272
+ totalRemoved += res.removed.length;
273
+ }
274
+ }
275
+ console.log(`Fleet prune complete: ${totalRemoved} worktree(s) removed.`);
276
+ return 0;
277
+ }
278
+ const res = pruneWorktrees(targetDir, { force });
279
+ console.log(`Pruned ${res.removed.length} worktree(s) in ${targetDir}${res.failed.length > 0 ? ` (${res.failed.length} failed)` : ''}.`);
280
+ return res.failed.length === 0 ? 0 : 1;
281
+ }
282
+ console.log('usage: yoke worktrees <list|prune> [dir] [--all] [--force]');
283
+ return 1;
284
+ }
221
285
  case 'dashboard': {
222
286
  const port = rest.find(a => a.startsWith('--port='))?.slice('--port='.length);
223
287
  if (port !== undefined && (!Number.isInteger(Number(port)) || Number(port) < 0 || Number(port) > 65535)) {
@@ -275,6 +339,15 @@ export function main(argv) {
275
339
  console.log('Pause requested at next safe boundary');
276
340
  return 0;
277
341
  }
342
+ if (sub === 'assess') {
343
+ const provider = value('runner');
344
+ if (provider && !SUPPORTED_AGENTS.includes(provider))
345
+ throw new Error('Unknown runner');
346
+ return assessProjectGoal(targetDir, { provider: provider, selection: { model: value('model'), reasoningEffort: value('effort'), bare: rest.includes('--bare') ? true : undefined } }).then(result => {
347
+ console.log(JSON.stringify(result));
348
+ return result.assessed ? 0 : 1;
349
+ }).catch(error => { console.error(`Goal assessment: ${error.message}`); return 2; });
350
+ }
278
351
  if (sub === 'run' || sub === 'resume') {
279
352
  const provider = value('runner');
280
353
  if (provider && !SUPPORTED_AGENTS.includes(provider))
@@ -284,7 +357,7 @@ export function main(argv) {
284
357
  return goal.status === 'complete' ? 0 : 1;
285
358
  }).catch(error => { console.error(`Goal: ${error.message}`); return 2; });
286
359
  }
287
- throw new Error('Use goal set|bind|status|run|resume|pause|handoff|budget');
360
+ throw new Error('Use goal set|bind|assess|status|run|resume|pause|handoff|budget');
288
361
  }
289
362
  catch (error) {
290
363
  console.error(`Goal: ${error.message}`);
@@ -319,6 +392,11 @@ export function main(argv) {
319
392
  case 'retrofit': {
320
393
  const targetDir = rest.find(a => !a.startsWith('-')) ?? '.';
321
394
  const loop = rest.includes('--loop');
395
+ const cleanWorktrees = rest.includes('--clean-worktrees');
396
+ const runnerModel = rest.find(a => a.startsWith('--runner-model='))?.slice('--runner-model='.length);
397
+ const runnerReasoning = rest.find(a => a.startsWith('--runner-reasoning='))?.slice('--runner-reasoning='.length);
398
+ const agentModels = parseKeyValueFlags(rest, 'model');
399
+ const agentReasoning = parseKeyValueFlags(rest, 'reasoning');
322
400
  const agentArg = rest.find(a => a.startsWith('--agent='))?.slice('--agent='.length);
323
401
  const all = [...SUPPORTED_AGENTS];
324
402
  const agents = !agentArg || agentArg === 'all'
@@ -338,7 +416,7 @@ export function main(argv) {
338
416
  console.error(`Invalid --code-intelligence value: ${ciArg} (expected off|shadow|active)`);
339
417
  return 1;
340
418
  }
341
- return runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence: ciArg });
419
+ return runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence: ciArg, cleanWorktrees, runnerModel, runnerReasoning, agentModels, agentReasoning });
342
420
  }
343
421
  case 'change': {
344
422
  const sub = rest[0];
@@ -387,7 +465,7 @@ export function main(argv) {
387
465
  return 0;
388
466
  }
389
467
  if (sub === 'status') {
390
- console.log(loopStatus(targetDir));
468
+ console.log(loopStatus(targetDir, undefined, { compact: rest.includes('--compact') }));
391
469
  return 0;
392
470
  }
393
471
  if (sub === 'pause') {
@@ -618,7 +696,7 @@ export function main(argv) {
618
696
  }
619
697
  return runLoopCommand(targetDir, { maxIterations: rawMax, agent, isolate, resumeWorktree: rest.includes('--resume-worktree'), parallel, parallelAuto: parallelArg === '--parallel=auto', reviewer, review, allowSelfReview, timeoutMinutes, json, routing, onAmbiguity: oaArg, decisionPolicy: dpArg, permissions, ...(explore ? { explore: true } : {}), ...(exploreIntervalMinutes !== undefined ? { exploreIntervalMinutes } : {}), ...(exploreLimit.milliseconds !== undefined ? { exploreLimitMs: exploreLimit.milliseconds } : {}), ...qualityFlags.options });
620
698
  }
621
- console.log(`usage: yoke loop <on|off|status|pause|decision|answer|resume [--discard] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
699
+ console.log(`usage: yoke loop <on|off|status [--compact]|pause|decision|answer|resume [--discard] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N]|cleanup [--remove-worktrees] [--discard-stale-recovery]|run [--max=N] [--explore] [--explore-interval=<minutes>] [--explore-limit=<Nh|Nd|Nw>] [--parallel=<auto|N>] [--runner=<${AGENT_LIST}>] [--reviewer=<${AGENT_LIST}>] [--review] [--allow-self-review] [--routing|--no-routing] [--isolate|--no-isolate] [--unsafe] [--timeout=<minutes>] [--decision-policy=<auto|critical>] [--quality|--no-quality] [--quality-rounds=N] [--quality-minutes=N] [--quality-policy=<blocking|advisory>] [--quality-unbounded] [--candidates=N] [--json]> [targetDir]`);
622
700
  return 1;
623
701
  }
624
702
  case 'new': {
@@ -763,8 +841,8 @@ export function main(argv) {
763
841
  case 'upgrade':
764
842
  return runUpgrade();
765
843
  default:
766
- console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
767
- console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--loop] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess|decompose> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
844
+ console.log('Project workflows: yoke check [dir] [--json|--protect] | goal set|bind|assess|run|resume|pause|status|handoff|budget [dir] | projects add|list|remove | dashboard [dir] [--port=N]');
845
+ console.log(`usage: yoke <setup [dir] | new <dir> [--idea="..."] | validate [canonDir] | retrofit [targetDir] [--agent=${AGENT_LIST}|all] [--code-graph=graphify|serena] [--code-intelligence=off|shadow|active] [--clean-worktrees] [--runner-model=<model>] [--runner-reasoning=<effort>] [--model=<agent>:<model>] [--reasoning=<agent>:<effort>] [--loop] | worktrees <list|prune> [dir] [--all] [--force] | change <add|status> [dir] | code-intelligence-server --workspace=<dir> --mode=<mode> | prd <draft|check|assess|decompose> [dir] | loop <on|off|status|decision|answer|resume|run|cleanup> | context <init|status> | review [dir] | design-scan [dir] | flow-smoke [dir] | upgrade>`);
768
846
  return cmd ? 1 : 0;
769
847
  }
770
848
  }
@@ -0,0 +1,138 @@
1
+ import { shorten } from './evidence.js';
2
+ const TRUNCATED = 'BUDGET_EXCEEDED: result truncated; coverage is partial.';
3
+ /** The provider/tokenizer is unknown to this facade. Count one budget unit per
4
+ * UTF-8 byte conservatively, including the response's own metrics. This is not
5
+ * provider-measured token usage. JSON-RPC transport framing is outside this
6
+ * structuredContent response and is not included. */
7
+ export function measureResponse(response) {
8
+ response.metrics.token_count_kind = 'estimated';
9
+ response.metrics.token_count_method = 'utf8_bytes_conservative';
10
+ for (let i = 0; i < 10; i++) {
11
+ const bytes = Buffer.byteLength(JSON.stringify(response), 'utf8');
12
+ if (response.metrics.returned_bytes === bytes && response.metrics.result_tokens === bytes)
13
+ return response;
14
+ response.metrics.returned_bytes = bytes;
15
+ response.metrics.result_tokens = bytes;
16
+ }
17
+ return response;
18
+ }
19
+ export function responseByteLimit(limits) {
20
+ return Math.max(0, Math.floor(Math.min(limits.tokenBudget, limits.maxBytes)));
21
+ }
22
+ /** This fixed-size failure is the only exception when a caller's budget cannot
23
+ * even represent the required error envelope. It never echoes backend content. */
24
+ export function budgetError(response) {
25
+ return measureResponse({
26
+ schema_version: response.schema_version, request_id: shorten(response.request_id, 64), workspace_id: shorten(response.workspace_id, 128), snapshot_id: shorten(response.snapshot_id, 128),
27
+ status: 'error', data: null,
28
+ coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' },
29
+ provenance: [], warnings: [], metrics: { latency_ms: response.metrics.latency_ms, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null,
30
+ error: { code: 'BUDGET_EXCEEDED', message: 'Budget cannot represent the mandatory response envelope.' },
31
+ });
32
+ }
33
+ function markPartial(response) {
34
+ if (response.status === 'success')
35
+ response.status = 'partial';
36
+ for (const key of ['structural', 'semantic', 'documents'])
37
+ if (response.coverage[key] !== 'unavailable')
38
+ response.coverage[key] = 'partial';
39
+ }
40
+ function usedEvidence(value, ids = new Set()) {
41
+ if (Array.isArray(value))
42
+ for (const entry of value)
43
+ usedEvidence(entry, ids);
44
+ else if (value && typeof value === 'object')
45
+ for (const [key, entry] of Object.entries(value)) {
46
+ if (key === 'evidence_ids' && Array.isArray(entry)) {
47
+ for (const id of entry)
48
+ if (typeof id === 'string')
49
+ ids.add(id);
50
+ }
51
+ else
52
+ usedEvidence(entry, ids);
53
+ }
54
+ return ids;
55
+ }
56
+ function pruneProvenance(response) {
57
+ const ids = usedEvidence(response.data);
58
+ response.provenance = response.provenance.filter(item => item.evidence_id && ids.has(item.evidence_id));
59
+ }
60
+ function shortenLargestText(value) {
61
+ const fields = [];
62
+ function visit(node) {
63
+ if (Array.isArray(node))
64
+ for (const item of node)
65
+ visit(item);
66
+ else if (node && typeof node === 'object')
67
+ for (const [key, entry] of Object.entries(node)) {
68
+ if (['excerpt', 'signature', 'message', 'label'].includes(key) && typeof entry === 'string' && entry.length > 64)
69
+ fields.push({ object: node, key, value: entry });
70
+ else if (entry && typeof entry === 'object')
71
+ visit(entry);
72
+ }
73
+ }
74
+ visit(value);
75
+ const field = fields.sort((a, b) => Buffer.byteLength(b.value) - Buffer.byteLength(a.value))[0];
76
+ if (!field)
77
+ return false;
78
+ field.object[field.key] = shorten(field.value, Math.max(63, Math.floor(field.value.length / 2))) + '…';
79
+ return true;
80
+ }
81
+ function removeResultEntry(value) {
82
+ if (!value || typeof value !== 'object' || Array.isArray(value))
83
+ return false;
84
+ const data = value;
85
+ // All facade result collections remain arrays; identifiers and operation
86
+ // receipts stay intact. Never replace typed data with a prose excerpt.
87
+ const keys = ['items', 'symbols', 'references', 'diagnostics', 'nodes', 'edges', 'semantic_findings', 'structural_candidates', 'documents', 'suggested_test_paths', 'unresolved', 'changed_paths'];
88
+ const candidates = keys.filter(key => Array.isArray(data[key]) && data[key].length > 0);
89
+ candidates.sort((a, b) => Buffer.byteLength(JSON.stringify(data[b].at(-1))) - Buffer.byteLength(JSON.stringify(data[a].at(-1))));
90
+ const key = candidates[0];
91
+ if (!key)
92
+ return false;
93
+ data[key].pop();
94
+ if (key === 'nodes') {
95
+ const ids = new Set(data.nodes.map((node) => node.id));
96
+ data.edges = data.edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to));
97
+ data.frontier_remaining = (data.frontier_remaining ?? 0) + 1;
98
+ data.traversal_complete = false;
99
+ }
100
+ else if (key === 'edges') {
101
+ data.frontier_remaining = (data.frontier_remaining ?? 0) + 1;
102
+ data.traversal_complete = false;
103
+ }
104
+ return true;
105
+ }
106
+ /** Bound the complete structured response, not just result.data. Retained data
107
+ * continues to satisfy its shape, and retained evidence links stay resolvable. */
108
+ export function fitResponse(response, limits) {
109
+ const limit = responseByteLimit(limits);
110
+ if (measureResponse(response).metrics.returned_bytes <= limit)
111
+ return response;
112
+ markPartial(response);
113
+ response.warnings = [TRUNCATED, ...response.warnings.filter(item => item !== TRUNCATED).map(item => shorten(item, 256))];
114
+ pruneProvenance(response);
115
+ while (measureResponse(response).metrics.returned_bytes > limit) {
116
+ if (shortenLargestText(response.data))
117
+ continue;
118
+ if (removeResultEntry(response.data)) {
119
+ pruneProvenance(response);
120
+ continue;
121
+ }
122
+ if (response.warnings.length > 1) {
123
+ response.warnings = [TRUNCATED, 'Additional warnings omitted.'];
124
+ if (measureResponse(response).metrics.returned_bytes <= limit)
125
+ break;
126
+ response.warnings = [TRUNCATED];
127
+ continue;
128
+ }
129
+ // A long diagnostic is allowed to shrink, but error codes and rejection
130
+ // status survive; an exhausted error body becomes the bounded budget error.
131
+ if (response.error && response.error.message.length > 64) {
132
+ response.error.message = shorten(response.error.message, 63) + '…';
133
+ continue;
134
+ }
135
+ return budgetError(response);
136
+ }
137
+ return response;
138
+ }
@@ -5,6 +5,7 @@ export const StatusSchema = z.enum(['success', 'partial', 'blocked', 'error']);
5
5
  export const ResolutionSchema = z.enum(['resolved', 'unresolved', 'ambiguous', 'not_applicable']);
6
6
  export const FreshnessSchema = z.enum(['current', 'stale', 'unknown']);
7
7
  export const ProvenanceSchema = z.object({
8
+ evidence_id: z.string().min(1).optional(),
8
9
  backend: BackendNameSchema,
9
10
  version: z.string().min(1),
10
11
  source_path: z.string().nullable(),
@@ -26,6 +27,7 @@ export const MetricsSchema = z.object({
26
27
  latency_ms: z.number().nonnegative(),
27
28
  result_tokens: z.number().int().nonnegative(),
28
29
  token_count_kind: z.enum(['exact', 'estimated']),
30
+ token_count_method: z.literal('utf8_bytes_conservative').optional(),
29
31
  returned_bytes: z.number().int().nonnegative(),
30
32
  });
31
33
  export const ErrorSchema = z.object({