@ngockhoale/ukit 3.4.29 → 3.4.31
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 +29 -0
- package/manifests/documentation.yaml +100 -0
- package/package.json +1 -1
- package/src/core/executionContracts.js +32 -0
- package/src/core/permissionPolicy.js +1 -1
- package/src/decision/client.js +15 -0
- package/src/decision/protocol.js +12 -14
- package/src/index/buildIndex.js +244 -4
- package/src/index/impactContext.js +91 -11
- package/src/index/languageTools.js +126 -0
- package/src/index/paths.js +5 -1
- package/src/index/playbookRegistry.js +108 -9
- package/src/index/queryIndex.js +415 -1
- package/src/index/routeCatalog.js +1 -1
- package/src/index/routeResolver.js +2 -2
- package/src/index/taskRouting.js +28 -4
- package/template_project/.claude/commands/ukit/handoff-create.md +5 -0
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +43 -4
- package/template_project/.claude/hooks/block-dangerous.mjs +7 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +235 -21
- package/template_project/.claude/hooks/record-execution.mjs +21 -0
- package/template_project/.claude/hooks/skill-router.sh +187 -16
- package/template_project/.claude/settings.json +43 -1
- package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
- package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +10 -4
- package/template_project/.claude/ukit/index/impact-context.mjs +70 -15
- package/template_project/.claude/ukit/index/land-worktree.mjs +2 -2
- package/template_project/.claude/ukit/index/lib/index-core.mjs +883 -14
- package/template_project/.claude/ukit/index/playbook-registry.mjs +108 -9
- package/template_project/.claude/ukit/index/query-index.mjs +100 -7
- package/template_project/.claude/ukit/index/review-verdict.mjs +14 -4
- package/template_project/.claude/ukit/index/route-catalog.mjs +1 -1
- package/template_project/.claude/ukit/index/route-resolver.mjs +34 -2
- package/template_project/.claude/ukit/index/route-task.mjs +243 -38
- package/template_project/.claude/ukit/index/sidecar-decision.mjs +3 -59
- package/template_project/.claude/ukit/index/unic-decision.mjs +27 -14
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +43 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +612 -56
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +122 -6
- package/template_project/.claude/ukit/runtime/loop-state.mjs +1 -1
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +87 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +417 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +635 -28
- package/template_project/.omp/hooks/pre/ukit-bridge.js +40 -5
- package/template_project/AGENTS.md +1 -1
- package/template_project/CLAUDE.md +1 -1
- package/template_project/instructions/core.md +1 -1
- package/template_user/playbooks/feature-implementation.md +9 -0
- package/template_user/playbooks/migration.md +4 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { classifyTaskSemantics, applyTaskSemantics } from './route-resolver.mjs';
|
|
2
3
|
import fs from 'node:fs/promises';
|
|
3
4
|
// `fsSync` aliases the SAME fs module — used ONLY by the process.on('exit')
|
|
4
5
|
// audit-sidecar fold below (event loop is gone; async cannot run) and by the
|
|
@@ -70,7 +71,7 @@ import {
|
|
|
70
71
|
deriveRouteFields,
|
|
71
72
|
} from './route-resolver.mjs';
|
|
72
73
|
import { applyMeasuredTier, loadTierMap } from './tier-selection.mjs';
|
|
73
|
-
import { resolvePlaybookRoute, loadRegistry } from './playbook-registry.mjs';
|
|
74
|
+
import { resolvePlaybookRoute, loadRegistry, deriveRouteOrchestrationSignals } from './playbook-registry.mjs';
|
|
74
75
|
import { extractHistorySignalsAsync } from './session-history-extractor.mjs';
|
|
75
76
|
import {
|
|
76
77
|
applyFixLoopEscalation,
|
|
@@ -190,6 +191,15 @@ If the user says "new task", re-route — do not treat the message as the next s
|
|
|
190
191
|
4. Implement the smallest change satisfying the predicate.
|
|
191
192
|
5. Verify against the predicate on the real artifact — not "it compiles".
|
|
192
193
|
6. Widen once: check the impact surface the route named, no broader.
|
|
194
|
+
Reference reuse: when the ask points at an existing surface ("like the Customer
|
|
195
|
+
page", "giống trang X", "same as module Y"), read that surface first and mirror
|
|
196
|
+
its structure — the reference defines the done predicate, not just inspiration.
|
|
197
|
+
Reuse-first: when the ask names an existing function, helper, or pattern
|
|
198
|
+
("using validation from Z", "dùng hàm có sẵn"), reuse it — extend or wrap
|
|
199
|
+
before writing new logic, and name the reuse in the report.
|
|
200
|
+
Clone: when the ask duplicates a feature ("clone the Customer page", "nhân bản
|
|
201
|
+
tính năng X"), copy the source feature's structure, rename and adapt each
|
|
202
|
+
surface, and verify the clone against the same predicate class as the original.
|
|
193
203
|
Reply: what changed, the predicate, the evidence it now holds.
|
|
194
204
|
Ask the human only for: irreversible writes, a genuine preference call no experiment
|
|
195
205
|
settles, or a real dead end. Everything else: do it, report it.`,
|
|
@@ -790,6 +800,7 @@ function buildRouteHotCacheState(sharedState = {}) {
|
|
|
790
800
|
delete routingContext.lastExplicitUserPromptText;
|
|
791
801
|
delete routingContext.commandText;
|
|
792
802
|
delete state.sessionId;
|
|
803
|
+
delete state.taskInstanceId;
|
|
793
804
|
state.routingContext = routingContext;
|
|
794
805
|
return state;
|
|
795
806
|
}
|
|
@@ -886,6 +897,7 @@ function buildReducedRouteState({ input, reason }) {
|
|
|
886
897
|
source: 'task-route',
|
|
887
898
|
requestKey: null,
|
|
888
899
|
sessionId: input.sessionId ?? null,
|
|
900
|
+
taskInstanceId: input.taskInstanceId || undefined,
|
|
889
901
|
});
|
|
890
902
|
}
|
|
891
903
|
|
|
@@ -902,7 +914,7 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
|
|
|
902
914
|
const absoluteRoot = path.resolve(input.rootDir ?? process.cwd());
|
|
903
915
|
const budget = createRouteBudget({ signal, deadlineMs });
|
|
904
916
|
const hotCache = cache ?? createDefaultRouteHotCache({ rootDir: absoluteRoot });
|
|
905
|
-
const normalizedInput = { ...input, rootDir: absoluteRoot };
|
|
917
|
+
const normalizedInput = { ...input, rootDir: absoluteRoot, taskInstanceId: input.taskInstanceId || `task-${crypto.randomBytes(16).toString('hex')}` };
|
|
906
918
|
const baseMeta = {
|
|
907
919
|
cacheHit: false,
|
|
908
920
|
reduced: false,
|
|
@@ -920,7 +932,7 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
|
|
|
920
932
|
elapsedMs: Date.now() - budget.startedAt,
|
|
921
933
|
ioReads: budget.ioReads,
|
|
922
934
|
};
|
|
923
|
-
return { ...state, cacheHit: routeMeta.cacheHit, routeMeta };
|
|
935
|
+
return { ...state, taskInstanceId: normalizedInput.taskInstanceId || state.taskInstanceId || `task-${crypto.randomBytes(16).toString('hex')}`, cacheHit: routeMeta.cacheHit, routeMeta };
|
|
924
936
|
};
|
|
925
937
|
const reduce = (reason) => finish(
|
|
926
938
|
buildReducedRouteState({ input: normalizedInput, reason }),
|
|
@@ -1070,6 +1082,7 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
|
|
|
1070
1082
|
requestKey,
|
|
1071
1083
|
indexGeneratedAtMs,
|
|
1072
1084
|
sessionId: normalizedInput.sessionId ?? null,
|
|
1085
|
+
taskInstanceId: normalizedInput.taskInstanceId || undefined,
|
|
1073
1086
|
});
|
|
1074
1087
|
|
|
1075
1088
|
// Phase 3 — store under a POST-route fingerprint so the next identical call (whose own
|
|
@@ -1117,7 +1130,12 @@ async function main() {
|
|
|
1117
1130
|
const sessionId = normalizeSessionId(readFlagValue(args, '--session-id'));
|
|
1118
1131
|
const previousStateOwnershipBlocked = hasRouteStateOwner(persistedPreviousState)
|
|
1119
1132
|
&& !isRouteStateCompatible(persistedPreviousState, sessionId);
|
|
1120
|
-
const
|
|
1133
|
+
const sessionSidecar = previousStateOwnershipBlocked && sessionId
|
|
1134
|
+
? await readJson(path.join(rootDir, '.ukit', 'storage', 'cache', 'route-state', `${sessionId.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'default'}.json`), {})
|
|
1135
|
+
: {};
|
|
1136
|
+
const previousState = previousStateOwnershipBlocked
|
|
1137
|
+
? (isRouteStateCompatible(sessionSidecar, sessionId) && sessionSidecar.sessionId === sessionId ? sessionSidecar : {})
|
|
1138
|
+
: persistedPreviousState;
|
|
1121
1139
|
const escalationConfig = await readMergedConfig(rootDir);
|
|
1122
1140
|
const targetFile = readFlagValue(args, '--target');
|
|
1123
1141
|
const taskType = readFlagValue(args, '--type');
|
|
@@ -1135,7 +1153,7 @@ async function main() {
|
|
|
1135
1153
|
const commandNamespace = adapter === 'codex'
|
|
1136
1154
|
? '.codex'
|
|
1137
1155
|
: (adapter === 'antigravity' ? '.antigravity' : '.claude');
|
|
1138
|
-
|
|
1156
|
+
let promptText = args
|
|
1139
1157
|
.filter((arg, index) => !isFlagOrValue(args, index))
|
|
1140
1158
|
.join(' ')
|
|
1141
1159
|
.trim();
|
|
@@ -1146,6 +1164,15 @@ async function main() {
|
|
|
1146
1164
|
return;
|
|
1147
1165
|
}
|
|
1148
1166
|
|
|
1167
|
+
const continuingTask = Boolean(previousState.taskInstanceId && lastPrompt
|
|
1168
|
+
&& /^(?:continue|resume|keep going|proceed|tiep tuc)(?:\s+(?:the task|this task|previous task))?[.!]?$/i.test(
|
|
1169
|
+
promptText.normalize('NFD').replace(/[̀-ͯ]/g, '').trim()));
|
|
1170
|
+
const taskInstanceId = readFlagValue(args, '--task-instance-id')
|
|
1171
|
+
|| (((['skill-router', 'skill-router-provisional'].includes(previousState.source)
|
|
1172
|
+
&& previousState.routingContext?.lastExplicitUserPromptText === promptText) || continuingTask)
|
|
1173
|
+
&& previousState.taskInstanceId
|
|
1174
|
+
? previousState.taskInstanceId : `task-${crypto.randomBytes(16).toString('hex')}`);
|
|
1175
|
+
if (continuingTask) promptText = lastPrompt;
|
|
1149
1176
|
const preparedRoute = await prepareTaskRoute({
|
|
1150
1177
|
rootDir,
|
|
1151
1178
|
promptText,
|
|
@@ -1240,7 +1267,12 @@ async function main() {
|
|
|
1240
1267
|
historySignals,
|
|
1241
1268
|
};
|
|
1242
1269
|
}
|
|
1243
|
-
|
|
1270
|
+
const sharedState = { ...reusableState, taskInstanceId, source: 'task-route', ts: Date.now() };
|
|
1271
|
+
if (canPersistRouteState) {
|
|
1272
|
+
try { await persistSharedRouteState(sharedStatePath, sharedState); }
|
|
1273
|
+
catch (error) { console.error(`route-task: state persist degraded — ${error?.message ?? error}`); }
|
|
1274
|
+
}
|
|
1275
|
+
printRouteState(sharedState);
|
|
1244
1276
|
return;
|
|
1245
1277
|
}
|
|
1246
1278
|
|
|
@@ -1322,6 +1354,7 @@ async function main() {
|
|
|
1322
1354
|
}
|
|
1323
1355
|
const sharedState = {
|
|
1324
1356
|
...cachedRouteState,
|
|
1357
|
+
taskInstanceId,
|
|
1325
1358
|
...(sessionId ? { sessionId } : {}),
|
|
1326
1359
|
source: 'task-route',
|
|
1327
1360
|
ts: Date.now(),
|
|
@@ -1382,6 +1415,7 @@ async function main() {
|
|
|
1382
1415
|
requestKey,
|
|
1383
1416
|
indexGeneratedAtMs,
|
|
1384
1417
|
sessionId,
|
|
1418
|
+
taskInstanceId,
|
|
1385
1419
|
});
|
|
1386
1420
|
if (canPersistRouteState) {
|
|
1387
1421
|
// Advisory persistence — an EACCES/ENOSPC under the lock must never lose
|
|
@@ -1464,6 +1498,56 @@ async function prepareTaskRoute({
|
|
|
1464
1498
|
const normalizedPrompt = String(promptText || '').trim();
|
|
1465
1499
|
const normalizedCommand = String(commandText || '').trim();
|
|
1466
1500
|
const normalizedTarget = normalizeRelativeFile(absoluteRoot, targetFile);
|
|
1501
|
+
// Greeting fast-path: a pure social turn short-circuits BEFORE any skill
|
|
1502
|
+
// scan, index work or vision resolution. executionScores/candidates keep
|
|
1503
|
+
// the zero-signal shape downstream consumers expect; the mode is stamped
|
|
1504
|
+
// informational and routingContext.greetingFastPath tells finalizeTaskRoute
|
|
1505
|
+
// to skip the playbook scan and the decision-plane shadow.
|
|
1506
|
+
if (isPureGreetingPrompt({
|
|
1507
|
+
promptText: normalizedPrompt,
|
|
1508
|
+
commandText: normalizedCommand,
|
|
1509
|
+
targetFile: normalizedTarget,
|
|
1510
|
+
})) {
|
|
1511
|
+
const greetingScores = deriveExecutionScores({
|
|
1512
|
+
promptText: '',
|
|
1513
|
+
commandText: '',
|
|
1514
|
+
targetFile: null,
|
|
1515
|
+
intentMode: null,
|
|
1516
|
+
taskType: '',
|
|
1517
|
+
});
|
|
1518
|
+
return {
|
|
1519
|
+
rootDir: absoluteRoot,
|
|
1520
|
+
activeSkills: [],
|
|
1521
|
+
routingContext: {
|
|
1522
|
+
adapter: String(adapter || 'claude').trim() || 'claude',
|
|
1523
|
+
promptText: normalizedPrompt,
|
|
1524
|
+
lastExplicitUserPromptText: normalizedPrompt || String(lastExplicitUserPromptText || '').trim(),
|
|
1525
|
+
commandText: normalizedCommand,
|
|
1526
|
+
targetFile: null,
|
|
1527
|
+
contextIntent: '',
|
|
1528
|
+
taskType: '',
|
|
1529
|
+
intentMode: null,
|
|
1530
|
+
executionScores: greetingScores,
|
|
1531
|
+
executionCandidates: buildExecutionModeCandidates({
|
|
1532
|
+
promptText: '',
|
|
1533
|
+
commandText: '',
|
|
1534
|
+
targetFile: null,
|
|
1535
|
+
intentMode: null,
|
|
1536
|
+
executionScores: greetingScores,
|
|
1537
|
+
}),
|
|
1538
|
+
executionMode: 'informational',
|
|
1539
|
+
greetingFastPath: true,
|
|
1540
|
+
orchestrationSignals: deriveRouteOrchestrationSignals({
|
|
1541
|
+
promptText: normalizedPrompt,
|
|
1542
|
+
commandText: normalizedCommand,
|
|
1543
|
+
}),
|
|
1544
|
+
visionLane: false,
|
|
1545
|
+
visionReason: null,
|
|
1546
|
+
visionModel: null,
|
|
1547
|
+
visionAdvisory: null,
|
|
1548
|
+
},
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1467
1551
|
const intentMode = deriveIntentMode({
|
|
1468
1552
|
promptText: normalizedPrompt,
|
|
1469
1553
|
commandText: normalizedCommand,
|
|
@@ -1520,6 +1604,10 @@ async function prepareTaskRoute({
|
|
|
1520
1604
|
targetFile: normalizedTarget,
|
|
1521
1605
|
visionFlag: Boolean(visionFlag),
|
|
1522
1606
|
});
|
|
1607
|
+
const orchestrationSignals = deriveRouteOrchestrationSignals({
|
|
1608
|
+
promptText: normalizedPrompt,
|
|
1609
|
+
commandText: normalizedCommand,
|
|
1610
|
+
});
|
|
1523
1611
|
|
|
1524
1612
|
return {
|
|
1525
1613
|
rootDir: absoluteRoot,
|
|
@@ -1536,6 +1624,7 @@ async function prepareTaskRoute({
|
|
|
1536
1624
|
executionScores,
|
|
1537
1625
|
executionCandidates,
|
|
1538
1626
|
executionMode,
|
|
1627
|
+
orchestrationSignals,
|
|
1539
1628
|
visionLane: Boolean(visionInfo?.visionLane),
|
|
1540
1629
|
visionReason: visionInfo?.visionReason ?? null,
|
|
1541
1630
|
visionModel: visionInfo?.visionModel ?? null,
|
|
@@ -1727,6 +1816,9 @@ export async function runRouteDecisionShadow({
|
|
|
1727
1816
|
} = {}) {
|
|
1728
1817
|
const stage = resolveDecisionPlaneStage(config);
|
|
1729
1818
|
if (stage === 'off') return null;
|
|
1819
|
+
// Greeting fast-path: a social turn carries nothing the decision plane could
|
|
1820
|
+
// answer — never spawn the adapter shadow for one.
|
|
1821
|
+
if (routingContext?.greetingFastPath) return null;
|
|
1730
1822
|
const questions = buildShadowDecisionQuestions(config);
|
|
1731
1823
|
if (questions.length === 0) {
|
|
1732
1824
|
return { stage, outcomeClass: 'skipped', decisionKeys: [], probabilityBands: {},
|
|
@@ -2046,6 +2138,9 @@ export async function emitResumableRun({
|
|
|
2046
2138
|
} = {}) {
|
|
2047
2139
|
const stage = resolveResumableRunStage(config);
|
|
2048
2140
|
if (stage === 'off') return null;
|
|
2141
|
+
// Greeting fast-path: a social turn starts no goal — never persist a
|
|
2142
|
+
// resumable run record (or ledger pointer) for one.
|
|
2143
|
+
if (routingContext?.greetingFastPath) return null;
|
|
2049
2144
|
try {
|
|
2050
2145
|
const taskBoundary = resumableTaskBoundary(routingContext);
|
|
2051
2146
|
const taskId = resumableTaskId({ sessionId, taskBoundary });
|
|
@@ -2205,34 +2300,45 @@ async function finalizeTaskRoute({
|
|
|
2205
2300
|
});
|
|
2206
2301
|
// TASK-007 (BL-009): playbook resolution rides the same route signals —
|
|
2207
2302
|
// the registry degrades to playbookId: null + reason and never blocks the
|
|
2208
|
-
// route. Same merge the hook performs via mergeResolverFields.
|
|
2303
|
+
// route. Same merge the hook performs via mergeResolverFields. A greeting
|
|
2304
|
+
// fast-path route skips the registry scan entirely: no playbook can claim a
|
|
2305
|
+
// social turn, so the route records the skip honestly (reason 'greeting').
|
|
2209
2306
|
try {
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
intentMode: routingContext.intentMode ?? null,
|
|
2216
|
-
escalationTriggers: routeSummary?.escalationTriggers ?? [],
|
|
2217
|
-
projectRoot: absoluteRoot,
|
|
2218
|
-
homeDir: os.homedir(),
|
|
2219
|
-
});
|
|
2220
|
-
routeSummary.playbookId = playbookResolution.playbookId;
|
|
2221
|
-
routeSummary.workGroup = playbookResolution.workGroup;
|
|
2222
|
-
routeSummary.playbookReason = playbookResolution.reason;
|
|
2223
|
-
routeSummary.playbookLoad = playbookResolution.playbookLoad;
|
|
2224
|
-
// BL-010 (ARCH Task Contract): `path` = direct|handoff; `fastPathKind` is
|
|
2225
|
-
// the ARCH `none|small-feature|bug-fix` enum — named Kind because
|
|
2226
|
-
// routeSummary.fastPath already carries the FR-004 eligibility object.
|
|
2227
|
-
// Keys land only when a routing-table row claimed the prompt (workGroup
|
|
2228
|
-
// non-null) — identical key-presence to deriveTaskRoute so schema-off /
|
|
2229
|
-
// no-match routes never grow stray keys.
|
|
2230
|
-
if (playbookResolution.workGroup != null) {
|
|
2231
|
-
routeSummary.path = playbookResolution.path;
|
|
2232
|
-
routeSummary.fastPathKind = playbookResolution.fastPath;
|
|
2233
|
-
} else {
|
|
2307
|
+
if (routingContext.greetingFastPath) {
|
|
2308
|
+
routeSummary.playbookId = null;
|
|
2309
|
+
routeSummary.workGroup = null;
|
|
2310
|
+
routeSummary.playbookReason = 'greeting';
|
|
2311
|
+
routeSummary.playbookLoad = null;
|
|
2234
2312
|
delete routeSummary.path;
|
|
2235
2313
|
delete routeSummary.fastPathKind;
|
|
2314
|
+
} else {
|
|
2315
|
+
const playbookResolution = await resolvePlaybookRoute({
|
|
2316
|
+
promptText: routingContext.promptText,
|
|
2317
|
+
commandText: routingContext.commandText,
|
|
2318
|
+
targetFile: routingContext.targetFile ?? null,
|
|
2319
|
+
executionMode: routingContext.executionMode ?? null,
|
|
2320
|
+
intentMode: routingContext.intentMode ?? null,
|
|
2321
|
+
escalationTriggers: routeSummary?.escalationTriggers ?? [],
|
|
2322
|
+
projectRoot: absoluteRoot,
|
|
2323
|
+
homeDir: os.homedir(),
|
|
2324
|
+
});
|
|
2325
|
+
routeSummary.playbookId = playbookResolution.playbookId;
|
|
2326
|
+
routeSummary.workGroup = playbookResolution.workGroup;
|
|
2327
|
+
routeSummary.playbookReason = playbookResolution.reason;
|
|
2328
|
+
routeSummary.playbookLoad = playbookResolution.playbookLoad;
|
|
2329
|
+
// BL-010 (ARCH Task Contract): `path` = direct|handoff; `fastPathKind` is
|
|
2330
|
+
// the ARCH `none|small-feature|bug-fix` enum — named Kind because
|
|
2331
|
+
// routeSummary.fastPath already carries the FR-004 eligibility object.
|
|
2332
|
+
// Keys land only when a routing-table row claimed the prompt (workGroup
|
|
2333
|
+
// non-null) — identical key-presence to deriveTaskRoute so schema-off /
|
|
2334
|
+
// no-match routes never grow stray keys.
|
|
2335
|
+
if (playbookResolution.workGroup != null) {
|
|
2336
|
+
routeSummary.path = playbookResolution.path;
|
|
2337
|
+
routeSummary.fastPathKind = playbookResolution.fastPath;
|
|
2338
|
+
} else {
|
|
2339
|
+
delete routeSummary.path;
|
|
2340
|
+
delete routeSummary.fastPathKind;
|
|
2341
|
+
}
|
|
2236
2342
|
}
|
|
2237
2343
|
} catch {
|
|
2238
2344
|
routeSummary.playbookId = null;
|
|
@@ -2982,7 +3088,7 @@ function hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext = false }
|
|
|
2982
3088
|
}
|
|
2983
3089
|
|
|
2984
3090
|
return /\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|implement|build|create|add|ship|deliver|refactor|integrate|integration|scaffold|feature|update|change|modify|inject|apply|review|audit|diff|pr feedback|code review|auth|login|api|endpoint|test|spec)\b/.test(lower)
|
|
2985
|
-
|| /(?<![A-Za-z0-9_])(sửa|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)(?![A-Za-z0-9_])/.test(raw);
|
|
3091
|
+
|| /(?<![A-Za-z0-9_])(sửa|đổi|thay|chỉnh|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)(?![A-Za-z0-9_])/.test(raw);
|
|
2986
3092
|
}
|
|
2987
3093
|
|
|
2988
3094
|
function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = false } = {}) {
|
|
@@ -3694,11 +3800,12 @@ function buildRouteSummary({
|
|
|
3694
3800
|
executionScores,
|
|
3695
3801
|
executionCandidates,
|
|
3696
3802
|
});
|
|
3697
|
-
const executionContract = buildExecutionContract(executionMode);
|
|
3803
|
+
const executionContract = applyTaskSemantics(buildExecutionContract(executionMode), classifyTaskSemantics({ promptText: routingContext.lastExplicitUserPromptText || routingContext.promptText, targetFile: routingContext.targetFile }));
|
|
3698
3804
|
const continuousExecution = buildContinuousExecutionPolicy(autonomyLevel);
|
|
3699
3805
|
const tierLane = buildTierLane({ modelTier: executionContract?.modelTier ?? null });
|
|
3700
3806
|
const completionState = buildCompletionState({
|
|
3701
3807
|
executionMode,
|
|
3808
|
+
executionContract,
|
|
3702
3809
|
verificationRecommendation,
|
|
3703
3810
|
});
|
|
3704
3811
|
const continuationState = buildContinuationState({
|
|
@@ -3811,6 +3918,11 @@ function buildRouteSummary({
|
|
|
3811
3918
|
promptText: routingContext.promptText,
|
|
3812
3919
|
commandText: routingContext.commandText,
|
|
3813
3920
|
});
|
|
3921
|
+
// Routing-orchestration signals (reference/clone/SQL/reuse/run-all): the
|
|
3922
|
+
// same booleans the playbook discriminators read. Purely advisory route
|
|
3923
|
+
// metadata — they never set mutation permission by themselves. Conditional
|
|
3924
|
+
// spreads keep every other route byte-identical.
|
|
3925
|
+
const orchestrationSignals = routingContext.orchestrationSignals ?? null;
|
|
3814
3926
|
return {
|
|
3815
3927
|
primaryCommands,
|
|
3816
3928
|
fallbackCommands,
|
|
@@ -3876,6 +3988,12 @@ function buildRouteSummary({
|
|
|
3876
3988
|
// FR-004 additive field — present only when the prompt is an advisor/reference/replan
|
|
3877
3989
|
// request, so every other route stays byte-identical.
|
|
3878
3990
|
...(advisorIntent ? { advisorIntent } : {}),
|
|
3991
|
+
// Routing-orchestration signals — present only when detected.
|
|
3992
|
+
...(orchestrationSignals?.referenceTarget ? { referenceTargets: true } : {}),
|
|
3993
|
+
...(orchestrationSignals?.cloneIntent ? { cloneIntent: true } : {}),
|
|
3994
|
+
...(orchestrationSignals?.sqlIntent ? { sqlIntent: true } : {}),
|
|
3995
|
+
...(orchestrationSignals?.reuseFirst ? { reuseFirst: true } : {}),
|
|
3996
|
+
...(orchestrationSignals?.runAll ? { goalScope: 'run-all' } : {}),
|
|
3879
3997
|
line: line || 'task=unknown',
|
|
3880
3998
|
};
|
|
3881
3999
|
}
|
|
@@ -3908,8 +4026,16 @@ function deriveExecutionScores({
|
|
|
3908
4026
|
} = {}) {
|
|
3909
4027
|
const raw = `${promptText ?? ''}\n${commandText ?? ''}`.toLowerCase();
|
|
3910
4028
|
const sharedRisk = isSharedImpactFile(targetFile);
|
|
3911
|
-
|
|
3912
|
-
|
|
4029
|
+
// Vietnamese edit verbs (\b never matches around diacritics — JS word chars
|
|
4030
|
+
// are ASCII-only), so a small Vietnamese edit like "đổi màu nút thành đỏ"
|
|
4031
|
+
// or "sửa cột …" scores as the small mutating task it is instead of
|
|
4032
|
+
// falling to the all-zero shared-edit floor.
|
|
4033
|
+
const vietnameseEditVerb = /(?<![A-Za-z0-9_])(?:sửa|sua|đổi|doi|thay|chỉnh|chinh|cập\s+nhật|cap\s+nhat)(?![A-Za-z0-9_])/.test(raw);
|
|
4034
|
+
const directTransformSignal = /\b(change|replace|set|rename|convert|swap)\b/.test(raw)
|
|
4035
|
+
|| /\bchange\s+.+\s+to\s+.+\b/.test(raw)
|
|
4036
|
+
// "đổi X thành Y" is the Vietnamese "change X to Y".
|
|
4037
|
+
|| /(?<![A-Za-z0-9_])(?:đổi|doi|sửa|sua|đặt|dat)\s+[^\n,.;]{1,48}?\s+(?:thành|thanh|to)\s+/.test(raw);
|
|
4038
|
+
const smallFixSignal = /\b(adjust|tweak|patch|fix|modify|update|apply)\b/.test(raw) || vietnameseEditVerb;
|
|
3913
4039
|
const buildSignal = /\b(build|create|add|implement|feature)\b/.test(raw);
|
|
3914
4040
|
const debugSignal = intentMode === 'debug-specific' || /\b(root cause|debug|triage|flaky|investigate|why)\b/.test(raw);
|
|
3915
4041
|
const reviewSignal = intentMode === 'review-specific' || /\b(review|audit|verify|release readiness)\b/.test(raw);
|
|
@@ -3917,6 +4043,7 @@ function deriveExecutionScores({
|
|
|
3917
4043
|
const impactSignal = /\b(map impact|impact|blast radius|all affected|across (?:runtime|templates|helpers|adapters|mirrors)|affected adapters|affected mirrors|inspect impact)\b/.test(raw);
|
|
3918
4044
|
const boundedEditSignal = /\b(one[- ]line|small|tiny|single|local|guard|log line|loading message|label)\b/.test(raw);
|
|
3919
4045
|
const implementSignal = /\b(implement|apply|update|modify|add|create|ship|deliver|fix)\b/.test(raw)
|
|
4046
|
+
|| vietnameseEditVerb
|
|
3920
4047
|
|| /\bchange\s+.+\s+to\s+.+\b/.test(raw);
|
|
3921
4048
|
const explicitTarget = Boolean(targetFile);
|
|
3922
4049
|
|
|
@@ -3979,6 +4106,49 @@ function deriveExecutionScores({
|
|
|
3979
4106
|
};
|
|
3980
4107
|
}
|
|
3981
4108
|
|
|
4109
|
+
// --- Greeting fast-path (routing orchestration) --------------------------------
|
|
4110
|
+
// A prompt that is ONLY a greeting/acknowledgment (≤ ~6 words, no task
|
|
4111
|
+
// vocabulary, no target file, no command) is a social turn, never work. It
|
|
4112
|
+
// used to fall through every signal gate with all-zero scores and the
|
|
4113
|
+
// no-target blastRadius=1 floor minted a shared-edit (mutating) lane + the
|
|
4114
|
+
// full pipeline (context snapshots, playbook scan, decision shadow). The
|
|
4115
|
+
// fast path stamps informational, loads no skill, resolves no playbook and
|
|
4116
|
+
// skips the shadow — deterministically, before any I/O.
|
|
4117
|
+
// Matched on diacritic-folded tokens so both Vietnamese spellings work.
|
|
4118
|
+
const GREETING_CORE_TOKENS = new Set([
|
|
4119
|
+
'hi', 'hello', 'hey', 'yo', 'chao', 'xin', 'good',
|
|
4120
|
+
'morning', 'afternoon', 'evening',
|
|
4121
|
+
'thanks', 'thank', 'cam', 'on', 'bye', 'tam', 'biet',
|
|
4122
|
+
]);
|
|
4123
|
+
const GREETING_FILLER_TOKENS = new Set([
|
|
4124
|
+
'you', 'u', 'there', 'guys', 'everyone', 'all',
|
|
4125
|
+
'ban', 'cac', 'minh', 'moi', 'nguoi', 'nhe', 'nha', 'nhieu',
|
|
4126
|
+
'ok', 'okay', 'again', 'still',
|
|
4127
|
+
]);
|
|
4128
|
+
function foldRouteText(text = '') {
|
|
4129
|
+
return String(text ?? '')
|
|
4130
|
+
.toLowerCase()
|
|
4131
|
+
.normalize('NFD')
|
|
4132
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
4133
|
+
.replace(/\u0111/g, 'd');
|
|
4134
|
+
}
|
|
4135
|
+
function isPureGreetingPrompt({ promptText = '', commandText = '', targetFile = null } = {}) {
|
|
4136
|
+
const raw = String(promptText ?? '').trim();
|
|
4137
|
+
if (!raw || String(commandText ?? '').trim() || targetFile) return false;
|
|
4138
|
+
if (raw.length > 64) return false;
|
|
4139
|
+
const tokens = foldRouteText(raw).replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(Boolean);
|
|
4140
|
+
if (tokens.length === 0 || tokens.length > 6) return false;
|
|
4141
|
+
let coreHit = false;
|
|
4142
|
+
for (const token of tokens) {
|
|
4143
|
+
if (GREETING_CORE_TOKENS.has(token)) {
|
|
4144
|
+
coreHit = true;
|
|
4145
|
+
} else if (!GREETING_FILLER_TOKENS.has(token)) {
|
|
4146
|
+
return false; // any non-greeting word means this is a task wearing a greeting
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
return coreHit;
|
|
4150
|
+
}
|
|
4151
|
+
|
|
3982
4152
|
function isInformationalPrompt({
|
|
3983
4153
|
promptText = '',
|
|
3984
4154
|
commandText = '',
|
|
@@ -4101,6 +4271,7 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
4101
4271
|
// — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
|
|
4102
4272
|
// order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
|
|
4103
4273
|
// TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
|
|
4274
|
+
const MUT_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh';
|
|
4104
4275
|
const TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
|
|
4105
4276
|
// C89-008: WAIT_TAIL ends a waiting-clause strip before any coordinating
|
|
4106
4277
|
// conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything up
|
|
@@ -4109,7 +4280,12 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
4109
4280
|
const WAIT_TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and|nhung|roi|then|later|sau\\s+do)\\s+[^.,;\\n]*?\\b(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
|
|
4110
4281
|
const residue = trimmed
|
|
4111
4282
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
4283
|
+
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
4284
|
+
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
4112
4285
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
4286
|
+
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
4287
|
+
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
4288
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
4113
4289
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
4114
4290
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
4115
4291
|
.replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
@@ -4129,7 +4305,6 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
|
|
|
4129
4305
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
4130
4306
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
4131
4307
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
4132
|
-
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|not|never|no)\\s+(?!only\\b)${TAIL}`, 'g'), ' ')
|
|
4133
4308
|
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
4134
4309
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
4135
4310
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
@@ -4213,6 +4388,7 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
|
|
|
4213
4388
|
// — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
|
|
4214
4389
|
// order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
|
|
4215
4390
|
// TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
|
|
4391
|
+
const MUT_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh';
|
|
4216
4392
|
const TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
|
|
4217
4393
|
// C89-008: WAIT_TAIL ends a waiting-clause strip before a coordinating
|
|
4218
4394
|
// conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything up
|
|
@@ -4220,10 +4396,15 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
|
|
|
4220
4396
|
const WAIT_TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and|nhung|roi|then|later|sau\\s+do)\\s+[^.,;\\n]*?\\b(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
|
|
4221
4397
|
const residue = trimmed
|
|
4222
4398
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
4399
|
+
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
4400
|
+
.replace(/\b(?:tao|lap|create|make|write|draft)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap)\b/g, ' ')
|
|
4223
4401
|
// Descriptive state, not an order: "đã/vừa [được] + verb" ("hooks vừa được cài đặt") reports
|
|
4224
4402
|
// something already done. Only the verb is stripped, so a later imperative still survives.
|
|
4225
4403
|
.replace(/(?<![a-z0-9_])(?:da|vua)\s+(?:duoc\s+)?(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![a-z0-9_])/g, ' ')
|
|
4226
4404
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
4405
|
+
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
4406
|
+
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
4407
|
+
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
4227
4408
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
4228
4409
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
4229
4410
|
.replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
@@ -4245,7 +4426,6 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
|
|
|
4245
4426
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
4246
4427
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
4247
4428
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
4248
|
-
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|not|never|no)\\s+(?!only\\b)${TAIL}`, 'g'), ' ')
|
|
4249
4429
|
.replace(new RegExp(`\\b(?:can|nen|phai|duoc)\\s+(?:duoc\\s+)?(?:sua|cau\\s+hinh|thay\\s+doi|cap\\s+nhat|edit|fix|config)\\b${TAIL}`, 'g'), ' ')
|
|
4250
4430
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
4251
4431
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
@@ -5263,7 +5443,7 @@ function isFlagOrValue(argv, index) {
|
|
|
5263
5443
|
const arg = argv[index];
|
|
5264
5444
|
if (!arg.startsWith('--')) {
|
|
5265
5445
|
const prev = argv[index - 1];
|
|
5266
|
-
return Boolean(prev && ['--root', '--target', '--type', '--tool-command', '--last-prompt', '--session-id', '--adapter', '--transcript'].includes(prev));
|
|
5446
|
+
return Boolean(prev && ['--root', '--target', '--type', '--tool-command', '--last-prompt', '--session-id', '--task-instance-id', '--adapter', '--transcript'].includes(prev));
|
|
5267
5447
|
}
|
|
5268
5448
|
return true;
|
|
5269
5449
|
}
|
|
@@ -5396,6 +5576,7 @@ function createSharedRouteState({
|
|
|
5396
5576
|
source = 'task-route',
|
|
5397
5577
|
requestKey = null,
|
|
5398
5578
|
sessionId = null,
|
|
5579
|
+
taskInstanceId = `task-${crypto.randomBytes(16).toString('hex')}`,
|
|
5399
5580
|
}) {
|
|
5400
5581
|
const helpers = compactHelpers({
|
|
5401
5582
|
verificationRecommendation: route.verificationRecommendation ?? null,
|
|
@@ -5405,6 +5586,7 @@ function createSharedRouteState({
|
|
|
5405
5586
|
return {
|
|
5406
5587
|
...(sessionId ? { sessionId } : {}),
|
|
5407
5588
|
requestKey,
|
|
5589
|
+
taskInstanceId,
|
|
5408
5590
|
fingerprint: buildRouteStateFingerprint(route),
|
|
5409
5591
|
ts: Date.now(),
|
|
5410
5592
|
source,
|
|
@@ -5591,6 +5773,14 @@ export function compactRouteSummary(routeSummary = null) {
|
|
|
5591
5773
|
// stage-off output stays byte-identical (no key emitted when absent).
|
|
5592
5774
|
...(routeSummary.decisionPlane ? { decisionPlane: routeSummary.decisionPlane } : {}),
|
|
5593
5775
|
...(routeSummary.advisorIntent ? { advisorIntent: routeSummary.advisorIntent } : {}),
|
|
5776
|
+
// Routing-orchestration signals survive compaction so persisted route
|
|
5777
|
+
// state carries the same reference/clone/SQL/reuse/run-all metadata the
|
|
5778
|
+
// helper emitted. Conditional spreads — absent keeps output identical.
|
|
5779
|
+
...(routeSummary.referenceTargets ? { referenceTargets: true } : {}),
|
|
5780
|
+
...(routeSummary.cloneIntent ? { cloneIntent: true } : {}),
|
|
5781
|
+
...(routeSummary.sqlIntent ? { sqlIntent: true } : {}),
|
|
5782
|
+
...(routeSummary.reuseFirst ? { reuseFirst: true } : {}),
|
|
5783
|
+
...(routeSummary.goalScope ? { goalScope: routeSummary.goalScope } : {}),
|
|
5594
5784
|
// C89 TASK-005: the typed delegation advice survives compaction so
|
|
5595
5785
|
// persisted route state keeps the shadow verdict. Conditional spread —
|
|
5596
5786
|
// stage-off output stays byte-identical (no key emitted when absent).
|
|
@@ -5618,6 +5808,11 @@ function shouldIncludePreviousContext({
|
|
|
5618
5808
|
const taskType = String(routingContext?.taskType ?? '').trim();
|
|
5619
5809
|
const hasExplicitTarget = Boolean(String(routingContext?.targetFile ?? '').trim());
|
|
5620
5810
|
|
|
5811
|
+
// Greeting fast-path: a social turn needs no memory recall.
|
|
5812
|
+
if (routingContext?.greetingFastPath) {
|
|
5813
|
+
return false;
|
|
5814
|
+
}
|
|
5815
|
+
|
|
5621
5816
|
if (!hasExplicitTarget) {
|
|
5622
5817
|
return true;
|
|
5623
5818
|
}
|
|
@@ -5750,6 +5945,16 @@ export async function persistSharedRouteState(filePath, state) {
|
|
|
5750
5945
|
if (!result.ok) {
|
|
5751
5946
|
console.error(`route-task: skill-router-state lock ${result.reason} after ${result.waitedMs}ms — state write skipped (fail-closed)`);
|
|
5752
5947
|
}
|
|
5948
|
+
// Per-session sidecar (see skill-router.sh persistRouteState): survives a concurrent session
|
|
5949
|
+
// overwriting the shared single-slot file. Best-effort; keyed by the state's own owner.
|
|
5950
|
+
const owner = normalizeSessionId(state?.sessionId);
|
|
5951
|
+
if (owner) {
|
|
5952
|
+
try {
|
|
5953
|
+
const root = path.resolve(path.dirname(filePath), '..', '..');
|
|
5954
|
+
const safe = owner.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'default';
|
|
5955
|
+
await writeJsonAtomic(path.join(root, '.ukit', 'storage', 'cache', 'route-state', `${safe}.json`), state);
|
|
5956
|
+
} catch { /* shared write is authoritative */ }
|
|
5957
|
+
}
|
|
5753
5958
|
return result;
|
|
5754
5959
|
}
|
|
5755
5960
|
|
|
@@ -393,65 +393,9 @@ export async function runSidecarDecision({ decision, context = {}, rootDir, home
|
|
|
393
393
|
fallbackPolicy: spec.fallbackPolicy,
|
|
394
394
|
};
|
|
395
395
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const result = runAdapterBatch({
|
|
401
|
-
spec, context: ctx, rootDir: rootDir ?? process.cwd(), config: mergedConfig,
|
|
402
|
-
batchId: `sidecar-${decision}-${Date.now().toString(36)}`,
|
|
403
|
-
});
|
|
404
|
-
const modelAnswer = validModelAnswer(result, spec.decisionKey);
|
|
405
|
-
const outcomeClass = result?.status ?? 'unavailable';
|
|
406
|
-
const agreement = modelAnswer === null
|
|
407
|
-
? 'unknown'
|
|
408
|
-
: modelAnswer.toLowerCase() === String(deterministic).toLowerCase()
|
|
409
|
-
? 'agree'
|
|
410
|
-
: 'disagree';
|
|
411
|
-
|
|
412
|
-
if (outcomeClass === 'unavailable' || outcomeClass === 'unsupported' || outcomeClass === 'invalid') {
|
|
413
|
-
return {
|
|
414
|
-
...base,
|
|
415
|
-
answer: deterministic,
|
|
416
|
-
source: 'deterministic-fallback',
|
|
417
|
-
outcomeClass: 'unavailable',
|
|
418
|
-
modelAnswer,
|
|
419
|
-
agreement,
|
|
420
|
-
fallbackCode: result?.fallbackCode ?? outcomeClass,
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
if (stage === 'shadow') {
|
|
424
|
-
return {
|
|
425
|
-
...base,
|
|
426
|
-
answer: deterministic,
|
|
427
|
-
source: 'deterministic',
|
|
428
|
-
outcomeClass,
|
|
429
|
-
modelAnswer,
|
|
430
|
-
agreement,
|
|
431
|
-
fallbackCode: result?.fallbackCode ?? null,
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
// canary/default: valid model answer wins; invalid → deterministic fallback.
|
|
435
|
-
if (modelAnswer !== null) {
|
|
436
|
-
return {
|
|
437
|
-
...base,
|
|
438
|
-
answer: modelAnswer,
|
|
439
|
-
source: 'unic-decision',
|
|
440
|
-
outcomeClass,
|
|
441
|
-
modelAnswer,
|
|
442
|
-
agreement,
|
|
443
|
-
fallbackCode: result?.fallbackCode ?? null,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
return {
|
|
447
|
-
...base,
|
|
448
|
-
answer: deterministic,
|
|
449
|
-
source: 'deterministic-fallback',
|
|
450
|
-
outcomeClass,
|
|
451
|
-
modelAnswer,
|
|
452
|
-
agreement,
|
|
453
|
-
fallbackCode: result?.fallbackCode ?? 'missing-answer',
|
|
454
|
-
};
|
|
396
|
+
// All six sidecar decisions have complete deterministic rules. A model vote
|
|
397
|
+
// cannot waive their constraints; skip inference rather than ask and clamp.
|
|
398
|
+
return { ...base, answer: deterministic, source: 'deterministic', outcomeClass: 'deterministic' };
|
|
455
399
|
}
|
|
456
400
|
|
|
457
401
|
// ---------------------------------------------------------------------------
|