@ngockhoale/ukit 3.4.30 → 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.
@@ -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
@@ -799,6 +800,7 @@ function buildRouteHotCacheState(sharedState = {}) {
799
800
  delete routingContext.lastExplicitUserPromptText;
800
801
  delete routingContext.commandText;
801
802
  delete state.sessionId;
803
+ delete state.taskInstanceId;
802
804
  state.routingContext = routingContext;
803
805
  return state;
804
806
  }
@@ -895,6 +897,7 @@ function buildReducedRouteState({ input, reason }) {
895
897
  source: 'task-route',
896
898
  requestKey: null,
897
899
  sessionId: input.sessionId ?? null,
900
+ taskInstanceId: input.taskInstanceId || undefined,
898
901
  });
899
902
  }
900
903
 
@@ -911,7 +914,7 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
911
914
  const absoluteRoot = path.resolve(input.rootDir ?? process.cwd());
912
915
  const budget = createRouteBudget({ signal, deadlineMs });
913
916
  const hotCache = cache ?? createDefaultRouteHotCache({ rootDir: absoluteRoot });
914
- const normalizedInput = { ...input, rootDir: absoluteRoot };
917
+ const normalizedInput = { ...input, rootDir: absoluteRoot, taskInstanceId: input.taskInstanceId || `task-${crypto.randomBytes(16).toString('hex')}` };
915
918
  const baseMeta = {
916
919
  cacheHit: false,
917
920
  reduced: false,
@@ -929,7 +932,7 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
929
932
  elapsedMs: Date.now() - budget.startedAt,
930
933
  ioReads: budget.ioReads,
931
934
  };
932
- return { ...state, cacheHit: routeMeta.cacheHit, routeMeta };
935
+ return { ...state, taskInstanceId: normalizedInput.taskInstanceId || state.taskInstanceId || `task-${crypto.randomBytes(16).toString('hex')}`, cacheHit: routeMeta.cacheHit, routeMeta };
933
936
  };
934
937
  const reduce = (reason) => finish(
935
938
  buildReducedRouteState({ input: normalizedInput, reason }),
@@ -1079,6 +1082,7 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
1079
1082
  requestKey,
1080
1083
  indexGeneratedAtMs,
1081
1084
  sessionId: normalizedInput.sessionId ?? null,
1085
+ taskInstanceId: normalizedInput.taskInstanceId || undefined,
1082
1086
  });
1083
1087
 
1084
1088
  // Phase 3 — store under a POST-route fingerprint so the next identical call (whose own
