@ngockhoale/ukit 1.6.4 → 1.6.6
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/CHANGELOG.md +28 -0
- package/manifests/platform.full.yaml +59 -0
- package/package.json +1 -1
- package/src/core/applyPlan.js +6 -1
- package/src/core/buildPlan.js +16 -2
- package/src/core/ensureGitignore.js +136 -60
- package/src/core/runInstallPipeline.js +19 -11
- package/src/core/runtimeConfig.js +37 -1
- package/src/core/uninstall.js +10 -5
- package/templates/.claude/agents/ukit-vision-analyst.md +101 -0
- package/templates/.claude/hooks/handoff-model-guard.sh +13 -0
- package/templates/.claude/hooks/vision-gate.sh +230 -0
- package/templates/.claude/hooks/vision-router.sh +155 -0
- package/templates/.claude/settings.json +10 -0
- package/templates/.claude/ukit/index/extract-image.mjs +434 -0
- package/templates/.claude/ukit/index/route-task.mjs +290 -7
- package/templates/.claude/ukit/index/unic-gateway.mjs +271 -0
- package/templates/CLAUDE.md +8 -0
- package/templates/ukit/storage/config.json +33 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import * as indexCore from './lib/index-core.mjs';
|
|
6
8
|
import {
|
|
7
9
|
DEFAULT_RECENT_CACHE_MAX_ENTRIES,
|
|
@@ -9,6 +11,7 @@ import {
|
|
|
9
11
|
writeRecentCacheEntry,
|
|
10
12
|
} from './cache-utils.mjs';
|
|
11
13
|
import { ROUTE_CATALOG } from './route-catalog.mjs';
|
|
14
|
+
import { detectUnicGateway } from './unic-gateway.mjs';
|
|
12
15
|
|
|
13
16
|
const {
|
|
14
17
|
resolveContext,
|
|
@@ -44,6 +47,119 @@ const buildNormalizedRouteSignalText = typeof indexCore.buildRouteSignalText ===
|
|
|
44
47
|
.join('\n')
|
|
45
48
|
.toLowerCase();
|
|
46
49
|
|
|
50
|
+
// --- Vision lane (TASK-007) ---------------------------------------------------
|
|
51
|
+
// Vision is a CAPABILITY lane, orthogonal to the lite/code/smart COST tier. It is emitted as
|
|
52
|
+
// separate `visionLane`/`visionModel`/`visionReason` fields alongside `modelTier`, never in place
|
|
53
|
+
// of it. `modelTier` must never take the value `vision` (see buildExecutionContract() below).
|
|
54
|
+
const __routeTaskDir = path.dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
const EXTRACT_IMAGE_SCRIPT_PATH = path.join(__routeTaskDir, 'extract-image.mjs');
|
|
56
|
+
const VISION_DETECT_TIMEOUT_MS = 800;
|
|
57
|
+
const VISION_IMAGE_URL_RE = /https?:\/\/\S*\.(?:png|jpe?g|gif|webp|bmp)\b/i;
|
|
58
|
+
const VISION_IMAGE_PATH_RE = /\.(?:png|jpe?g|gif|webp|bmp)\b/i;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Cheap, synchronous, anchored detection against real file extensions / image URLs only.
|
|
62
|
+
* Deliberately does NOT match loose substrings like "imagine", "imagery", or "anh" — those
|
|
63
|
+
* contain image-ish text but never a literal `.png`/`.jpg`/etc extension.
|
|
64
|
+
*/
|
|
65
|
+
function detectVisionReasonFromText(text = '') {
|
|
66
|
+
const value = String(text || '');
|
|
67
|
+
if (!value) return null;
|
|
68
|
+
if (VISION_IMAGE_URL_RE.test(value)) return 'url-in-text';
|
|
69
|
+
if (VISION_IMAGE_PATH_RE.test(value)) return 'path-in-text';
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Best-effort probe for a pasted image in the current session transcript, via extract-image.mjs
|
|
75
|
+
* --detect. Never throws, never blocks the hot path beyond a short timeout: any spawn failure,
|
|
76
|
+
* non-zero exit, timeout, or malformed JSON degrades to "no image detected".
|
|
77
|
+
*/
|
|
78
|
+
function detectPastedImageViaExtractor({ rootDir = process.cwd() } = {}) {
|
|
79
|
+
try {
|
|
80
|
+
const result = spawnSync(process.execPath, [EXTRACT_IMAGE_SCRIPT_PATH, '--detect', '--json'], {
|
|
81
|
+
cwd: rootDir,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
timeout: VISION_DETECT_TIMEOUT_MS,
|
|
84
|
+
});
|
|
85
|
+
if (!result || result.error || result.status !== 0 || !result.stdout) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const parsed = JSON.parse(result.stdout);
|
|
89
|
+
return Number(parsed?.imageCount) > 0;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Gateway-aware model pick (TASK-003 aware). Calls detectUnicGateway() directly (not the CLI) to
|
|
97
|
+
* avoid a spawn on the hot path. unicMode true -> unic-vision. unicMode false -> read
|
|
98
|
+
* orchestration.modelTiers.vision.fallbackModel from config (TASK-001). Neither available -> omit
|
|
99
|
+
* visionModel and return an advisory; a blind model is never silently named.
|
|
100
|
+
*/
|
|
101
|
+
async function resolveVisionModel({ rootDir = process.cwd() } = {}) {
|
|
102
|
+
let gateway = null;
|
|
103
|
+
try {
|
|
104
|
+
gateway = detectUnicGateway({ rootDir });
|
|
105
|
+
} catch {
|
|
106
|
+
gateway = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (gateway?.unicMode) {
|
|
110
|
+
return { visionModel: gateway.visionModel || 'unic-vision', visionAdvisory: null };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const config = await readJson(path.join(rootDir, '.ukit', 'storage', 'config.json'), null);
|
|
114
|
+
const fallbackModel = config?.orchestration?.modelTiers?.vision?.fallbackModel;
|
|
115
|
+
if (typeof fallbackModel === 'string' && fallbackModel.trim()) {
|
|
116
|
+
return { visionModel: fallbackModel.trim(), visionAdvisory: null };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
visionModel: null,
|
|
121
|
+
visionAdvisory: 'vision lane detected but no vision model is configured (UNIC gateway inactive '
|
|
122
|
+
+ 'and orchestration.modelTiers.vision.fallbackModel is missing); no model was named to avoid '
|
|
123
|
+
+ 'silently naming a blind model.',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Detects the vision lane and, when applicable, resolves the model to use for it. Returns null
|
|
129
|
+
* when the task has no image signal at all (silent when not applicable — no visionLane line, and
|
|
130
|
+
* the (costlier) extractor probe only runs when the cheap flag/text checks found nothing).
|
|
131
|
+
*/
|
|
132
|
+
async function resolveVisionLane({
|
|
133
|
+
rootDir = process.cwd(),
|
|
134
|
+
promptText = '',
|
|
135
|
+
commandText = '',
|
|
136
|
+
targetFile = '',
|
|
137
|
+
visionFlag = false,
|
|
138
|
+
} = {}) {
|
|
139
|
+
let visionReason = visionFlag ? 'explicit-flag' : null;
|
|
140
|
+
|
|
141
|
+
if (!visionReason) {
|
|
142
|
+
const haystack = [promptText, commandText, targetFile].filter(Boolean).join('\n');
|
|
143
|
+
visionReason = detectVisionReasonFromText(haystack);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!visionReason && detectPastedImageViaExtractor({ rootDir })) {
|
|
147
|
+
visionReason = 'pasted-image';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!visionReason) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const { visionModel, visionAdvisory } = await resolveVisionModel({ rootDir });
|
|
155
|
+
return {
|
|
156
|
+
visionLane: true,
|
|
157
|
+
visionReason,
|
|
158
|
+
visionModel,
|
|
159
|
+
visionAdvisory,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
47
163
|
async function main() {
|
|
48
164
|
const args = process.argv.slice(2);
|
|
49
165
|
const rootDir = getRootDir(args);
|
|
@@ -51,9 +167,11 @@ async function main() {
|
|
|
51
167
|
const routeCachePath = path.join(rootDir, '.claude', 'ukit', 'route-cache.json');
|
|
52
168
|
const routeAuditPath = path.join(rootDir, '.ukit', 'storage', 'cache', 'route-audit.json');
|
|
53
169
|
const previousState = await readJson(sharedStatePath, {});
|
|
170
|
+
const escalationConfig = await readJson(path.join(rootDir, '.ukit', 'storage', 'config.json'), null);
|
|
54
171
|
const targetFile = readFlagValue(args, '--target');
|
|
55
172
|
const taskType = readFlagValue(args, '--type');
|
|
56
173
|
const commandText = readFlagValue(args, '--tool-command') ?? '';
|
|
174
|
+
const visionFlag = args.includes('--vision');
|
|
57
175
|
const lastPrompt = readFlagValue(args, '--last-prompt')
|
|
58
176
|
?? previousState?.routingContext?.lastExplicitUserPromptText
|
|
59
177
|
?? '';
|
|
@@ -81,6 +199,7 @@ async function main() {
|
|
|
81
199
|
adapter,
|
|
82
200
|
lastExplicitUserPromptText: lastPrompt,
|
|
83
201
|
commandNamespace,
|
|
202
|
+
visionFlag,
|
|
84
203
|
});
|
|
85
204
|
const useIndexedContext = shouldUseIndexedContext({
|
|
86
205
|
activeSkills: preparedRoute.activeSkills,
|
|
@@ -145,11 +264,16 @@ async function main() {
|
|
|
145
264
|
continuationState: advanceContinuationState(
|
|
146
265
|
cachedRouteState.routeSummary.continuationState ?? null,
|
|
147
266
|
cachedRouteState.routeSummary.continuationState ?? null,
|
|
267
|
+
resolveEscalationSettings(escalationConfig),
|
|
148
268
|
),
|
|
149
269
|
}
|
|
150
270
|
: null;
|
|
151
271
|
if (rescuedRouteSummary) {
|
|
152
272
|
applyRescueBiasToRouteSummary(rescuedRouteSummary);
|
|
273
|
+
applyEscalationToRouteSummary(rescuedRouteSummary, {
|
|
274
|
+
config: escalationConfig,
|
|
275
|
+
targetFile: cachedRouteState.routingContext?.targetFile ?? null,
|
|
276
|
+
});
|
|
153
277
|
}
|
|
154
278
|
const sharedState = {
|
|
155
279
|
...cachedRouteState,
|
|
@@ -185,6 +309,7 @@ async function main() {
|
|
|
185
309
|
previousContextSnapshot,
|
|
186
310
|
recentOutputSnapshot,
|
|
187
311
|
previousRouteSummary: previousState?.routeSummary ?? null,
|
|
312
|
+
escalationConfig,
|
|
188
313
|
});
|
|
189
314
|
await seedHelperCaches({
|
|
190
315
|
rootDir,
|
|
@@ -248,6 +373,7 @@ async function prepareTaskRoute({
|
|
|
248
373
|
taskType = null,
|
|
249
374
|
adapter = 'claude',
|
|
250
375
|
lastExplicitUserPromptText = '',
|
|
376
|
+
visionFlag = false,
|
|
251
377
|
} = {}) {
|
|
252
378
|
const absoluteRoot = path.resolve(rootDir);
|
|
253
379
|
const normalizedPrompt = String(promptText || '').trim();
|
|
@@ -301,6 +427,13 @@ async function prepareTaskRoute({
|
|
|
301
427
|
executionCandidates,
|
|
302
428
|
});
|
|
303
429
|
const preservedPrompt = normalizedPrompt || String(lastExplicitUserPromptText || '').trim();
|
|
430
|
+
const visionInfo = await resolveVisionLane({
|
|
431
|
+
rootDir: absoluteRoot,
|
|
432
|
+
promptText: normalizedPrompt,
|
|
433
|
+
commandText: normalizedCommand,
|
|
434
|
+
targetFile: normalizedTarget,
|
|
435
|
+
visionFlag: Boolean(visionFlag),
|
|
436
|
+
});
|
|
304
437
|
|
|
305
438
|
return {
|
|
306
439
|
rootDir: absoluteRoot,
|
|
@@ -317,6 +450,10 @@ async function prepareTaskRoute({
|
|
|
317
450
|
executionScores,
|
|
318
451
|
executionCandidates,
|
|
319
452
|
executionMode,
|
|
453
|
+
visionLane: Boolean(visionInfo?.visionLane),
|
|
454
|
+
visionReason: visionInfo?.visionReason ?? null,
|
|
455
|
+
visionModel: visionInfo?.visionModel ?? null,
|
|
456
|
+
visionAdvisory: visionInfo?.visionAdvisory ?? null,
|
|
320
457
|
},
|
|
321
458
|
};
|
|
322
459
|
}
|
|
@@ -332,6 +469,7 @@ async function finalizeTaskRoute({
|
|
|
332
469
|
previousContextSnapshot = null,
|
|
333
470
|
recentOutputSnapshot = null,
|
|
334
471
|
previousRouteSummary = null,
|
|
472
|
+
escalationConfig = null,
|
|
335
473
|
} = {}) {
|
|
336
474
|
const absoluteRoot = path.resolve(rootDir);
|
|
337
475
|
const activeSkills = preparedRoute?.activeSkills ?? [];
|
|
@@ -399,8 +537,13 @@ async function finalizeTaskRoute({
|
|
|
399
537
|
routeSummary.continuationState = advanceContinuationState(
|
|
400
538
|
routeSummary.continuationState,
|
|
401
539
|
previousRouteSummary?.continuationState ?? null,
|
|
540
|
+
resolveEscalationSettings(escalationConfig),
|
|
402
541
|
);
|
|
403
542
|
applyRescueBiasToRouteSummary(routeSummary);
|
|
543
|
+
applyEscalationToRouteSummary(routeSummary, {
|
|
544
|
+
config: escalationConfig,
|
|
545
|
+
targetFile: routingContext.targetFile ?? null,
|
|
546
|
+
});
|
|
404
547
|
const approachSelector = routeSummary?.approachSelector ?? null;
|
|
405
548
|
|
|
406
549
|
return {
|
|
@@ -436,6 +579,24 @@ function printRouteState(state) {
|
|
|
436
579
|
if (state.routeSummary?.executionContract?.modelTier) {
|
|
437
580
|
console.log(`modelTier: ${state.routeSummary.executionContract.modelTier}`);
|
|
438
581
|
}
|
|
582
|
+
if (state.routeSummary?.escalatedTier) {
|
|
583
|
+
console.log(`escalatedTier: ${state.routeSummary.escalatedTier}`);
|
|
584
|
+
if (state.routeSummary.escalationReason) {
|
|
585
|
+
console.log(`escalationReason: ${state.routeSummary.escalationReason}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (state.routeSummary?.visionLane) {
|
|
589
|
+
console.log(`visionLane: ${state.routeSummary.visionLane}`);
|
|
590
|
+
if (state.routeSummary.visionModel) {
|
|
591
|
+
console.log(`visionModel: ${state.routeSummary.visionModel}`);
|
|
592
|
+
}
|
|
593
|
+
if (state.routeSummary.visionReason) {
|
|
594
|
+
console.log(`visionReason: ${state.routeSummary.visionReason}`);
|
|
595
|
+
}
|
|
596
|
+
if (state.routeSummary.visionAdvisory) {
|
|
597
|
+
console.log(`visionAdvisory: ${state.routeSummary.visionAdvisory}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
439
600
|
if (nextDisplay) {
|
|
440
601
|
console.log(`next: ${nextDisplay}`);
|
|
441
602
|
}
|
|
@@ -1355,6 +1516,7 @@ function buildRouteSummary({
|
|
|
1355
1516
|
);
|
|
1356
1517
|
const nextActionCommand = compactHelperLane ? null : nextAction?.command ?? null;
|
|
1357
1518
|
const handoffFile = routingContext.intentMode === 'handoff' ? 'docs/AI_HANDOFF/ACTIVE.md' : null;
|
|
1519
|
+
const visionLane = Boolean(routingContext.visionLane);
|
|
1358
1520
|
const line = [
|
|
1359
1521
|
routingContext.taskType ? `task=${routingContext.taskType}` : null,
|
|
1360
1522
|
handoffFile ? `handoff=${handoffFile}` : null,
|
|
@@ -1386,6 +1548,12 @@ function buildRouteSummary({
|
|
|
1386
1548
|
nextActionCommand,
|
|
1387
1549
|
helperHint,
|
|
1388
1550
|
contextMode,
|
|
1551
|
+
...(visionLane ? {
|
|
1552
|
+
visionLane: true,
|
|
1553
|
+
visionReason: routingContext.visionReason ?? null,
|
|
1554
|
+
...(routingContext.visionModel ? { visionModel: routingContext.visionModel } : {}),
|
|
1555
|
+
...(routingContext.visionAdvisory ? { visionAdvisory: routingContext.visionAdvisory } : {}),
|
|
1556
|
+
} : {}),
|
|
1389
1557
|
line: line || 'task=unknown',
|
|
1390
1558
|
};
|
|
1391
1559
|
}
|
|
@@ -1876,11 +2044,114 @@ function buildContinuationState({
|
|
|
1876
2044
|
};
|
|
1877
2045
|
}
|
|
1878
2046
|
|
|
1879
|
-
|
|
2047
|
+
// --- Escalation (TASK-009) ------------------------------------------------------------------
|
|
2048
|
+
// Consumes the repeatCount/rescueMode signal `advanceContinuationState` already computes and
|
|
2049
|
+
// exposes it to model-tier selection. Escalation is a COST ladder (lite -> code -> smart),
|
|
2050
|
+
// orthogonal to the vision capability lane (TASK-007): 'vision' must never appear in tierOrder
|
|
2051
|
+
// and escalatedTier must never take the value 'vision'. Thresholds are read from
|
|
2052
|
+
// orchestration.escalation (TASK-001); when config is absent or malformed, every threshold falls
|
|
2053
|
+
// back to the literal `2` / `3` that shipped before this change, so an unconfigured project sees
|
|
2054
|
+
// byte-identical behaviour.
|
|
2055
|
+
const DEFAULT_ESCALATION_DEBUG_LOOP_THRESHOLD = 2;
|
|
2056
|
+
const DEFAULT_ESCALATION_STUCK_RISK_THRESHOLD = 3;
|
|
2057
|
+
const DEFAULT_ESCALATION_TIER_ORDER = ['lite', 'code', 'smart'];
|
|
2058
|
+
const DEFAULT_ESCALATION_CAP = 'smart';
|
|
2059
|
+
|
|
2060
|
+
function resolveEscalationSettings(config = null) {
|
|
2061
|
+
const raw = config?.orchestration?.escalation;
|
|
2062
|
+
const enabled = typeof raw?.enabled === 'boolean' ? raw.enabled : true;
|
|
2063
|
+
const debugLoopThreshold = Number.isFinite(raw?.debugLoopThreshold) && raw.debugLoopThreshold > 0
|
|
2064
|
+
? raw.debugLoopThreshold
|
|
2065
|
+
: DEFAULT_ESCALATION_DEBUG_LOOP_THRESHOLD;
|
|
2066
|
+
const stuckRiskThreshold = Number.isFinite(raw?.stuckRiskThreshold) && raw.stuckRiskThreshold > 0
|
|
2067
|
+
? raw.stuckRiskThreshold
|
|
2068
|
+
: DEFAULT_ESCALATION_STUCK_RISK_THRESHOLD;
|
|
2069
|
+
const tierOrderCandidate = Array.isArray(raw?.tierOrder) && raw.tierOrder.length > 0
|
|
2070
|
+
&& raw.tierOrder.every((tier) => typeof tier === 'string' && tier.trim())
|
|
2071
|
+
? raw.tierOrder
|
|
2072
|
+
: DEFAULT_ESCALATION_TIER_ORDER;
|
|
2073
|
+
// 'vision' is a capability lane, never a cost rung — never let it into the escalation ladder.
|
|
2074
|
+
const tierOrder = tierOrderCandidate.includes('vision')
|
|
2075
|
+
? DEFAULT_ESCALATION_TIER_ORDER
|
|
2076
|
+
: tierOrderCandidate;
|
|
2077
|
+
const cap = typeof raw?.cap === 'string' && tierOrder.includes(raw.cap)
|
|
2078
|
+
? raw.cap
|
|
2079
|
+
: (tierOrder.includes(DEFAULT_ESCALATION_CAP) ? DEFAULT_ESCALATION_CAP : tierOrder[tierOrder.length - 1]);
|
|
2080
|
+
|
|
2081
|
+
return {
|
|
2082
|
+
enabled, debugLoopThreshold, stuckRiskThreshold, tierOrder, cap,
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/**
|
|
2087
|
+
* Escalates exactly one rung above `modelTier` on `tierOrder`, capped at `cap`. Returns null when
|
|
2088
|
+
* escalation is disabled, `repeatCount` hasn't reached `debugLoopThreshold`, or `modelTier` isn't
|
|
2089
|
+
* on the cost ladder (e.g. no contract selected). Never returns 'vision'.
|
|
2090
|
+
*/
|
|
2091
|
+
function computeEscalation({
|
|
2092
|
+
modelTier = null,
|
|
2093
|
+
repeatCount = null,
|
|
2094
|
+
config = null,
|
|
2095
|
+
targetFile = null,
|
|
2096
|
+
} = {}) {
|
|
2097
|
+
if (!modelTier || !Number.isFinite(repeatCount)) {
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2100
|
+
const settings = resolveEscalationSettings(config);
|
|
2101
|
+
if (!settings.enabled || repeatCount < settings.debugLoopThreshold) {
|
|
2102
|
+
return null;
|
|
2103
|
+
}
|
|
2104
|
+
const currentIndex = settings.tierOrder.indexOf(modelTier);
|
|
2105
|
+
if (currentIndex === -1) {
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
const capIndex = settings.tierOrder.indexOf(settings.cap);
|
|
2109
|
+
const effectiveCapIndex = capIndex === -1 ? settings.tierOrder.length - 1 : capIndex;
|
|
2110
|
+
const escalatedIndex = Math.min(currentIndex + 1, effectiveCapIndex);
|
|
2111
|
+
const escalatedTier = settings.tierOrder[escalatedIndex];
|
|
2112
|
+
|
|
2113
|
+
return {
|
|
2114
|
+
escalatedTier,
|
|
2115
|
+
escalationReason: `repeat-count=${repeatCount} threshold=${settings.debugLoopThreshold} file=${targetFile ?? 'null'}`,
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
/** Attaches escalatedTier/escalationReason to routeSummary, additive alongside modelTier. */
|
|
2120
|
+
function applyEscalationToRouteSummary(routeSummary = null, { config = null, targetFile = null } = {}) {
|
|
2121
|
+
if (!routeSummary || typeof routeSummary !== 'object') {
|
|
2122
|
+
return routeSummary;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
const escalation = computeEscalation({
|
|
2126
|
+
modelTier: routeSummary.executionContract?.modelTier ?? null,
|
|
2127
|
+
repeatCount: routeSummary.continuationState?.repeatCount ?? null,
|
|
2128
|
+
config,
|
|
2129
|
+
targetFile,
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
if (escalation) {
|
|
2133
|
+
routeSummary.escalatedTier = escalation.escalatedTier;
|
|
2134
|
+
routeSummary.escalationReason = escalation.escalationReason;
|
|
2135
|
+
} else {
|
|
2136
|
+
delete routeSummary.escalatedTier;
|
|
2137
|
+
delete routeSummary.escalationReason;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
return routeSummary;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function advanceContinuationState(continuationState = null, previousContinuationState = null, thresholds = null) {
|
|
1880
2144
|
if (!continuationState || typeof continuationState !== 'object') {
|
|
1881
2145
|
return continuationState;
|
|
1882
2146
|
}
|
|
1883
2147
|
|
|
2148
|
+
const debugLoopThreshold = Number.isFinite(thresholds?.debugLoopThreshold)
|
|
2149
|
+
? thresholds.debugLoopThreshold
|
|
2150
|
+
: DEFAULT_ESCALATION_DEBUG_LOOP_THRESHOLD;
|
|
2151
|
+
const stuckRiskThreshold = Number.isFinite(thresholds?.stuckRiskThreshold)
|
|
2152
|
+
? thresholds.stuckRiskThreshold
|
|
2153
|
+
: DEFAULT_ESCALATION_STUCK_RISK_THRESHOLD;
|
|
2154
|
+
|
|
1884
2155
|
if (!continuationState.required) {
|
|
1885
2156
|
return {
|
|
1886
2157
|
...continuationState,
|
|
@@ -1903,7 +2174,7 @@ function advanceContinuationState(continuationState = null, previousContinuation
|
|
|
1903
2174
|
const repeatCount = sameMilestone
|
|
1904
2175
|
? Math.max(1, Number(previousContinuationState?.repeatCount ?? 1) + 1)
|
|
1905
2176
|
: 1;
|
|
1906
|
-
const rescueMode = repeatCount >=
|
|
2177
|
+
const rescueMode = repeatCount >= debugLoopThreshold
|
|
1907
2178
|
? (
|
|
1908
2179
|
currentReasons.includes('pending-bounded-context')
|
|
1909
2180
|
? 'finish-current-milestone-before-more-reading'
|
|
@@ -1914,14 +2185,14 @@ function advanceContinuationState(continuationState = null, previousContinuation
|
|
|
1914
2185
|
)
|
|
1915
2186
|
)
|
|
1916
2187
|
: null;
|
|
1917
|
-
const stuckRisk = repeatCount >=
|
|
2188
|
+
const stuckRisk = repeatCount >= stuckRiskThreshold
|
|
1918
2189
|
? 'high'
|
|
1919
|
-
: (repeatCount ===
|
|
1920
|
-
const wideningBlocked = repeatCount >=
|
|
1921
|
-
const nextMilestone = repeatCount >=
|
|
2190
|
+
: (repeatCount === debugLoopThreshold ? 'elevated' : null);
|
|
2191
|
+
const wideningBlocked = repeatCount >= debugLoopThreshold && Boolean(rescueMode);
|
|
2192
|
+
const nextMilestone = repeatCount >= debugLoopThreshold && currentReasons.includes('missing-write-evidence')
|
|
1922
2193
|
? 'finish-write-before-more-analysis'
|
|
1923
2194
|
: (
|
|
1924
|
-
repeatCount >=
|
|
2195
|
+
repeatCount >= debugLoopThreshold && currentReasons.includes('pending-bounded-context')
|
|
1925
2196
|
? 'finish-current-milestone-before-more-reading'
|
|
1926
2197
|
: continuationState.nextMilestone
|
|
1927
2198
|
);
|
|
@@ -2376,6 +2647,8 @@ function buildRouteRequestKey({
|
|
|
2376
2647
|
contextIntent: routingContext.contextIntent ?? '',
|
|
2377
2648
|
taskType: routingContext.taskType ?? '',
|
|
2378
2649
|
executionMode: routingContext.executionMode ?? '',
|
|
2650
|
+
visionLane: Boolean(routingContext.visionLane),
|
|
2651
|
+
visionReason: routingContext.visionReason ?? '',
|
|
2379
2652
|
});
|
|
2380
2653
|
}
|
|
2381
2654
|
|
|
@@ -2559,6 +2832,16 @@ function compactRouteSummary(routeSummary = null) {
|
|
|
2559
2832
|
nextActionType: routeSummary.nextActionType ?? null,
|
|
2560
2833
|
nextActionCommand: routeSummary.nextActionCommand ?? null,
|
|
2561
2834
|
helperHint: routeSummary.helperHint ?? null,
|
|
2835
|
+
...(routeSummary.escalatedTier ? {
|
|
2836
|
+
escalatedTier: routeSummary.escalatedTier,
|
|
2837
|
+
escalationReason: routeSummary.escalationReason ?? null,
|
|
2838
|
+
} : {}),
|
|
2839
|
+
...(routeSummary.visionLane ? {
|
|
2840
|
+
visionLane: true,
|
|
2841
|
+
visionReason: routeSummary.visionReason ?? null,
|
|
2842
|
+
...(routeSummary.visionModel ? { visionModel: routeSummary.visionModel } : {}),
|
|
2843
|
+
...(routeSummary.visionAdvisory ? { visionAdvisory: routeSummary.visionAdvisory } : {}),
|
|
2844
|
+
} : {}),
|
|
2562
2845
|
};
|
|
2563
2846
|
}
|
|
2564
2847
|
|