@ngockhoale/ukit 3.4.29 → 3.4.30

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 (34) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/manifests/documentation.yaml +88 -0
  3. package/package.json +1 -1
  4. package/src/core/permissionPolicy.js +1 -1
  5. package/src/index/buildIndex.js +244 -4
  6. package/src/index/impactContext.js +91 -11
  7. package/src/index/languageTools.js +126 -0
  8. package/src/index/paths.js +5 -1
  9. package/src/index/playbookRegistry.js +108 -9
  10. package/src/index/queryIndex.js +415 -1
  11. package/src/index/routeCatalog.js +1 -1
  12. package/src/index/taskRouting.js +25 -3
  13. package/template_project/.claude/commands/ukit/handoff-create.md +5 -0
  14. package/template_project/.claude/commands/ukit/handoff-fullstack.md +43 -4
  15. package/template_project/.claude/hooks/block-dangerous.mjs +7 -1
  16. package/template_project/.claude/hooks/handoff-model-guard.sh +235 -21
  17. package/template_project/.claude/hooks/record-execution.mjs +23 -0
  18. package/template_project/.claude/hooks/skill-router.sh +105 -7
  19. package/template_project/.claude/settings.json +21 -1
  20. package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
  21. package/template_project/.claude/ukit/index/impact-context.mjs +70 -15
  22. package/template_project/.claude/ukit/index/land-worktree.mjs +2 -2
  23. package/template_project/.claude/ukit/index/lib/index-core.mjs +883 -14
  24. package/template_project/.claude/ukit/index/playbook-registry.mjs +108 -9
  25. package/template_project/.claude/ukit/index/query-index.mjs +100 -7
  26. package/template_project/.claude/ukit/index/route-catalog.mjs +1 -1
  27. package/template_project/.claude/ukit/index/route-task.mjs +208 -31
  28. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +453 -24
  29. package/template_project/.claude/ukit/runtime/handoff-intent.mjs +122 -6
  30. package/template_project/.claude/ukit/runtime/resumable-run.mjs +417 -0
  31. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +635 -28
  32. package/template_project/.omp/hooks/pre/ukit-bridge.js +32 -4
  33. package/template_user/playbooks/feature-implementation.md +9 -0
  34. package/template_user/playbooks/migration.md +4 -0
@@ -70,7 +70,7 @@ import {
70
70
  deriveRouteFields,
71
71
  } from './route-resolver.mjs';
72
72
  import { applyMeasuredTier, loadTierMap } from './tier-selection.mjs';
73
- import { resolvePlaybookRoute, loadRegistry } from './playbook-registry.mjs';
73
+ import { resolvePlaybookRoute, loadRegistry, deriveRouteOrchestrationSignals } from './playbook-registry.mjs';
74
74
  import { extractHistorySignalsAsync } from './session-history-extractor.mjs';
