@hecer/yoke 1.21.1 → 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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +27 -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 +12 -3
  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/dispatcher.js +89 -26
  29. package/dist/loop/failure.js +104 -0
  30. package/dist/loop/gate-snapshot.js +19 -0
  31. package/dist/loop/git.js +1 -1
  32. package/dist/loop/loop.js +100 -66
  33. package/dist/loop/parallel-adapters.js +22 -4
  34. package/dist/loop/parallel-command.js +32 -5
  35. package/dist/loop/recovery.js +23 -5
  36. package/dist/loop/reporter.js +21 -4
  37. package/dist/loop/run-command.js +95 -46
  38. package/dist/loop/runner.js +5 -4
  39. package/dist/loop/worker.js +145 -91
  40. package/dist/observability/history.js +1 -1
  41. package/dist/observability/invocation.js +42 -0
  42. package/dist/observability/usage.js +17 -0
  43. package/dist/prd/command.js +20 -7
  44. package/dist/prd/decompose.js +5 -2
  45. package/dist/retrofit/config.js +26 -1
  46. package/dist/retrofit/gitignore.js +2 -0
  47. package/dist/routing/attempts.js +241 -0
  48. package/dist/routing/capability.js +13 -9
  49. package/dist/routing/optimization.js +73 -0
  50. package/dist/routing/registry.js +7 -1
  51. package/dist/routing/router.js +280 -127
  52. package/dist/setup/command.js +8 -2
  53. package/dist/smoke/command.js +302 -74
  54. package/docs/BENCHMARK-MANIFEST.md +131 -0
  55. package/docs/CODE-INTELLIGENCE.md +43 -1
  56. package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
  57. package/docs/DELIVERY-JOURNEYS.md +199 -0
  58. package/docs/ECONOMIC-ROUTING.md +180 -0
  59. package/docs/GOALS.md +61 -4
  60. package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
  61. package/docs/parallel-execution.md +37 -9
  62. package/gemini-extension.json +1 -1
  63. package/package.json +1 -1
