@hecer/yoke 1.21.1 → 1.23.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 (103) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +48 -0
  4. package/README.md +8 -1
  5. package/TODOS.md +6 -0
  6. package/bench/analyze-codex-comparison.mjs +90 -17
  7. package/bench/compare-codex.mjs +159 -36
  8. package/bench/result-schema.mjs +132 -0
  9. package/canon/manifest.yaml +1 -1
  10. package/canon/skills/visual-verification/SKILL.md +25 -2
  11. package/canon/tools/codex-rtk-hook.mjs +6 -16
  12. package/dist/agents/pi-telemetry.js +2 -1
  13. package/dist/agents/process-streams.js +12 -64
  14. package/dist/agents/provider-selection.js +12 -0
  15. package/dist/agents/telemetry.js +52 -52
  16. package/dist/change/inbox.js +8 -3
  17. package/dist/check/command.js +69 -17
  18. package/dist/check/delivery.js +121 -0
  19. package/dist/cli.js +91 -3
  20. package/dist/code-intelligence/adapters/mcp.js +1 -0
  21. package/dist/code-intelligence/budgets.js +138 -0
  22. package/dist/code-intelligence/contracts.js +2 -0
  23. package/dist/code-intelligence/coordinator.js +159 -85
  24. package/dist/code-intelligence/evidence.js +87 -34
  25. package/dist/code-intelligence/index.js +1 -0
  26. package/dist/code-intelligence/mcp-client.js +25 -6
  27. package/dist/code-intelligence/mcp-server.js +14 -11
  28. package/dist/code-intelligence/preflight.js +71 -0
  29. package/dist/dashboard/analytics.js +5 -3
  30. package/dist/goals/command.js +183 -53
  31. package/dist/goals/usage.js +87 -0
  32. package/dist/loop/cache-isolation.js +36 -0
  33. package/dist/loop/candidate-cleanup.js +47 -17
  34. package/dist/loop/candidates.js +17 -11
  35. package/dist/loop/dispatcher.js +89 -26
  36. package/dist/loop/failure.js +104 -0
  37. package/dist/loop/gate-snapshot.js +19 -0
  38. package/dist/loop/git.js +1 -1
  39. package/dist/loop/loop.js +124 -70
  40. package/dist/loop/parallel-adapters.js +57 -6
  41. package/dist/loop/parallel-command.js +49 -7
  42. package/dist/loop/proof-retention.js +70 -0
  43. package/dist/loop/recovery.js +23 -5
  44. package/dist/loop/reporter.js +22 -5
  45. package/dist/loop/run-command.js +101 -47
  46. package/dist/loop/runner.js +6 -5
  47. package/dist/loop/worker.js +152 -91
  48. package/dist/observability/history.js +2 -1
  49. package/dist/observability/invocation.js +42 -0
  50. package/dist/observability/local-report.js +120 -0
  51. package/dist/observability/usage.js +19 -0
  52. package/dist/prd/command.js +20 -7
  53. package/dist/prd/decompose.js +5 -2
  54. package/dist/retrofit/config.js +29 -2
  55. package/dist/retrofit/gitignore.js +12 -0
  56. package/dist/retrofit/planners/codex.js +20 -20
  57. package/dist/routing/attempts.js +241 -0
  58. package/dist/routing/capability.js +13 -9
  59. package/dist/routing/optimization.js +73 -0
  60. package/dist/routing/registry.js +7 -1
  61. package/dist/routing/router.js +282 -127
  62. package/dist/setup/command.js +8 -2
  63. package/dist/smoke/command.js +387 -85
  64. package/dist/update/check.js +1 -1
  65. package/docs/BENCHMARK-MANIFEST.md +131 -0
  66. package/docs/CODE-INTELLIGENCE.md +43 -1
  67. package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
  68. package/docs/DELIVERY-JOURNEYS.md +206 -0
  69. package/docs/ECONOMIC-ROUTING.md +180 -0
  70. package/docs/GOALS.md +61 -4
  71. package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
  72. package/docs/RELEASE-VALIDATION-1.23.0.md +39 -0
  73. package/docs/benchmarks/2026-10-04-efficiency/ANALYSE.md +182 -0
  74. package/docs/benchmarks/2026-10-04-efficiency/compare-help.py +55 -0
  75. package/docs/benchmarks/2026-10-04-efficiency/manifest.json +125 -0
  76. package/docs/benchmarks/2026-10-04-efficiency/provenance-analysis.json +90 -0
  77. package/docs/benchmarks/2026-10-04-efficiency/provenance-design.json +90 -0
  78. package/docs/benchmarks/2026-10-04-efficiency/provenance-original-report.json +90 -0
  79. package/docs/benchmarks/2026-10-04-efficiency/raw/DEVELOPMENT_ANALYSIS.md +142 -0
  80. package/docs/benchmarks/2026-10-04-efficiency/raw/RESULT.md +21 -0
  81. package/docs/benchmarks/2026-10-04-efficiency/raw/commands.jsonl +26 -0
  82. package/docs/benchmarks/2026-10-04-efficiency/raw/environment.json +31 -0
  83. package/docs/benchmarks/2026-10-04-efficiency/raw/final-yoke-smoke.json +40 -0
  84. package/docs/benchmarks/2026-10-04-efficiency/raw/model-purpose-hints.csv +19 -0
  85. package/docs/benchmarks/2026-10-04-efficiency/raw/observations.jsonl +21 -0
  86. package/docs/benchmarks/2026-10-04-efficiency/raw/observer-command-phases.csv +12 -0
  87. package/docs/benchmarks/2026-10-04-efficiency/raw/roles.csv +5 -0
  88. package/docs/benchmarks/2026-10-04-efficiency/raw/shell-categories.csv +8 -0
  89. package/docs/benchmarks/2026-10-04-efficiency/raw/stories.csv +8 -0
  90. package/docs/benchmarks/2026-10-04-efficiency/raw/summary.json +469 -0
  91. package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-history.jsonl +104 -0
  92. package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-1.log +58 -0
  93. package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-2.log +29 -0
  94. package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-3.log +5 -0
  95. package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-4.log +12 -0
  96. package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-phases.csv +10 -0
  97. package/docs/benchmarks/2026-10-04-efficiency/regression-comparison.json +104 -0
  98. package/docs/parallel-execution.md +37 -9
  99. package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency-prd.json +11 -0
  100. package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency.md +83 -0
  101. package/docs/superpowers/specs/2026-10-04-yoke-1.23-efficiency-design.md +120 -0
  102. package/gemini-extension.json +1 -1
  103. package/package.json +1 -1