@@ -1126,7 +1130,12 @@ async function main() {
1126
1130
  const sessionId = normalizeSessionId(readFlagValue(args, '--session-id'));
1127
1131
  const previousStateOwnershipBlocked = hasRouteStateOwner(persistedPreviousState)
1128
1132
  && !isRouteStateCompatible(persistedPreviousState, sessionId);
1129
- const previousState = previousStateOwnershipBlocked ? {} : persistedPreviousState;
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;
1130
1139
  const escalationConfig = await readMergedConfig(rootDir);
1131
1140
  const targetFile = readFlagValue(args, '--target');
1132
1141
  const taskType = readFlagValue(args, '--type');
@@ -1144,7 +1153,7 @@ async function main() {
1144
1153
  const commandNamespace = adapter === 'codex'
1145
1154
  ? '.codex'
1146
1155
  : (adapter === 'antigravity' ? '.antigravity' : '.claude');
1147
- const promptText = args
1156
+ let promptText = args
1148
1157
  .filter((arg, index) => !isFlagOrValue(args, index))
1149
1158
  .join(' ')
1150
1159
  .trim();
@@ -1155,6 +1164,15 @@ async function main() {
1155
1164
  return;
1156
1165
  }
1157
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;
1158
1176
  const preparedRoute = await prepareTaskRoute({
1159
1177
  rootDir,
1160
1178
  promptText,
@@ -1249,7 +1267,12 @@ async function main() {
1249
1267
  historySignals,
1250
1268
  };
1251
1269
  }
1252
- printRouteState(reusableState);
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);
1253
1276
  return;
1254
1277
  }
1255
1278
 
@@ -1331,6 +1354,7 @@ async function main() {
1331
1354
  }
1332
1355
  const sharedState = {
1333
1356
  ...cachedRouteState,
1357
+ taskInstanceId,
1334
1358
  ...(sessionId ? { sessionId } : {}),
1335
1359
  source: 'task-route',
1336
1360
  ts: Date.now(),
@@ -1391,6 +1415,7 @@ async function main() {
1391
1415
  requestKey,
1392
1416
  indexGeneratedAtMs,
1393
1417
  sessionId,
1418
+ taskInstanceId,
1394
1419
  });
1395
1420
  if (canPersistRouteState) {
1396
1421
  // Advisory persistence — an EACCES/ENOSPC under the lock must never lose
@@ -3775,11 +3800,12 @@ function buildRouteSummary({
3775
3800
  executionScores,
3776
3801
  executionCandidates,
3777
3802
  });
3778
- const executionContract = buildExecutionContract(executionMode);
3803
+ const executionContract = applyTaskSemantics(buildExecutionContract(executionMode), classifyTaskSemantics({ promptText: routingContext.lastExplicitUserPromptText || routingContext.promptText, targetFile: routingContext.targetFile }));
3779
3804
  const continuousExecution = buildContinuousExecutionPolicy(autonomyLevel);
3780
3805
  const tierLane = buildTierLane({ modelTier: executionContract?.modelTier ?? null });
3781
3806
  const completionState = buildCompletionState({
3782
3807
  executionMode,
3808
+ executionContract,
3783
3809
  verificationRecommendation,
3784
3810
  });
3785
3811
  const continuationState = buildContinuationState({
@@ -5417,7 +5443,7 @@ function isFlagOrValue(argv, index) {
5417
5443
  const arg = argv[index];
5418
5444
  if (!arg.startsWith('--')) {
5419
5445
  const prev = argv[index - 1];
5420
- 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));
5421
5447
  }
5422
5448
  return true;
5423
5449
  }
@@ -5550,6 +5576,7 @@ function createSharedRouteState({
5550
5576
  source = 'task-route',
5551
5577
  requestKey = null,
5552
5578
  sessionId = null,
5579
+ taskInstanceId = `task-${crypto.randomBytes(16).toString('hex')}`,
5553
5580
  }) {
5554
5581
  const helpers = compactHelpers({
5555
5582
  verificationRecommendation: route.verificationRecommendation ?? null,
@@ -5559,6 +5586,7 @@ function createSharedRouteState({
5559
5586
  return {
5560
5587
  ...(sessionId ? { sessionId } : {}),
5561
5588
  requestKey,
5589
+ taskInstanceId,
5562
5590
  fingerprint: buildRouteStateFingerprint(route),
5563
5591
  ts: Date.now(),
5564
5592
  source,
@@ -393,65 +393,9 @@ export async function runSidecarDecision({ decision, context = {}, rootDir, home
393
393
  fallbackPolicy: spec.fallbackPolicy,
394
394
  };
395
395
 
396
- if (stage === 'off') {
397
- return { ...base, answer: deterministic, source: 'deterministic', outcomeClass: 'deterministic' };
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
  // ---------------------------------------------------------------------------
@@ -83,23 +83,20 @@ export function encodeQuestion(question) {
83
83
  description: 'true iff the proposition holds; interpreted as P(true).',
84
84
  };
85
85
  } else {
86
- // score — ordinal integer; the legend MUST be defined in the description.
87
- const legend = Array.isArray(candidates) ? candidates : [];
88
- value = { type: 'integer' };
89
- if (legend.length > 0) {
90
- value.minimum = 1;
91
- value.maximum = legend.length;
92
- value.description = `Ordered scale: ${legend
93
- .map((label, i) => `${i + 1}=${label}`)
94
- .join(', ')}.`;
86
+ if (!Array.isArray(candidates) || candidates.length === 0
87
+ || candidates.some(label => typeof label !== 'string' || !label.trim())
88
+ || new Set(candidates).size !== candidates.length) {
89
+ throw new TypeError('score requires unique non-empty ordinal labels.');
95
90
  }
91
+ value = { type: 'string', enum: [...candidates] };
96
92
  }
97
93
 
98
94
  return {
99
95
  type: 'function',
100
96
  function: {
101
97
  name: decisionKey,
102
- description: String(instruction ?? ''),
98
+ description: String(instruction ?? '') + (kind === 'score'
99
+ ? `\nOrdered ordinal rubric: ${candidates.map((label, i) => `${i + 1}=${JSON.stringify(label)}`).join(', ')}. Choose exactly one label; do not return an expected numeric score.` : ''),
103
100
  parameters: {
104
101
  type: 'object',
105
102
  properties: { value },
@@ -196,11 +193,12 @@ function validateValue(question, args) {
196
193
  if (isFiniteNumber(value) && value >= 0 && value <= 1) return { value };
197
194
  return 'missing-value';
198
195
  }
199
- // score — finite integer within the declared legend range.
200
- if (!isFiniteNumber(value) || !Number.isInteger(value)) return 'missing-value';
196
+ // Decode an explicit choice losslessly; never reinterpret native expectations.
201
197
  const legend = Array.isArray(question.candidates) ? question.candidates : [];
202
- if (legend.length > 0 && (value < 1 || value > legend.length)) return 'out-of-enum';
203
- return { value };
198
+ if (!legend.length || new Set(legend).size !== legend.length) return 'out-of-enum';
199
+ if (typeof value !== 'string') return 'missing-value';
200
+ const ordinal = legend.indexOf(value);
201
+ return ordinal < 0 ? 'out-of-enum' : { value: ordinal + 1 };
204
202
  }
205
203
 
206
204
  export function parseBatchResponse(response, questions) {
@@ -834,6 +832,21 @@ export async function requestBatch(batch, {
834
832
  return outcome('invalid', { fallbackCode: 'empty-batch' });
835
833
  }
836
834
 
835
+ const maxBatch = dp.maxBatchQuestions ?? 8;
836
+ const maxEnum = dp.maxEnumLabels ?? 11;
837
+ if (![maxBatch, maxEnum].every(n => Number.isInteger(n) && n > 0)) {
838
+ return outcome('unsupported', { fallbackCode: 'invalid-decision-limits' });
839
+ }
840
+ if (questions.length > maxBatch) {
841
+ return outcome('unsupported', { fallbackCode: 'max-batch-questions' });
842
+ }
843
+ if (questions.some(q => Array.isArray(q.candidates) && q.candidates.length > maxEnum)) {
844
+ return outcome('unsupported', { fallbackCode: 'max-enum-labels' });
845
+ }
846
+ if (new Set(questions.map(q => q.decisionKey)).size !== questions.length) {
847
+ return outcome('invalid', { fallbackCode: 'duplicate-question' });
848
+ }
849
+
837
850
  // Language classification covers state + question text; the English
838
851
  // checkpoint is reachable only when the whole request proves English.
839
852
  // Malformed batch input (cyclic state, non-serializable values, internal
@@ -0,0 +1,43 @@
1
+ // Capability adapters normalize native results; completion consumes their common receipt shape.
2
+ const ID = /^[a-f0-9-]{8,36}$/i;
3
+ function responseText(response) {
4
+ if (typeof response === 'string') return response.trim();
5
+ if (response?.content !== undefined && (!Array.isArray(response.content)
6
+ || !response.content.every((c) => c?.type === 'text' && typeof c.text === 'string'))) return '';
7
+ if (typeof response?.output === 'string') return response.output.trim();
8
+ if (typeof response?.stdout === 'string') return response.stdout.trim();
9
+ if (Array.isArray(response?.content) && response.content.every((c) => c?.type === 'text' && typeof c.text === 'string')) {
10
+ return response.content.map((c) => c.text).join('\n').trim();
11
+ }
12
+ return '';
13
+ }
14
+ const capabilities = {
15
+ CronDelete: { domain: 'scheduled-job', role: 'mutation', normalize(input, response) {
16
+ const target = typeof input?.id === 'string' && ID.test(input.id) ? input.id.toLowerCase() : null;
17
+ return { target, accepted: Boolean(target && responseText(response) === `Cancelled ${input.id}`) };
18
+ } },
19
+ CronList: { domain: 'scheduled-job', role: 'state', normalize(_input, response) {
20
+ const body = response?.details && Object.hasOwn(response.details, 'jobs') ? response.details : response;
21
+ const structured = body != null && typeof body === 'object' && Object.hasOwn(body, 'jobs');
22
+ const jobs = Array.isArray(body?.jobs) ? body.jobs : null;
23
+ const ids = jobs && jobs.length <= 256 && jobs.every((job) => typeof job?.id === 'string' && ID.test(job.id))
24
+ ? jobs.map((job) => job.id.toLowerCase()) : null;
25
+ const partial = [body, response].some((value) => value?.partial === true || value?.hasMore === true
26
+ || Boolean(value?.nextCursor) || value?.complete === false);
27
+ const supportedContent = response?.content === undefined || (Array.isArray(response.content)
28
+ && response.content.every((c) => c?.type === 'text' && typeof c.text === 'string'));
29
+ const valid = !partial && supportedContent
30
+ && (structured ? ids !== null : /^No scheduled jobs\.?$/.test(responseText(response)));
31
+ return { target: null, state: 'absent', presentTargets: valid ? ids || [] : null, complete: valid, accepted: valid };
32
+ } },
33
+ };
34
+ export function cronOperationReceipt(toolName, input, response, transportSuccess, { sessionId, harness } = {}) {
35
+ const capability = capabilities[toolName];
36
+ if (!capability) return null;
37
+ const normalized = capability.normalize(input, response);
38
+ const { accepted, ...state } = normalized;
39
+ const scope = typeof sessionId === 'string' && sessionId.trim() ? `session:${sessionId.trim()}` : null;
40
+ return { kind: 'operation', success: transportSuccess === true && accepted && Boolean(scope),
41
+ transportSuccess: transportSuccess === true, harness,
42
+ operation: { domain: capability.domain, role: capability.role, scope, ...state } };
43
+ }