@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/manifests/documentation.yaml +100 -0
  3. package/package.json +1 -1
  4. package/src/core/executionContracts.js +32 -0
  5. package/src/core/permissionPolicy.js +1 -1
  6. package/src/decision/client.js +15 -0
  7. package/src/decision/protocol.js +12 -14
  8. package/src/index/buildIndex.js +244 -4
  9. package/src/index/impactContext.js +91 -11
  10. package/src/index/languageTools.js +126 -0
  11. package/src/index/paths.js +5 -1
  12. package/src/index/playbookRegistry.js +108 -9
  13. package/src/index/queryIndex.js +415 -1
  14. package/src/index/routeCatalog.js +1 -1
  15. package/src/index/routeResolver.js +2 -2
  16. package/src/index/taskRouting.js +28 -4
  17. package/template_project/.claude/commands/ukit/handoff-create.md +5 -0
  18. package/template_project/.claude/commands/ukit/handoff-fullstack.md +43 -4
  19. package/template_project/.claude/hooks/block-dangerous.mjs +7 -1
  20. package/template_project/.claude/hooks/handoff-model-guard.sh +235 -21
  21. package/template_project/.claude/hooks/record-execution.mjs +21 -0
  22. package/template_project/.claude/hooks/skill-router.sh +187 -16
  23. package/template_project/.claude/settings.json +43 -1
  24. package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
  25. package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +10 -4
  26. package/template_project/.claude/ukit/index/impact-context.mjs +70 -15
  27. package/template_project/.claude/ukit/index/land-worktree.mjs +2 -2
  28. package/template_project/.claude/ukit/index/lib/index-core.mjs +883 -14
  29. package/template_project/.claude/ukit/index/playbook-registry.mjs +108 -9
  30. package/template_project/.claude/ukit/index/query-index.mjs +100 -7
  31. package/template_project/.claude/ukit/index/review-verdict.mjs +14 -4
  32. package/template_project/.claude/ukit/index/route-catalog.mjs +1 -1
  33. package/template_project/.claude/ukit/index/route-resolver.mjs +34 -2
  34. package/template_project/.claude/ukit/index/route-task.mjs +243 -38
  35. package/template_project/.claude/ukit/index/sidecar-decision.mjs +3 -59
  36. package/template_project/.claude/ukit/index/unic-decision.mjs +27 -14
  37. package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +43 -0
  38. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +612 -56
  39. package/template_project/.claude/ukit/runtime/handoff-intent.mjs +122 -6
  40. package/template_project/.claude/ukit/runtime/loop-state.mjs +1 -1
  41. package/template_project/.claude/ukit/runtime/operation-evidence.mjs +87 -0
  42. package/template_project/.claude/ukit/runtime/resumable-run.mjs +417 -0
  43. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +635 -28
  44. package/template_project/.omp/hooks/pre/ukit-bridge.js +40 -5
  45. package/template_project/AGENTS.md +1 -1
  46. package/template_project/CLAUDE.md +1 -1
  47. package/template_project/instructions/core.md +1 -1
  48. package/template_user/playbooks/feature-implementation.md +9 -0
  49. 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 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;
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
- const promptText = args
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
- 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);
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
- const playbookResolution = await resolvePlaybookRoute({
2211
- promptText: routingContext.promptText,
2212
- commandText: routingContext.commandText,
2213
- targetFile: routingContext.targetFile ?? null,
2214
- executionMode: routingContext.executionMode ?? null,
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
- const directTransformSignal = /\b(change|replace|set|rename|convert|swap)\b/.test(raw) || /\bchange\s+.+\s+to\s+.+\b/.test(raw);
3912
- const smallFixSignal = /\b(adjust|tweak|patch|fix|modify|update|apply)\b/.test(raw);
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
- 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
  // ---------------------------------------------------------------------------