@@ -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({
@@ -3,19 +3,18 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { ContextInputSchema, EditApplyInputSchema, EditPreviewInputSchema, ImpactInputSchema, SymbolInputSchema, TraceInputSchema, } from './contracts.js';
5
5
  import { createGraftAdapter, createGraphifyAdapter, createSerenaAdapter } from './adapters/index.js';
6
- import { normalizeEdges, normalizeItems, normalizeSymbols } from './evidence.js';
7
- import { assertSnapshotCurrent, createSnapshot, loadSnapshot, workspaceId } from './snapshots.js';
6
+ import { normalizeEdges, normalizeItems, normalizeSymbols, normalizeReferenceEdges, serenaSymbolId, shorten } from './evidence.js';
7
+ import { budgetError, fitResponse, measureResponse, responseByteLimit } from './budgets.js';
8
+ import { assertSafePath, assertSnapshotCurrent, createSnapshot, loadSnapshot, workspaceId } from './snapshots.js';
8
9
  import { applyEditPlan } from './transactions.js';
9
10
  import { createEditPlan, loadEditPlan } from './edit-plans.js';
10
11
  function nowMs() { return Date.now(); }
11
- function compact(value, maxBytes) {
12
- const encoded = JSON.stringify(value) ?? '';
13
- if (Buffer.byteLength(encoded) <= maxBytes)
14
- return value;
15
- return { truncated: true, excerpt: encoded.slice(0, Math.max(0, maxBytes - 64)) };
16
- }
17
12
  function statusFor(missing, hard = false) { return hard ? 'error' : missing.length ? 'partial' : 'success'; }
18
13
  function errorCode(message) {
14
+ if (/deadline|timed out/iu.test(message))
15
+ return 'BACKEND_TIMEOUT';
16
+ if (/budget/iu.test(message))
17
+ return 'BUDGET_EXCEEDED';
19
18
  if (/outside|workspace|symbolic/u.test(message))
20
19
  return 'WORKSPACE_DENIED';
21
20
  if (/stale/u.test(message))
@@ -34,6 +33,8 @@ function errorCode(message) {
34
33
  return 'SCOPE_VIOLATION';
35
34
  return 'INVALID_ARGUMENT';
36
35
  }
36
+ const INPUT_SCHEMAS = { code_context: ContextInputSchema, code_symbol: SymbolInputSchema, code_trace: TraceInputSchema, code_impact: ImpactInputSchema, code_edit_preview: EditPreviewInputSchema, code_edit_apply: EditApplyInputSchema };
37
+ const SYNCHRONOUS_DEADLINE = 'Request deadline exceeded during synchronous processing; retained receipts describe completed operations.';
37
38
  function symbolArgs(symbol, fallbackPath = '') {
38
39
  if (symbol.startsWith('serena:')) {
39
40
  try {
@@ -64,47 +65,75 @@ export class CodeIntelligenceCoordinator {
64
65
  async dispatch(name, input) {
65
66
  const requestId = randomUUID();
66
67
  const started = nowMs();
67
- const suppliedWorkspace = typeof input === 'object' && input !== null && typeof input.workspace_id === 'string' ? input.workspace_id : this.workspace_id;
68
68
  let snapshot;
69
+ let limits = { tokenBudget: this.options.limits?.tokenBudget ?? 2400, maxBytes: this.options.limits?.maxBytes ?? 2_000_000 };
69
70
  try {
70
- snapshot = this.snapshotFor(input);
71
- }
72
- catch (error) {
73
- return this.errorResponse(requestId, suppliedWorkspace, this.safeSnapshotId(), started, error);
74
- }
75
- try {
71
+ const schema = INPUT_SCHEMAS[name];
72
+ if (!schema)
73
+ throw new Error(`unknown code-intelligence tool: ${shorten(name, 64)}`);
74
+ const parsed = schema.parse(input);
75
+ if ('token_budget' in parsed)
76
+ limits = { ...limits, tokenBudget: Math.min(limits.tokenBudget, parsed.token_budget) };
77
+ const timeout = Math.min(this.options.limits?.timeoutMs ?? 10000, 'timeout_ms' in parsed ? parsed.timeout_ms : 10000);
78
+ const state = { used: [], missing: [], warnings: [], deadline: started + timeout, limits };
79
+ const minimum = budgetError(this.errorResponse(requestId, this.workspace_id, '0'.repeat(64), started, new Error('budget')));
80
+ if (minimum.metrics.returned_bytes > responseByteLimit(limits))
81
+ return minimum;
82
+ snapshot = this.snapshotFor(parsed);
83
+ state.receiptFits = data => {
84
+ const envelope = {
85
+ ...this.errorResponse(requestId, this.workspace_id, snapshot.snapshot_id, started, new Error('budget')),
86
+ status: 'partial', data, error: null, warnings: [SYNCHRONOUS_DEADLINE],
87
+ };
88
+ // Reserve a late-operation warning and the maximum finite latency width
89
+ // before a transaction can mutate. Apply returns this exact public shape.
90
+ envelope.metrics.latency_ms = Number.MAX_SAFE_INTEGER;
91
+ return measureResponse(envelope).metrics.returned_bytes <= responseByteLimit(limits);
92
+ };
76
93
  let result;
77
94
  switch (name) {
78
95
  case 'code_context':
79
- result = await this.context(ContextInputSchema.parse(input), snapshot);
96
+ result = await this.context(parsed, snapshot, state);
80
97
  break;
81
98
  case 'code_symbol':
82
- result = await this.symbol(SymbolInputSchema.parse(input), snapshot);
99
+ result = await this.symbol(parsed, snapshot, state);
83
100
  break;
84
101
  case 'code_trace':
85
- result = await this.trace(TraceInputSchema.parse(input), snapshot);
102
+ result = await this.trace(parsed, snapshot, state);
86
103
  break;
87
104
  case 'code_impact':
88
- result = await this.impact(ImpactInputSchema.parse(input), snapshot);
105
+ result = await this.impact(parsed, snapshot, state);
89
106
  break;
90
107
  case 'code_edit_preview':
91
- result = await this.preview(EditPreviewInputSchema.parse(input), snapshot);
108
+ result = await this.preview(parsed, snapshot, state);
92
109
  break;
93
110
  case 'code_edit_apply':
94
- result = await this.apply(EditApplyInputSchema.parse(input), snapshot);
111
+ result = await this.apply(parsed, snapshot, state);
95
112
  break;
96
113
  default: throw new Error(`unknown code-intelligence tool: ${name}`);
97
114
  }
98
- const bytes = Buffer.byteLength(JSON.stringify(result.data) ?? '');
99
- const maxBytes = this.options.limits?.maxBytes ?? 2_000_000;
100
- return {
115
+ const read = !name.startsWith('code_edit_');
116
+ if (read)
117
+ result.warnings.push('Backend freshness and exhaustive coverage are unverified; candidates may require source checks.');
118
+ let response = fitResponse({
101
119
  schema_version: '0.1.0', request_id: requestId, workspace_id: this.workspace_id, snapshot_id: snapshot.snapshot_id,
102
- status: statusFor(result.missing, result.hard), data: compact(result.data, maxBytes), coverage: this.coverage(result.used, result.missing), provenance: result.provenance,
103
- warnings: result.warnings, metrics: { latency_ms: nowMs() - started, result_tokens: Math.ceil(bytes / 4), token_count_kind: 'estimated', returned_bytes: Math.min(bytes, maxBytes) }, artifact_uri: null, error: null,
104
- };
120
+ status: result.hard ? 'error' : read ? 'partial' : statusFor(result.missing), data: result.data, coverage: this.coverage(result.used, result.missing), provenance: result.provenance,
121
+ warnings: [...new Set(result.warnings)], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null, error: null,
122
+ }, limits);
123
+ response.metrics.latency_ms = nowMs() - started;
124
+ response = fitResponse(response, limits);
125
+ // A filesystem operation or normalization cannot be preempted mid-call.
126
+ // Check after reduction too, and never hide a receipt for an applied edit.
127
+ if (nowMs() >= state.deadline && response.status !== 'error' && response.status !== 'blocked') {
128
+ response.status = 'partial';
129
+ response.warnings.push(SYNCHRONOUS_DEADLINE);
130
+ response.metrics.latency_ms = nowMs() - started;
131
+ response = fitResponse(response, limits);
132
+ }
133
+ return response;
105
134
  }
106
135
  catch (error) {
107
- return this.errorResponse(requestId, this.workspace_id, snapshot.snapshot_id, started, error);
136
+ return fitResponse(this.errorResponse(requestId, this.workspace_id, snapshot?.snapshot_id ?? '0'.repeat(64), started, error), limits);
108
137
  }
109
138
  }
110
139
  snapshotFor(input) {
@@ -116,23 +145,15 @@ export class CodeIntelligenceCoordinator {
116
145
  assertSnapshotCurrent(this.root, snapshot, this.options.policy);
117
146
  return snapshot;
118
147
  }
119
- safeSnapshotId() {
120
- try {
121
- return createSnapshot(this.root, this.options.policy, this.workspace_id).snapshot_id;
122
- }
123
- catch {
124
- return '0'.repeat(64);
125
- }
126
- }
127
148
  coverage(used, missing) {
128
149
  return {
129
150
  backends_requested: [...new Set([...used, ...missing])], backends_used: [...new Set(used)], backends_missing: [...new Set(missing)],
130
- structural: used.includes('graft') ? 'complete' : 'unavailable',
131
- semantic: used.includes('serena-lsp') ? 'complete' : 'unavailable',
132
- documents: used.includes('graphify') ? 'complete' : 'unavailable',
151
+ structural: used.includes('graft') ? 'partial' : 'unavailable',
152
+ semantic: used.includes('serena-lsp') ? 'partial' : 'unavailable',
153
+ documents: used.includes('graphify') ? 'partial' : 'unavailable',
133
154
  };
134
155
  }
135
- async call(backend, tool, args, timeout, state) {
156
+ async call(backend, tool, args, timeout, state, adapter = this.adapters[backend]) {
136
157
  const seen = new Set([...state.used, ...state.missing]);
137
158
  const maxBackends = this.options.limits?.maxBackends ?? 3;
138
159
  if (!seen.has(backend) && seen.size >= maxBackends) {
@@ -140,8 +161,17 @@ export class CodeIntelligenceCoordinator {
140
161
  state.warnings.push(`${backend}: backend budget exceeded`);
141
162
  return null;
142
163
  }
164
+ let timer;
143
165
  try {
144
- const value = await this.adapters[backend].call({ tool, arguments: args }, Math.min(timeout, this.options.limits?.timeoutMs ?? timeout));
166
+ const remaining = Math.min(timeout, state.deadline - nowMs());
167
+ if (remaining <= 0)
168
+ throw new Error('request deadline exceeded; backend was not started');
169
+ const expired = new Promise((_resolve, reject) => {
170
+ timer = setTimeout(() => { reject(new Error('request deadline exceeded')); void adapter.close().catch(() => { }); }, remaining);
171
+ });
172
+ const value = await Promise.race([adapter.call({ tool, arguments: args }, remaining), expired]);
173
+ if (nowMs() >= state.deadline)
174
+ throw new Error('request deadline exceeded; late backend result discarded');
145
175
  if (!state.used.includes(backend))
146
176
  state.used.push(backend);
147
177
  return value;
@@ -149,35 +179,39 @@ export class CodeIntelligenceCoordinator {
149
179
  catch (error) {
150
180
  if (!state.missing.includes(backend))
151
181
  state.missing.push(backend);
152
- state.warnings.push(`${backend}: ${error.message}`);
182
+ state.warnings.push(`${backend}: ${shorten(error.message, 256)}`);
153
183
  return null;
154
184
  }
185
+ finally {
186
+ if (timer)
187
+ clearTimeout(timer);
188
+ }
155
189
  }
156
190
  evidenceOptions(snapshot, backend, maxChars = 12000) {
157
191
  return { root: this.root, snapshot, adapter: this.adapters[backend], maxChars };
158
192
  }
159
- async context(input, snapshot) {
160
- const state = { used: [], missing: [], warnings: [] };
193
+ async context(input, snapshot, state) {
161
194
  const provenance = [];
162
195
  let items = [];
163
- const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(input.token_budget / 300)), ...(input.paths[0] ? { in: input.paths[0] } : {}) }, input.timeout_ms, state);
196
+ const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(state.limits.tokenBudget / 300)), ...(input.paths[0] ? { in: input.paths[0] } : {}) }, input.timeout_ms, state);
164
197
  if (raw !== null) {
165
198
  const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'graft'), 'code', 100);
166
199
  items.push(...normalized.items);
167
200
  provenance.push(...normalized.provenance);
201
+ state.warnings.push(...normalized.warnings);
168
202
  }
169
203
  if (input.include_docs) {
170
- const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget: input.token_budget }, input.timeout_ms, state);
204
+ const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
171
205
  if (docs !== null) {
172
206
  const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
173
207
  items.push(...normalized.items);
174
208
  provenance.push(...normalized.provenance);
209
+ state.warnings.push(...normalized.warnings);
175
210
  }
176
211
  }
177
212
  return { data: { items, next_cursor: null }, provenance, ...state, warnings: state.warnings };
178
213
  }
179
- async symbol(input, snapshot) {
180
- const state = { used: [], missing: [], warnings: [] };
214
+ async symbol(input, snapshot, state) {
181
215
  const provenance = [];
182
216
  let symbols = [];
183
217
  let references = [];
@@ -196,13 +230,15 @@ export class CodeIntelligenceCoordinator {
196
230
  const normalized = normalizeSymbols(symbol, this.evidenceOptions(snapshot, 'serena-lsp'));
197
231
  symbols.push(...normalized.symbols);
198
232
  provenance.push(...normalized.provenance);
233
+ state.warnings.push(...normalized.warnings);
199
234
  }
200
235
  if (input.include.includes('references')) {
201
236
  const raw = await this.call('serena-lsp', 'references', reference, input.timeout_ms, state);
202
237
  if (raw !== null) {
203
- const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'serena-lsp'), 'code', 100);
204
- references = normalized.items.map(item => ({ symbol_id: input.symbol_id ?? `symbol-${item.item_id.slice(5)}`, path: item.path, line: null, excerpt: item.excerpt, evidence_ids: item.evidence_ids }));
238
+ const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
239
+ references = normalized.symbols.map(symbol => ({ symbol_id: input.symbol_id ?? serenaSymbolId(reference.name_path, reference.relative_path), path: symbol.path, line: null, excerpt: shorten(symbol.signature ?? '', 4000), evidence_ids: symbol.evidence_ids }));
205
240
  provenance.push(...normalized.provenance);
241
+ state.warnings.push(...normalized.warnings);
206
242
  }
207
243
  }
208
244
  if (input.include.includes('implementations')) {
@@ -211,45 +247,83 @@ export class CodeIntelligenceCoordinator {
211
247
  const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
212
248
  symbols.push(...normalized.symbols);
213
249
  provenance.push(...normalized.provenance);
250
+ state.warnings.push(...normalized.warnings);
214
251
  }
215
252
  }
216
253
  if (input.include.includes('diagnostics')) {
217
254
  const raw = await this.call('serena-lsp', 'diagnostics', { relative_path: reference.relative_path }, input.timeout_ms, state);
218
255
  if (raw !== null) {
219
256
  const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'serena-lsp'), 'code', 100);
220
- diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: item.excerpt, evidence_ids: item.evidence_ids }));
257
+ diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: shorten(item.excerpt, 4000), evidence_ids: item.evidence_ids }));
221
258
  provenance.push(...normalized.provenance);