@@ -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 {
@@ -52,7 +53,7 @@ export class CodeIntelligenceCoordinator {
52
53
  adapters;
53
54
  constructor(root, options = {}) {
54
55
  this.root = root;
55
- this.workspace_id = workspaceId(root);
56
+ this.workspace_id = options.workspaceId ?? workspaceId(root);
56
57
  this.options = options;
57
58
  this.adapters = {
58
59
  graft: options.adapters?.graft ?? createGraftAdapter(root, options.graft),
@@ -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) {
@@ -112,27 +141,21 @@ export class CodeIntelligenceCoordinator {
112
141
  if (candidate.workspace_id && candidate.workspace_id !== this.workspace_id)
113
142
  throw new Error('workspace is not permitted');
114
143
  const snapshot = candidate.snapshot_id ? loadSnapshot(this.root, candidate.snapshot_id) : createSnapshot(this.root, this.options.policy, this.workspace_id);
144
+ if (snapshot.workspace_id !== this.workspace_id)
145
+ throw new Error('snapshot workspace identifier differs from the configured workspace; create a new snapshot');
115
146
  if (candidate.snapshot_id)
116
147
  assertSnapshotCurrent(this.root, snapshot, this.options.policy);
117
148
  return snapshot;
118
149
  }
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
150
  coverage(used, missing) {
128
151
  return {
129
152
  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',
153
+ structural: used.includes('graft') ? 'partial' : 'unavailable',
154
+ semantic: used.includes('serena-lsp') ? 'partial' : 'unavailable',
155
+ documents: used.includes('graphify') ? 'partial' : 'unavailable',
133
156
  };
134
157
  }
135
- async call(backend, tool, args, timeout, state) {
158
+ async call(backend, tool, args, timeout, state, adapter = this.adapters[backend]) {
136
159
  const seen = new Set([...state.used, ...state.missing]);
137
160
  const maxBackends = this.options.limits?.maxBackends ?? 3;
138
161
  if (!seen.has(backend) && seen.size >= maxBackends) {
@@ -140,8 +163,17 @@ export class CodeIntelligenceCoordinator {
140
163
  state.warnings.push(`${backend}: backend budget exceeded`);
141
164
  return null;
142
165
  }
166
+ let timer;
143
167
  try {
144
- const value = await this.adapters[backend].call({ tool, arguments: args }, Math.min(timeout, this.options.limits?.timeoutMs ?? timeout));
168
+ const remaining = Math.min(timeout, state.deadline - nowMs());
169
+ if (remaining <= 0)
170
+ throw new Error('request deadline exceeded; backend was not started');
171
+ const expired = new Promise((_resolve, reject) => {
172
+ timer = setTimeout(() => { reject(new Error('request deadline exceeded')); void adapter.close().catch(() => { }); }, remaining);
173
+ });
174
+ const value = await Promise.race([adapter.call({ tool, arguments: args }, remaining), expired]);
175
+ if (nowMs() >= state.deadline)
176
+ throw new Error('request deadline exceeded; late backend result discarded');
145
177
  if (!state.used.includes(backend))
146
178
  state.used.push(backend);
147
179
  return value;
@@ -149,35 +181,39 @@ export class CodeIntelligenceCoordinator {
149
181
  catch (error) {
150
182
  if (!state.missing.includes(backend))
151
183
  state.missing.push(backend);
152
- state.warnings.push(`${backend}: ${error.message}`);
184
+ state.warnings.push(`${backend}: ${shorten(error.message, 256)}`);
153
185
  return null;
154
186
  }
187
+ finally {
188
+ if (timer)
189
+ clearTimeout(timer);
190
+ }
155
191
  }
156
192
  evidenceOptions(snapshot, backend, maxChars = 12000) {
157
193
  return { root: this.root, snapshot, adapter: this.adapters[backend], maxChars };
158
194
  }
159
- async context(input, snapshot) {
160
- const state = { used: [], missing: [], warnings: [] };
195
+ async context(input, snapshot, state) {
161
196
  const provenance = [];
162
197
  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);
198
+ 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
199
  if (raw !== null) {
165
200
  const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'graft'), 'code', 100);
166
201
  items.push(...normalized.items);
167
202
  provenance.push(...normalized.provenance);
203
+ state.warnings.push(...normalized.warnings);
168
204
  }
169
205
  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);
206
+ const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
171
207
  if (docs !== null) {
172
208
  const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
173
209
  items.push(...normalized.items);
174
210
  provenance.push(...normalized.provenance);
211
+ state.warnings.push(...normalized.warnings);
175
212
  }
176
213
  }
177
214
  return { data: { items, next_cursor: null }, provenance, ...state, warnings: state.warnings };
178
215
  }
179
- async symbol(input, snapshot) {
180
- const state = { used: [], missing: [], warnings: [] };
216
+ async symbol(input, snapshot, state) {
181
217
  const provenance = [];
182
218
  let symbols = [];
183
219
  let references = [];
@@ -196,13 +232,15 @@ export class CodeIntelligenceCoordinator {
196
232
  const normalized = normalizeSymbols(symbol, this.evidenceOptions(snapshot, 'serena-lsp'));
197
233
  symbols.push(...normalized.symbols);
198
234
  provenance.push(...normalized.provenance);
235
+ state.warnings.push(...normalized.warnings);
199
236
  }
200
237
  if (input.include.includes('references')) {
201
238
  const raw = await this.call('serena-lsp', 'references', reference, input.timeout_ms, state);
202
239
  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 }));
240
+ const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
241
+ 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
242
  provenance.push(...normalized.provenance);
243
+ state.warnings.push(...normalized.warnings);
206
244
  }
207
245
  }
208
246
  if (input.include.includes('implementations')) {
@@ -211,45 +249,83 @@ export class CodeIntelligenceCoordinator {
211
249
  const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
212
250
  symbols.push(...normalized.symbols);
213
251
  provenance.push(...normalized.provenance);
252
+ state.warnings.push(...normalized.warnings);
214
253
  }
215
254
  }
216
255
  if (input.include.includes('diagnostics')) {
217
256
  const raw = await this.call('serena-lsp', 'diagnostics', { relative_path: reference.relative_path }, input.timeout_ms, state);
218
257
  if (raw !== null) {
219
258
  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 }));
259
+ diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: shorten(item.excerpt, 4000), evidence_ids: item.evidence_ids }));
221
260
  provenance.push(...normalized.provenance);