75
75
  import {
76
76
  applyFixLoopEscalation,
@@ -190,6 +190,15 @@ If the user says "new task", re-route — do not treat the message as the next s
190
190
  4. Implement the smallest change satisfying the predicate.
191
191
  5. Verify against the predicate on the real artifact — not "it compiles".
192
192
  6. Widen once: check the impact surface the route named, no broader.
193
+ Reference reuse: when the ask points at an existing surface ("like the Customer
194
+ page", "giống trang X", "same as module Y"), read that surface first and mirror
195
+ its structure — the reference defines the done predicate, not just inspiration.
196
+ Reuse-first: when the ask names an existing function, helper, or pattern
197
+ ("using validation from Z", "dùng hàm có sẵn"), reuse it — extend or wrap
198
+ before writing new logic, and name the reuse in the report.
199
+ Clone: when the ask duplicates a feature ("clone the Customer page", "nhân bản
200
+ tính năng X"), copy the source feature's structure, rename and adapt each
201
+ surface, and verify the clone against the same predicate class as the original.
193
202
  Reply: what changed, the predicate, the evidence it now holds.
194
203
  Ask the human only for: irreversible writes, a genuine preference call no experiment
195
204
  settles, or a real dead end. Everything else: do it, report it.`,
@@ -1464,6 +1473,56 @@ async function prepareTaskRoute({
1464
1473
  const normalizedPrompt = String(promptText || '').trim();
1465
1474
  const normalizedCommand = String(commandText || '').trim();
1466
1475
  const normalizedTarget = normalizeRelativeFile(absoluteRoot, targetFile);
1476
+ // Greeting fast-path: a pure social turn short-circuits BEFORE any skill
1477
+ // scan, index work or vision resolution. executionScores/candidates keep
1478
+ // the zero-signal shape downstream consumers expect; the mode is stamped
1479
+ // informational and routingContext.greetingFastPath tells finalizeTaskRoute
1480
+ // to skip the playbook scan and the decision-plane shadow.
1481
+ if (isPureGreetingPrompt({
1482
+ promptText: normalizedPrompt,
1483
+ commandText: normalizedCommand,
1484
+ targetFile: normalizedTarget,
1485
+ })) {
1486
+ const greetingScores = deriveExecutionScores({
1487
+ promptText: '',
1488
+ commandText: '',
1489
+ targetFile: null,
1490
+ intentMode: null,
1491
+ taskType: '',
1492
+ });
1493
+ return {
1494
+ rootDir: absoluteRoot,
1495
+ activeSkills: [],
1496
+ routingContext: {
1497
+ adapter: String(adapter || 'claude').trim() || 'claude',
1498
+ promptText: normalizedPrompt,
1499
+ lastExplicitUserPromptText: normalizedPrompt || String(lastExplicitUserPromptText || '').trim(),
1500
+ commandText: normalizedCommand,
1501
+ targetFile: null,
1502
+ contextIntent: '',
1503
+ taskType: '',
1504
+ intentMode: null,
1505
+ executionScores: greetingScores,
1506
+ executionCandidates: buildExecutionModeCandidates({
1507
+ promptText: '',
1508
+ commandText: '',
1509
+ targetFile: null,
1510
+ intentMode: null,
1511
+ executionScores: greetingScores,
1512
+ }),
1513
+ executionMode: 'informational',
1514
+ greetingFastPath: true,
1515
+ orchestrationSignals: deriveRouteOrchestrationSignals({
1516
+ promptText: normalizedPrompt,
1517
+ commandText: normalizedCommand,
1518
+ }),
1519
+ visionLane: false,
1520
+ visionReason: null,
1521
+ visionModel: null,
1522
+ visionAdvisory: null,
1523
+ },
1524
+ };
1525
+ }
1467
1526
  const intentMode = deriveIntentMode({
1468
1527
  promptText: normalizedPrompt,
1469
1528
  commandText: normalizedCommand,
@@ -1520,6 +1579,10 @@ async function prepareTaskRoute({
1520
1579
  targetFile: normalizedTarget,
1521
1580
  visionFlag: Boolean(visionFlag),
1522
1581
  });
1582
+ const orchestrationSignals = deriveRouteOrchestrationSignals({
1583
+ promptText: normalizedPrompt,
1584
+ commandText: normalizedCommand,
1585
+ });
1523
1586
 
1524
1587
  return {
1525
1588
  rootDir: absoluteRoot,
@@ -1536,6 +1599,7 @@ async function prepareTaskRoute({
1536
1599
  executionScores,
1537
1600
  executionCandidates,
1538
1601
  executionMode,
1602
+ orchestrationSignals,
1539
1603
  visionLane: Boolean(visionInfo?.visionLane),
1540
1604
  visionReason: visionInfo?.visionReason ?? null,
1541
1605
  visionModel: visionInfo?.visionModel ?? null,
@@ -1727,6 +1791,9 @@ export async function runRouteDecisionShadow({
1727
1791
  } = {}) {
1728
1792
  const stage = resolveDecisionPlaneStage(config);
1729
1793
  if (stage === 'off') return null;
1794
+ // Greeting fast-path: a social turn carries nothing the decision plane could
1795
+ // answer — never spawn the adapter shadow for one.
1796
+ if (routingContext?.greetingFastPath) return null;
1730
1797
  const questions = buildShadowDecisionQuestions(config);
1731
1798
  if (questions.length === 0) {
1732
1799
  return { stage, outcomeClass: 'skipped', decisionKeys: [], probabilityBands: {},
@@ -2046,6 +2113,9 @@ export async function emitResumableRun({
2046
2113
  } = {}) {
2047
2114
  const stage = resolveResumableRunStage(config);
2048
2115
  if (stage === 'off') return null;
2116
+ // Greeting fast-path: a social turn starts no goal — never persist a
2117
+ // resumable run record (or ledger pointer) for one.
2118
+ if (routingContext?.greetingFastPath) return null;
2049
2119
  try {
2050
2120
  const taskBoundary = resumableTaskBoundary(routingContext);
2051
2121
  const taskId = resumableTaskId({ sessionId, taskBoundary });
@@ -2205,34 +2275,45 @@ async function finalizeTaskRoute({
2205
2275
  });
2206
2276
  // TASK-007 (BL-009): playbook resolution rides the same route signals —
2207
2277
  // the registry degrades to playbookId: null + reason and never blocks the
2208
- // route. Same merge the hook performs via mergeResolverFields.
2278
+ // route. Same merge the hook performs via mergeResolverFields. A greeting
2279
+ // fast-path route skips the registry scan entirely: no playbook can claim a
2280
+ // social turn, so the route records the skip honestly (reason 'greeting').
2209
2281
  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 {
2282
+ if (routingContext.greetingFastPath) {
2283
+ routeSummary.playbookId = null;
2284
+ routeSummary.workGroup = null;
2285
+ routeSummary.playbookReason = 'greeting';
2286
+ routeSummary.playbookLoad = null;
2234
2287
  delete routeSummary.path;
2235
2288
  delete routeSummary.fastPathKind;
2289
+ } else {
2290
+ const playbookResolution = await resolvePlaybookRoute({
2291
+ promptText: routingContext.promptText,
2292
+ commandText: routingContext.commandText,
2293
+ targetFile: routingContext.targetFile ?? null,
2294
+ executionMode: routingContext.executionMode ?? null,
2295
+ intentMode: routingContext.intentMode ?? null,
2296
+ escalationTriggers: routeSummary?.escalationTriggers ?? [],
2297
+ projectRoot: absoluteRoot,
2298
+ homeDir: os.homedir(),
2299
+ });
2300
+ routeSummary.playbookId = playbookResolution.playbookId;
2301
+ routeSummary.workGroup = playbookResolution.workGroup;
2302
+ routeSummary.playbookReason = playbookResolution.reason;
2303
+ routeSummary.playbookLoad = playbookResolution.playbookLoad;
2304
+ // BL-010 (ARCH Task Contract): `path` = direct|handoff; `fastPathKind` is
2305
+ // the ARCH `none|small-feature|bug-fix` enum — named Kind because
2306
+ // routeSummary.fastPath already carries the FR-004 eligibility object.
2307
+ // Keys land only when a routing-table row claimed the prompt (workGroup
2308
+ // non-null) — identical key-presence to deriveTaskRoute so schema-off /
2309
+ // no-match routes never grow stray keys.
2310
+ if (playbookResolution.workGroup != null) {
2311
+ routeSummary.path = playbookResolution.path;
2312
+ routeSummary.fastPathKind = playbookResolution.fastPath;
2313
+ } else {
2314
+ delete routeSummary.path;
2315
+ delete routeSummary.fastPathKind;
2316
+ }
2236
2317
  }
2237
2318
  } catch {
2238
2319
  routeSummary.playbookId = null;
@@ -2982,7 +3063,7 @@ function hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext = false }
2982
3063
  }
2983
3064
 
2984
3065
  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);
3066
+ || /(?<![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
3067
  }
2987
3068
 
2988
3069
  function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = false } = {}) {
@@ -3811,6 +3892,11 @@ function buildRouteSummary({
3811
3892
  promptText: routingContext.promptText,
3812
3893
  commandText: routingContext.commandText,
3813
3894
  });
3895
+ // Routing-orchestration signals (reference/clone/SQL/reuse/run-all): the
3896
+ // same booleans the playbook discriminators read. Purely advisory route
3897
+ // metadata — they never set mutation permission by themselves. Conditional
3898
+ // spreads keep every other route byte-identical.
3899
+ const orchestrationSignals = routingContext.orchestrationSignals ?? null;
3814
3900
  return {
3815
3901
  primaryCommands,
3816
3902
  fallbackCommands,
@@ -3876,6 +3962,12 @@ function buildRouteSummary({
3876
3962
  // FR-004 additive field — present only when the prompt is an advisor/reference/replan
3877
3963
  // request, so every other route stays byte-identical.
3878
3964
  ...(advisorIntent ? { advisorIntent } : {}),
3965
+ // Routing-orchestration signals — present only when detected.
3966
+ ...(orchestrationSignals?.referenceTarget ? { referenceTargets: true } : {}),
3967
+ ...(orchestrationSignals?.cloneIntent ? { cloneIntent: true } : {}),
3968
+ ...(orchestrationSignals?.sqlIntent ? { sqlIntent: true } : {}),
3969
+ ...(orchestrationSignals?.reuseFirst ? { reuseFirst: true } : {}),
3970
+ ...(orchestrationSignals?.runAll ? { goalScope: 'run-all' } : {}),
3879
3971
  line: line || 'task=unknown',
3880
3972
  };
3881
3973
  }
@@ -3908,8 +4000,16 @@ function deriveExecutionScores({
3908
4000
  } = {}) {
3909
4001
  const raw = `${promptText ?? ''}\n${commandText ?? ''}`.toLowerCase();
3910
4002
  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);
4003
+ // Vietnamese edit verbs (\b never matches around diacritics — JS word chars
4004
+ // are ASCII-only), so a small Vietnamese edit like "đổi màu nút thành đỏ"
4005
+ // or "sửa cột …" scores as the small mutating task it is instead of
4006
+ // falling to the all-zero shared-edit floor.
4007
+ 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);
4008
+ const directTransformSignal = /\b(change|replace|set|rename|convert|swap)\b/.test(raw)
4009
+ || /\bchange\s+.+\s+to\s+.+\b/.test(raw)
4010
+ // "đổi X thành Y" is the Vietnamese "change X to Y".
4011
+ || /(?<![A-Za-z0-9_])(?:đổi|doi|sửa|sua|đặt|dat)\s+[^\n,.;]{1,48}?\s+(?:thành|thanh|to)\s+/.test(raw);
4012
+ const smallFixSignal = /\b(adjust|tweak|patch|fix|modify|update|apply)\b/.test(raw) || vietnameseEditVerb;
3913
4013
  const buildSignal = /\b(build|create|add|implement|feature)\b/.test(raw);
3914
4014
  const debugSignal = intentMode === 'debug-specific' || /\b(root cause|debug|triage|flaky|investigate|why)\b/.test(raw);
3915
4015
  const reviewSignal = intentMode === 'review-specific' || /\b(review|audit|verify|release readiness)\b/.test(raw);
@@ -3917,6 +4017,7 @@ function deriveExecutionScores({
3917
4017
  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
4018
  const boundedEditSignal = /\b(one[- ]line|small|tiny|single|local|guard|log line|loading message|label)\b/.test(raw);
3919
4019
  const implementSignal = /\b(implement|apply|update|modify|add|create|ship|deliver|fix)\b/.test(raw)
4020
+ || vietnameseEditVerb
3920
4021
  || /\bchange\s+.+\s+to\s+.+\b/.test(raw);
3921
4022
  const explicitTarget = Boolean(targetFile);
3922
4023
 
@@ -3979,6 +4080,49 @@ function deriveExecutionScores({
3979
4080
  };
3980
4081
  }
3981
4082
 
4083
+ // --- Greeting fast-path (routing orchestration) --------------------------------
4084
+ // A prompt that is ONLY a greeting/acknowledgment (≤ ~6 words, no task
4085
+ // vocabulary, no target file, no command) is a social turn, never work. It
4086
+ // used to fall through every signal gate with all-zero scores and the
4087
+ // no-target blastRadius=1 floor minted a shared-edit (mutating) lane + the
4088
+ // full pipeline (context snapshots, playbook scan, decision shadow). The
4089
+ // fast path stamps informational, loads no skill, resolves no playbook and
4090
+ // skips the shadow — deterministically, before any I/O.
4091
+ // Matched on diacritic-folded tokens so both Vietnamese spellings work.
4092
+ const GREETING_CORE_TOKENS = new Set([
4093
+ 'hi', 'hello', 'hey', 'yo', 'chao', 'xin', 'good',
4094
+ 'morning', 'afternoon', 'evening',
4095
+ 'thanks', 'thank', 'cam', 'on', 'bye', 'tam', 'biet',
4096
+ ]);
4097
+ const GREETING_FILLER_TOKENS = new Set([
4098
+ 'you', 'u', 'there', 'guys', 'everyone', 'all',
4099
+ 'ban', 'cac', 'minh', 'moi', 'nguoi', 'nhe', 'nha', 'nhieu',
4100
+ 'ok', 'okay', 'again', 'still',
4101
+ ]);
4102
+ function foldRouteText(text = '') {
4103
+ return String(text ?? '')
4104
+ .toLowerCase()
4105
+ .normalize('NFD')
4106
+ .replace(/[\u0300-\u036f]/g, '')
4107
+ .replace(/\u0111/g, 'd');
4108
+ }
4109
+ function isPureGreetingPrompt({ promptText = '', commandText = '', targetFile = null } = {}) {
4110
+ const raw = String(promptText ?? '').trim();
4111
+ if (!raw || String(commandText ?? '').trim() || targetFile) return false;
4112
+ if (raw.length > 64) return false;
4113
+ const tokens = foldRouteText(raw).replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(Boolean);
4114
+ if (tokens.length === 0 || tokens.length > 6) return false;
4115
+ let coreHit = false;
4116
+ for (const token of tokens) {
4117
+ if (GREETING_CORE_TOKENS.has(token)) {
4118
+ coreHit = true;
4119
+ } else if (!GREETING_FILLER_TOKENS.has(token)) {
4120
+ return false; // any non-greeting word means this is a task wearing a greeting
4121
+ }
4122
+ }
4123
+ return coreHit;
4124
+ }
4125
+
3982
4126
  function isInformationalPrompt({
3983
4127
  promptText = '',
3984
4128
  commandText = '',
@@ -4101,6 +4245,7 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
4101
4245
  // — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
4102
4246
  // order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
4103
4247
  // TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
4248
+ 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
4249
  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
4250
  // C89-008: WAIT_TAIL ends a waiting-clause strip before any coordinating
4106
4251
  // conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything up
@@ -4109,7 +4254,12 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
4109
4254
  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
4255
  const residue = trimmed
4111
4256
  .replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
4257
+ // "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
4258
+ .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
4259
  .replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
4260
+ // A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
4261
+ // verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
4262
+ .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
4263
  .replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
4114
4264
  .replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
4115
4265
  .replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
@@ -4129,7 +4279,6 @@ function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
4129
4279
  .replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
4130
4280
  .replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
4131
4281
  .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
4282
  .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
4283
  .replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
4135
4284
  .replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
@@ -4213,6 +4362,7 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
4213
4362
  // — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
4214
4363
  // order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
4215
4364
  // TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
4365
+ 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
4366
  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
4367
  // C89-008: WAIT_TAIL ends a waiting-clause strip before a coordinating
4218
4368
  // conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything up
@@ -4220,10 +4370,15 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
4220
4370
  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
4371
  const residue = trimmed
4222
4372
  .replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
4373
+ // "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
4374
+ .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
4375
  // Descriptive state, not an order: "đã/vừa [được] + verb" ("hooks vừa được cài đặt") reports
4224
4376
  // something already done. Only the verb is stripped, so a later imperative still survives.
4225
4377
  .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
4378
  .replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
4379
+ // A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
4380
+ // verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
4381
+ .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
4382
  .replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
4228
4383
  .replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
4229
4384
  .replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
@@ -4245,7 +4400,6 @@ function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
4245
4400
  .replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
4246
4401
  .replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
4247
4402
  .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
4403
  .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
4404
  .replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
4251
4405
  .replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
@@ -5591,6 +5745,14 @@ export function compactRouteSummary(routeSummary = null) {
5591
5745
  // stage-off output stays byte-identical (no key emitted when absent).
5592
5746
  ...(routeSummary.decisionPlane ? { decisionPlane: routeSummary.decisionPlane } : {}),
5593
5747
  ...(routeSummary.advisorIntent ? { advisorIntent: routeSummary.advisorIntent } : {}),
5748
+ // Routing-orchestration signals survive compaction so persisted route
5749
+ // state carries the same reference/clone/SQL/reuse/run-all metadata the
5750
+ // helper emitted. Conditional spreads — absent keeps output identical.
5751
+ ...(routeSummary.referenceTargets ? { referenceTargets: true } : {}),
5752
+ ...(routeSummary.cloneIntent ? { cloneIntent: true } : {}),
5753
+ ...(routeSummary.sqlIntent ? { sqlIntent: true } : {}),
5754
+ ...(routeSummary.reuseFirst ? { reuseFirst: true } : {}),
5755
+ ...(routeSummary.goalScope ? { goalScope: routeSummary.goalScope } : {}),
5594
5756
  // C89 TASK-005: the typed delegation advice survives compaction so
5595
5757
  // persisted route state keeps the shadow verdict. Conditional spread —
5596
5758
  // stage-off output stays byte-identical (no key emitted when absent).
@@ -5618,6 +5780,11 @@ function shouldIncludePreviousContext({
5618
5780
  const taskType = String(routingContext?.taskType ?? '').trim();
5619
5781
  const hasExplicitTarget = Boolean(String(routingContext?.targetFile ?? '').trim());
5620
5782
 
5783
+ // Greeting fast-path: a social turn needs no memory recall.
5784
+ if (routingContext?.greetingFastPath) {
5785
+ return false;
5786
+ }
5787
+
5621
5788
  if (!hasExplicitTarget) {
5622
5789
  return true;
5623
5790
  }
@@ -5750,6 +5917,16 @@ export async function persistSharedRouteState(filePath, state) {
5750
5917
  if (!result.ok) {
5751
5918
  console.error(`route-task: skill-router-state lock ${result.reason} after ${result.waitedMs}ms — state write skipped (fail-closed)`);
5752
5919
  }
5920
+ // Per-session sidecar (see skill-router.sh persistRouteState): survives a concurrent session
5921
+ // overwriting the shared single-slot file. Best-effort; keyed by the state's own owner.
5922
+ const owner = normalizeSessionId(state?.sessionId);
5923
+ if (owner) {
5924
+ try {
5925
+ const root = path.resolve(path.dirname(filePath), '..', '..');
5926
+ const safe = owner.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'default';
5927
+ await writeJsonAtomic(path.join(root, '.ukit', 'storage', 'cache', 'route-state', `${safe}.json`), state);
5928
+ } catch { /* shared write is authoritative */ }
5929
+ }
5753
5930
  return result;
5754
5931
  }
5755
5932