259
+ state.warnings.push(...normalized.warnings);
222
260
  }
223
261
  }
224
262
  return { data: { symbols, references, diagnostics }, provenance, ...state, warnings: state.warnings };
225
263
  }
226
- async trace(input, snapshot) {
227
- const state = { used: [], missing: [], warnings: [] };
264
+ async trace(input, snapshot, state) {
228
265
  const provenance = [];
229
266
  let nodes = [];
230
267
  let edges = [];
231
- const raw = await this.call('graft', 'trace', { symbol: input.symbol_id, direction: input.direction, depth: input.max_depth }, input.timeout_ms, state);
232
- if (raw !== null) {
233
- const normalized = normalizeEdges(raw, this.evidenceOptions(snapshot, 'graft'), input.relations[0] ?? 'references');
234
- nodes.push(...normalized.nodes);
235
- edges.push(...normalized.edges);
236
- provenance.push(...normalized.provenance);
237
- }
238
- if (input.require_resolved || input.relations.some(relation => relation === 'implements' || relation === 'extends')) {
239
- const rawSemantic = await this.call('serena-lsp', 'references', symbolArgs(input.symbol_id), input.timeout_ms, state);
240
- if (rawSemantic !== null) {
241
- const normalized = normalizeEdges(rawSemantic, this.evidenceOptions(snapshot, 'serena-lsp'), 'references');
268
+ const reference = symbolArgs(input.symbol_id);
269
+ if (input.relations.includes('calls') && !input.require_resolved) {
270
+ const raw = await this.call('graft', 'trace', { symbol: reference.name_path, direction: input.direction, depth: input.max_depth }, input.timeout_ms, state);
271
+ if (raw !== null) {
272
+ const normalized = normalizeEdges(raw, this.evidenceOptions(snapshot, 'graft'), 'calls');
242
273
  nodes.push(...normalized.nodes);
243
- edges.push(...normalized.edges);
244
274
  provenance.push(...normalized.provenance);
275
+ state.warnings.push(...normalized.warnings);
276
+ }
277
+ }
278
+ const supported = input.direction === 'in' ? input.relations.filter((relation) => relation === 'references' || relation === 'implements') : [];
279
+ if (supported.length && !reference.relative_path) {
280
+ const raw = await this.call('serena-lsp', 'symbol', { name_path_pattern: reference.name_path, relative_path: '', depth: 0, include_body: false, max_matches: 2 }, input.timeout_ms, state);
281
+ if (raw !== null) {
282
+ const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
283
+ provenance.push(...normalized.provenance);
284
+ state.warnings.push(...normalized.warnings);
285
+ if (normalized.symbols.length === 1)
286
+ Object.assign(reference, symbolArgs(normalized.symbols[0].symbol_id));
287
+ else
288
+ state.warnings.push('reference target is unresolved or ambiguous; use a path-bound symbol_id');
289
+ }
290
+ }
291
+ if (supported.length && reference.relative_path) {
292
+ let validTarget = false;
293
+ try {
294
+ validTarget = snapshot.files.some(file => file.path === assertSafePath(this.root, reference.relative_path, this.options.policy));
245
295
  }
296
+ catch { /* unresolved target is reported below */ }
297
+ if (!validTarget)
298
+ state.warnings.push('reference target is absent from the permitted snapshot');
299
+ else
300
+ for (const relation of supported) {
301
+ const raw = await this.call('serena-lsp', relation === 'implements' ? 'implementations' : 'references', reference, input.timeout_ms, state);
302
+ if (raw === null)
303
+ continue;
304
+ const target = { ...reference, symbol_id: serenaSymbolId(reference.name_path, reference.relative_path) };
305
+ const normalized = normalizeReferenceEdges(raw, this.evidenceOptions(snapshot, 'serena-lsp'), target, relation);
306
+ nodes.unshift(...normalized.nodes);
307
+ edges.push(...normalized.edges);
308
+ provenance.push(...normalized.provenance);
309
+ state.warnings.push(...normalized.warnings);
310
+ }
311
+ if (input.max_depth > 1)
312
+ state.warnings.push('Semantic trace contains one-hop results; deeper traversal is unverified.');
246
313
  }
247
- nodes = [...new Map(nodes.map(node => [node.id, node])).values()].slice(0, input.max_nodes);
248
- edges = edges.filter(edge => nodes.some(node => node.id === edge.from) && nodes.some(node => node.id === edge.to));
249
- return { data: { nodes, edges, frontier_remaining: Math.max(0, edges.length - nodes.length) }, provenance, ...state, warnings: state.warnings };
314
+ const unsupported = input.relations.filter(relation => !supported.includes(relation));
315
+ if (unsupported.length)
316
+ state.warnings.push(`No validated ${input.direction} edge contract for: ${unsupported.join(', ')}`);
317
+ const allNodes = [...new Map(nodes.map(node => [node.id, node])).values()];
318
+ nodes = allNodes.slice(0, input.max_nodes);
319
+ const ids = new Set(nodes.map(node => node.id));
320
+ edges = [...new Map(edges.filter(edge => ids.has(edge.from) && ids.has(edge.to)).map(edge => [`${edge.from}\0${edge.to}\0${edge.relation}`, edge])).values()];
321
+ const omitted = allNodes.length - nodes.length;
322
+ if (omitted)
323
+ state.warnings.push(`${omitted} trace nodes omitted by max_nodes; traversal is incomplete.`);
324
+ return { data: { nodes, edges, frontier_remaining: omitted, traversal_complete: false }, provenance, ...state, warnings: state.warnings };
250
325
  }
251
- async impact(input, snapshot) {
252
- const state = { used: [], missing: [], warnings: [] };
326
+ async impact(input, snapshot, state) {
253
327
  const provenance = [];
254
328
  const structural = [];
255
329
  const semantic = [];
@@ -287,7 +361,7 @@ export class CodeIntelligenceCoordinator {
287
361
  }
288
362
  }
289
363
  if (input.include_docs) {
290
- const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget: 2000 }, input.timeout_ms, state);
364
+ const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
291
365
  if (docs !== null) {
292
366
  const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
293
367
  documents.push(...normalized.items);
@@ -297,10 +371,9 @@ export class CodeIntelligenceCoordinator {
297
371
  const suggested = [...new Set(snapshot.files.map(file => file.path).filter(path => /(^|\/)(test|tests|__tests__|spec)(\/|\.)/u.test(path)))].slice(0, 20);
298
372
  return { data: { semantic_findings: semantic, structural_candidates: structural, documents, suggested_test_paths: suggested, unresolved }, provenance, ...state, warnings: state.warnings };
299
373
  }
300
- async preview(input, snapshot) {
374
+ async preview(input, snapshot, state) {
301
375
  if (this.options.mode !== 'active')
302
376
  throw new Error('code edits require code-intelligence mode=active');
303
- const state = { used: [], missing: [], warnings: [] };
304
377
  const provenance = [];
305
378
  const sandbox = join(this.root, '.yoke', 'code-intelligence', 'worktrees', `preview-${randomUUID()}`);
306
379
  mkdirSync(sandbox, { recursive: true });
@@ -317,15 +390,8 @@ export class CodeIntelligenceCoordinator {
317
390
  const reference = symbolArgs(operation.symbol_id);
318
391
  const args = 'new_name' in operation ? { ...reference, new_name: operation.new_name } : operation.kind === 'safe_delete' ? { name_path_pattern: reference.name_path, relative_path: reference.relative_path } : { ...reference, body: 'content' in operation ? operation.content : '' };
319
392
  const tool = operation.kind === 'safe_delete' ? 'safe_delete' : operation.kind;
320
- try {
321
- await adapter.call({ tool, arguments: args }, input.timeout_ms);
322
- state.used.push('serena-lsp');
323
- }
324
- catch (error) {
325
- state.missing.push('serena-lsp');
326
- state.warnings.push(`serena-lsp: ${error.message}`);
393
+ if (await this.call('serena-lsp', tool, args, input.timeout_ms, state, adapter) === null)
327
394
  break;
328
- }
329
395
  }
330
396
  }
331
397
  finally {
@@ -350,22 +416,28 @@ export class CodeIntelligenceCoordinator {
350
416
  const plan = createEditPlan(this.root, snapshot, sandbox, input.operations, state.missing.length ? 'incomplete' : 'passed', input.validation_profile, [...changed], diff);
351
417
  return { data: plan, provenance, ...state, warnings: state.warnings };
352
418
  }
353
- async apply(input, snapshot) {
419
+ async apply(input, snapshot, state) {
354
420
  if (this.options.mode !== 'active')
355
421
  throw new Error('code edits require code-intelligence mode=active');
356
- const state = { used: [], missing: [], warnings: [] };
422
+ if (nowMs() >= state.deadline)
423
+ throw new Error('request deadline exceeded before apply');
357
424
  const plan = loadEditPlan(this.root, input.plan_id);
358
425
  if (plan.workspace_id !== this.workspace_id || plan.snapshot_id !== input.expected_snapshot_id)
359
426
  throw new Error('edit plan scope does not match workspace or expected snapshot');
360
427
  const expected = loadSnapshot(this.root, input.expected_snapshot_id);
361
428
  assertSnapshotCurrent(this.root, expected, this.options.policy);
429
+ const receipt = { plan_id: plan.plan_id, transaction_id: `tx-${'0'.repeat(36)}`, changed_paths: plan.changed_paths, new_snapshot_id: expected.snapshot_id, validation: plan.validation, committed: false, merged: false };
430
+ if (!state.receiptFits?.(receipt))
431
+ throw new Error('Budget cannot represent the required apply receipt; no transaction was started');
432
+ if (nowMs() >= state.deadline)
433
+ throw new Error('request deadline exceeded before apply');
362
434
  const result = applyEditPlan(this.root, plan, expected, input.idempotency_key);
363
435
  return { data: { plan_id: result.plan_id, transaction_id: result.transaction_id, changed_paths: result.changed_paths, new_snapshot_id: result.new_snapshot_id, validation: result.validation, committed: false, merged: false }, provenance: [], ...state, warnings: state.warnings };
364
436
  }
365
437
  errorResponse(requestId, workspace, snapshotId, started, error) {
366
438
  const code = errorCode(error.message);
367
439
  const blocked = ['WORKSPACE_DENIED', 'SNAPSHOT_STALE', 'APPROVAL_REQUIRED', 'PLAN_EXPIRED', 'CAPABILITY_UNAVAILABLE', 'SCOPE_VIOLATION', 'WRITE_CONFLICT'].includes(code);
368
- return { schema_version: '0.1.0', request_id: requestId, workspace_id: workspace, snapshot_id: snapshotId, status: blocked ? 'blocked' : 'error', data: null, coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' }, provenance: [], warnings: [], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'exact', returned_bytes: 0 }, artifact_uri: null, error: { code, message: error.message } };
440
+ return measureResponse({ schema_version: '0.1.0', request_id: requestId, workspace_id: workspace, snapshot_id: snapshotId, status: blocked ? 'blocked' : 'error', data: null, coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' }, provenance: [], warnings: [], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null, error: { code, message: shorten(error.message, 256) } });
369
441
  }
370
442
  }
371
443
  export function createCodeIntelligenceCoordinator(root, options) { return new CodeIntelligenceCoordinator(root, options); }