261
+ state.warnings.push(...normalized.warnings);
222
262
  }
223
263
  }
224
264
  return { data: { symbols, references, diagnostics }, provenance, ...state, warnings: state.warnings };
225
265
  }
226
- async trace(input, snapshot) {
227
- const state = { used: [], missing: [], warnings: [] };
266
+ async trace(input, snapshot, state) {
228
267
  const provenance = [];
229
268
  let nodes = [];
230
269
  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');
270
+ const reference = symbolArgs(input.symbol_id);
271
+ if (input.relations.includes('calls') && !input.require_resolved) {
272
+ const raw = await this.call('graft', 'trace', { symbol: reference.name_path, direction: input.direction, depth: input.max_depth }, input.timeout_ms, state);
273
+ if (raw !== null) {
274
+ const normalized = normalizeEdges(raw, this.evidenceOptions(snapshot, 'graft'), 'calls');
242
275
  nodes.push(...normalized.nodes);
243
- edges.push(...normalized.edges);
244
276
  provenance.push(...normalized.provenance);
277
+ state.warnings.push(...normalized.warnings);
278
+ }
279
+ }
280
+ const supported = input.direction === 'in' ? input.relations.filter((relation) => relation === 'references' || relation === 'implements') : [];
281
+ if (supported.length && !reference.relative_path) {
282
+ 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);
283
+ if (raw !== null) {
284
+ const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
285
+ provenance.push(...normalized.provenance);
286
+ state.warnings.push(...normalized.warnings);
287
+ if (normalized.symbols.length === 1)
288
+ Object.assign(reference, symbolArgs(normalized.symbols[0].symbol_id));
289
+ else
290
+ state.warnings.push('reference target is unresolved or ambiguous; use a path-bound symbol_id');
291
+ }
292
+ }
293
+ if (supported.length && reference.relative_path) {
294
+ let validTarget = false;
295
+ try {
296
+ validTarget = snapshot.files.some(file => file.path === assertSafePath(this.root, reference.relative_path, this.options.policy));
245
297
  }
298
+ catch { /* unresolved target is reported below */ }
299
+ if (!validTarget)
300
+ state.warnings.push('reference target is absent from the permitted snapshot');
301
+ else
302
+ for (const relation of supported) {
303
+ const raw = await this.call('serena-lsp', relation === 'implements' ? 'implementations' : 'references', reference, input.timeout_ms, state);
304
+ if (raw === null)
305
+ continue;
306
+ const target = { ...reference, symbol_id: serenaSymbolId(reference.name_path, reference.relative_path) };
307
+ const normalized = normalizeReferenceEdges(raw, this.evidenceOptions(snapshot, 'serena-lsp'), target, relation);
308
+ nodes.unshift(...normalized.nodes);
309
+ edges.push(...normalized.edges);
310
+ provenance.push(...normalized.provenance);
311
+ state.warnings.push(...normalized.warnings);
312
+ }
313
+ if (input.max_depth > 1)
314
+ state.warnings.push('Semantic trace contains one-hop results; deeper traversal is unverified.');
246
315
  }
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 };
316
+ const unsupported = input.relations.filter(relation => !supported.includes(relation));
317
+ if (unsupported.length)
318
+ state.warnings.push(`No validated ${input.direction} edge contract for: ${unsupported.join(', ')}`);
319
+ const allNodes = [...new Map(nodes.map(node => [node.id, node])).values()];
320
+ nodes = allNodes.slice(0, input.max_nodes);
321
+ const ids = new Set(nodes.map(node => node.id));
322
+ 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()];
323
+ const omitted = allNodes.length - nodes.length;
324
+ if (omitted)
325
+ state.warnings.push(`${omitted} trace nodes omitted by max_nodes; traversal is incomplete.`);
326
+ return { data: { nodes, edges, frontier_remaining: omitted, traversal_complete: false }, provenance, ...state, warnings: state.warnings };
250
327
  }
