@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +6 -1
- package/bench/analyze-codex-comparison.mjs +90 -17
- package/bench/compare-codex.mjs +159 -36
- package/bench/result-schema.mjs +132 -0
- package/canon/manifest.yaml +1 -1
- package/dist/agents/pi-telemetry.js +2 -1
- package/dist/agents/process-streams.js +12 -64
- package/dist/agents/provider-selection.js +12 -0
- package/dist/agents/telemetry.js +52 -52
- package/dist/change/inbox.js +8 -3
- package/dist/check/command.js +69 -17
- package/dist/check/delivery.js +121 -0
- package/dist/cli.js +86 -8
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +156 -84
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/mcp-client.js +10 -2
- package/dist/code-intelligence/mcp-server.js +10 -10
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/cleanup.js +100 -0
- package/dist/loop/dispatcher.js +89 -26
- package/dist/loop/failure.js +104 -0
- package/dist/loop/gate-snapshot.js +19 -0
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +100 -66
- package/dist/loop/parallel-adapters.js +22 -4
- package/dist/loop/parallel-command.js +32 -5
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +21 -4
- package/dist/loop/run-command.js +102 -48
- package/dist/loop/runner.js +5 -4
- package/dist/loop/worker.js +145 -91
- package/dist/observability/history.js +1 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/usage.js +17 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/command.js +22 -0
- package/dist/retrofit/config.js +26 -1
- package/dist/retrofit/gitignore.js +2 -0
- package/dist/retrofit/planners/claude.js +4 -4
- package/dist/retrofit/wsl.js +23 -3
- package/dist/routing/attempts.js +241 -0
- package/dist/routing/capability.js +13 -9
- package/dist/routing/optimization.js +73 -0
- package/dist/routing/registry.js +7 -1
- package/dist/routing/router.js +280 -127
- package/dist/setup/command.js +54 -6
- package/dist/smoke/command.js +302 -74
- package/docs/BENCHMARK-MANIFEST.md +131 -0
- package/docs/CODE-INTELLIGENCE.md +43 -1
- package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
- package/docs/DELIVERY-JOURNEYS.md +199 -0
- package/docs/ECONOMIC-ROUTING.md +180 -0
- package/docs/GOALS.md +61 -4
- package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
- package/docs/parallel-execution.md +37 -9
- package/gemini-extension.json +1 -1
- 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({
|