251
- async impact(input, snapshot) {
252
- const state = { used: [], missing: [], warnings: [] };
328
+ async impact(input, snapshot, state) {
253
329
  const provenance = [];
254
330
  const structural = [];
255
331
  const semantic = [];
@@ -287,7 +363,7 @@ export class CodeIntelligenceCoordinator {
287
363
  }
288
364
  }
289
365
  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);
366
+ 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
367
  if (docs !== null) {
292
368
  const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
293
369
  documents.push(...normalized.items);
@@ -297,10 +373,9 @@ export class CodeIntelligenceCoordinator {
297
373
  const suggested = [...new Set(snapshot.files.map(file => file.path).filter(path => /(^|\/)(test|tests|__tests__|spec)(\/|\.)/u.test(path)))].slice(0, 20);
298
374
  return { data: { semantic_findings: semantic, structural_candidates: structural, documents, suggested_test_paths: suggested, unresolved }, provenance, ...state, warnings: state.warnings };
299
375
  }
300
- async preview(input, snapshot) {
376
+ async preview(input, snapshot, state) {
301
377
  if (this.options.mode !== 'active')
302
378
  throw new Error('code edits require code-intelligence mode=active');
303
- const state = { used: [], missing: [], warnings: [] };
304
379
  const provenance = [];
305
380
  const sandbox = join(this.root, '.yoke', 'code-intelligence', 'worktrees', `preview-${randomUUID()}`);
306
381
  mkdirSync(sandbox, { recursive: true });
@@ -317,15 +392,8 @@ export class CodeIntelligenceCoordinator {
317
392
  const reference = symbolArgs(operation.symbol_id);
318
393
  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
394
  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}`);
395
+ if (await this.call('serena-lsp', tool, args, input.timeout_ms, state, adapter) === null)
327
396
  break;
328
- }
329
397
  }
330
398
  }
331
399
  finally {
@@ -350,22 +418,28 @@ export class CodeIntelligenceCoordinator {
350
418
  const plan = createEditPlan(this.root, snapshot, sandbox, input.operations, state.missing.length ? 'incomplete' : 'passed', input.validation_profile, [...changed], diff);
351
419
  return { data: plan, provenance, ...state, warnings: state.warnings };
352
420
  }
353
- async apply(input, snapshot) {
421
+ async apply(input, snapshot, state) {
354
422
  if (this.options.mode !== 'active')
355
423
  throw new Error('code edits require code-intelligence mode=active');
356
- const state = { used: [], missing: [], warnings: [] };
424
+ if (nowMs() >= state.deadline)
425
+ throw new Error('request deadline exceeded before apply');
357
426
  const plan = loadEditPlan(this.root, input.plan_id);
358
427
  if (plan.workspace_id !== this.workspace_id || plan.snapshot_id !== input.expected_snapshot_id)
359
428
  throw new Error('edit plan scope does not match workspace or expected snapshot');
360
429
  const expected = loadSnapshot(this.root, input.expected_snapshot_id);
361
430
  assertSnapshotCurrent(this.root, expected, this.options.policy);
431
+ 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 };
432
+ if (!state.receiptFits?.(receipt))
433
+ throw new Error('Budget cannot represent the required apply receipt; no transaction was started');
434
+ if (nowMs() >= state.deadline)
435
+ throw new Error('request deadline exceeded before apply');
362
436
  const result = applyEditPlan(this.root, plan, expected, input.idempotency_key);
363
437
  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
438
  }
365
439
  errorResponse(requestId, workspace, snapshotId, started, error) {
366
440
  const code = errorCode(error.message);
367
441
  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 } };
442
+ 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
443
  }
370
444
  }
371
445
  export function createCodeIntelligenceCoordinator(root, options) { return new CodeIntelligenceCoordinator(root, options); }
@@ -1,5 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { assertSafePath } from './snapshots.js';
3
+ /** Truncate only between Unicode code points, including at normalizer limits. */
4
+ export function shorten(value, maxChars) {
5
+ const clipped = value.slice(0, Math.max(0, maxChars));
6
+ return /[\uD800-\uDBFF]$/u.test(clipped) ? clipped.slice(0, -1) : clipped;
7
+ }
3
8
  function hash(value) { return createHash('sha256').update(value).digest('hex'); }
4
9
  function textOf(raw) {
5
10
  if (typeof raw === 'string')
@@ -22,32 +27,46 @@ function locate(text, root) {
22
27
  return { path: null, line: null };
23
28
  }
24
29
  }
25
- export function makeProvenance(options, text, resolution = 'resolved') {
30
+ export function makeProvenance(options, text, resolution = 'unresolved') {
26
31
  const location = locate(text, options.root);
27
- const file = location.path ? options.snapshot.files.find(item => item.path === location.path) : undefined;
28
32
  return {
29
- backend: options.adapter.name, version: options.adapter.version, source_path: location.path, content_hash: file?.hash ?? null,
30
- byte_range: null, origin: options.origin ?? (options.adapter.semantic ? 'lsp' : 'parser'), resolution, freshness: 'current',
33
+ backend: options.adapter.name, version: options.adapter.version, source_path: location.path, content_hash: null,
34
+ // A current workspace snapshot does not prove that a backend index read it.
35
+ // None of the supported backend wire contracts supplies a validated index
36
+ // snapshot binding. Do not trust raw `freshness` or attach a newer file hash.
37
+ byte_range: null, origin: options.origin ?? (options.adapter.semantic ? 'lsp' : 'parser'), resolution, freshness: 'unknown',
31
38
  };
32
39
  }
33
40
  export function evidenceId(provenance, excerpt) {
34
- return `evidence-${hash(JSON.stringify({ provenance, excerpt })).slice(0, 24)}`;
41
+ const { evidence_id: _id, ...fields } = provenance;
42
+ return `evidence-${hash(JSON.stringify({ provenance: fields, excerpt })).slice(0, 24)}`;
43
+ }
44
+ function candidates(raw) {
45
+ if (Array.isArray(raw))
46
+ return raw;
47
+ if (raw && typeof raw === 'object' && Array.isArray(raw.results))
48
+ return raw.results;
49
+ return null;
35
50
  }
36
51
  export function normalizeItems(raw, options, kind = 'code', limit = 100) {
37
- const full = textOf(raw);
38
- const chunks = Array.isArray(raw) ? raw : (raw && typeof raw === 'object' && Array.isArray(raw.results) ? raw.results : [raw]);
52
+ const chunks = candidates(raw) ?? (raw === null || raw === undefined || raw === '' ? [] : [raw]);
39
53
  const provenance = [];
40
54
  const items = [];
41
- const maxChars = options.maxChars ?? 12000;
55
+ const warnings = [];
56
+ const maxChars = Math.min(12000, options.maxChars ?? 12000);
57
+ if (chunks.length > limit)
58
+ warnings.push('backend result list truncated');
42
59
  for (const candidate of chunks.slice(0, limit)) {
43
- const excerpt = textOf(candidate).slice(0, maxChars);
60
+ const text = textOf(candidate);
61
+ const excerpt = shorten(text, maxChars);
62
+ if (excerpt !== text)
63
+ warnings.push('backend result excerpt truncated');
44
64
  if (!excerpt)
45
65
  continue;
46
- const p = makeProvenance(options, excerpt, excerpt.toLowerCase().includes('ambiguous') ? 'ambiguous' : 'resolved');
47
- if (candidate && typeof candidate === 'object' && typeof candidate.relative_path === 'string') {
66
+ const p = makeProvenance(options, excerpt);
67
+ if (candidate && typeof candidate === 'object' && 'relative_path' in candidate && typeof candidate.relative_path === 'string') {
48
68
  try {
49
69
  p.source_path = assertSafePath(options.root, candidate.relative_path);
50
- p.content_hash = options.snapshot.files.find(file => file.path === p.source_path)?.hash ?? null;
51
70
  }
52
71
  catch {
53
72
  p.source_path = null;
@@ -56,33 +75,67 @@ export function normalizeItems(raw, options, kind = 'code', limit = 100) {
56
75
  }
57
76
  }
58
77
  const id = evidenceId(p, excerpt);
78
+ p.evidence_id = id;
59
79
  provenance.push(p);
60
80
  items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: items.length });
61
81
  }
62
- if (items.length === 0 && full) {
63
- const excerpt = full.slice(0, maxChars);
64
- const p = makeProvenance(options, excerpt, 'unresolved');
65
- const id = evidenceId(p, excerpt);
66
- provenance.push(p);
67
- items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: 0 });
68
- }
69
- return { items, provenance };
82
+ return { items, provenance, warnings: [...new Set(warnings)] };
83
+ }
84
+ export function serenaSymbolId(namePath, relativePath) {
85
+ return `serena:${encodeURIComponent(JSON.stringify({ name_path: namePath, relative_path: relativePath }))}`;
70
86
  }
71
87
  export function normalizeSymbols(raw, options) {
72
- const normalized = normalizeItems(raw, options, 'symbol');
73
- const candidates = Array.isArray(raw) ? raw : (raw && typeof raw === 'object' && Array.isArray(raw.results) ? raw.results : []);
74
- const symbols = normalized.items.map((item, index) => {
75
- const candidate = candidates[index] && typeof candidates[index] === 'object' ? candidates[index] : {};
76
- const namePath = String(candidate.name_path ?? candidate.namePath ?? candidate.name ?? item.excerpt.split(/\s|\(|\{/u)[0] ?? 'unknown');
77
- const path = typeof candidate.relative_path === 'string' ? candidate.relative_path : item.path;
78
- const stableId = `serena:${encodeURIComponent(JSON.stringify({ name_path: namePath, relative_path: path ?? '' }))}`;
79
- return { symbol_id: stableId, name: String(candidate.name ?? namePath.split('/').at(-1) ?? namePath), path, kind: String(candidate.kind ?? 'unknown'), signature: item.excerpt.slice(0, 500), evidence_ids: item.evidence_ids };
80
- });
81
- return { symbols, references: [], diagnostics: [], provenance: normalized.provenance };
88
+ const list = candidates(raw);
89
+ const symbols = [];
90
+ const provenance = [];
91
+ const warnings = [];
92
+ if (!list) {
93
+ const normalized = normalizeItems(raw, options, 'symbol');
94
+ return { symbols, references: [], diagnostics: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'unrecognized semantic symbol response'] };
95
+ }
96
+ if (list.length > 100)
97
+ warnings.push('semantic symbol result list truncated');
98
+ for (const value of list.slice(0, 100)) {
99
+ const normalized = normalizeItems([value], options, 'symbol', 1);
100
+ warnings.push(...normalized.warnings);
101
+ const item = normalized.items[0];
102
+ const p = normalized.provenance[0];
103
+ if (!item || !p)
104
+ continue;
105
+ const candidate = value && typeof value === 'object' ? value : {};
106
+ const namePath = typeof candidate.name_path === 'string' ? candidate.name_path.trim() : '';
107
+ if (!options.adapter.semantic || !namePath || !item.path || !options.snapshot.files.some(file => file.path === item.path)) {
108
+ provenance.push(p);
109
+ warnings.push('unresolved semantic symbol record');
110
+ continue;
111
+ }
112
+ p.resolution = 'resolved';
113
+ p.evidence_id = evidenceId(p, item.excerpt);
114
+ provenance.push(p);
115
+ symbols.push({ symbol_id: serenaSymbolId(namePath, item.path), name: typeof candidate.name === 'string' && candidate.name ? candidate.name : namePath.split('/').at(-1) || namePath,
116
+ path: item.path, kind: typeof candidate.kind === 'string' ? candidate.kind : 'unknown', signature: shorten(item.excerpt, 500), evidence_ids: [p.evidence_id] });
117
+ }
118
+ return { symbols, references: [], diagnostics: [], provenance, warnings: [...new Set(warnings)] };
82
119
  }
83
- export function normalizeEdges(raw, options, relation) {
120
+ /** Graft prose/search hits are candidates, not an ordered graph path. */
121
+ export function normalizeEdges(raw, options, _relation) {
84
122
  const normalized = normalizeItems(raw, options, 'code');
85
- const nodes = normalized.items.map(item => ({ id: item.item_id, label: item.excerpt.slice(0, 160), path: item.path, evidence_ids: item.evidence_ids }));
86
- const edges = nodes.slice(1).map((node, index) => ({ from: nodes[index].id, to: node.id, relation, evidence_ids: node.evidence_ids }));
87
- return { nodes, edges, provenance: normalized.provenance };
123
+ const nodes = normalized.items.map(item => ({ id: item.item_id, label: shorten(item.excerpt, 160), path: item.path, evidence_ids: item.evidence_ids }));
124
+ return { nodes, edges: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'structural trace response has no validated edge contract'] };
125
+ }
126
+ export function normalizeReferenceEdges(raw, options, target, relation = 'references') {
127
+ const normalized = normalizeSymbols(raw, options);
128
+ const nodes = normalized.symbols.map(symbol => ({ id: symbol.symbol_id, label: symbol.name, path: symbol.path, evidence_ids: symbol.evidence_ids }));
129
+ let targetPath;
130
+ try {
131
+ targetPath = assertSafePath(options.root, target.relative_path);
132
+ if (!options.snapshot.files.some(file => file.path === targetPath))
133
+ throw new Error('target is absent from snapshot');
134
+ }
135
+ catch {
136
+ return { nodes, edges: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'reference target is unresolved'] };
137
+ }
138
+ const targetNode = { id: target.symbol_id, label: target.name_path, path: targetPath, evidence_ids: [] };
139
+ const edges = nodes.map(node => ({ from: node.id, to: targetNode.id, relation, evidence_ids: node.evidence_ids }));
140
+ return { nodes: [targetNode, ...nodes], edges, provenance: normalized.provenance, warnings: normalized.warnings };
88
141
  }
@@ -3,3 +3,4 @@ export * from './coordinator.js';
3
3
  export * from './edit-plans.js';
4
4
  export * from './snapshots.js';
5
5
  export * from './transactions.js';
6
+ export * from './preflight.js';