@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
@@ -125,3 +125,129 @@ function foldTextForSearch(value) {
125
125
  .replace(/\p{M}+/gu, '')
126
126
  .toLowerCase();
127
127
  }
128
+
129
+ // ── Fuzzy identifier resolution ──
130
+ // Shared by resolveSymbolCandidates (queryIndex.js) and scoreDirectFileMatch's
131
+ // symbol_fuzzy tier: camelCase/snake_case/kebab-case identifiers normalize to
132
+ // the same folded token sequence, ordered subsequences bridge compacted queries
133
+ // ('checknull' ⊂ check_if_is_null), and a bounded Damerau distance catches
134
+ // misspellings ('custmer_code' → 'customer_code').
135
+
136
+ export function normalizeIdentifierTokens(value) {
137
+ return tokenizeFoldedText(foldTextForSearch(addCamelBoundaries(value)));
138
+ }
139
+
140
+ // Query variants for phrase-level matching: the folded text as-is, then the
141
+ // Vietnamese phrase aliases rewritten in place ('kiem tra null' → 'check null'),
142
+ // then token-level aliases. Each variant is an ordered, deduped token list.
143
+ export function expandIdentifierQueryVariants(value) {
144
+ const folded = foldTextForSearch(value);
145
+ const variants = [tokenizeFoldedText(folded)];
146
+
147
+ for (const entry of VIETNAMESE_PHRASE_ALIASES) {
148
+ entry.regex.lastIndex = 0;
149
+ if (!entry.regex.test(folded)) {
150
+ continue;
151
+ }
152
+ for (const expansion of entry.expansions) {
153
+ entry.regex.lastIndex = 0;
154
+ variants.push(tokenizeFoldedText(folded.replace(entry.regex, expansion)));
155
+ }
156
+ }
157
+
158
+ variants.push(variants[0].map((token) => VIETNAMESE_TOKEN_ALIASES.get(token)?.[0] ?? token));
159
+
160
+ const seen = new Set();
161
+ return variants
162
+ .map((tokens) => [...new Set(tokens)])
163
+ .filter((tokens) => tokens.length > 0)
164
+ .filter((tokens) => {
165
+ const key = tokens.join('\u0000');
166
+ if (seen.has(key)) {
167
+ return false;
168
+ }
169
+ seen.add(key);
170
+ return true;
171
+ })
172
+ .slice(0, 8);
173
+ }
174
+
175
+ // `queryTokens` fits inside `symbolTokens` in order ('check null' ⊆ check,if,null).
176
+ export function isOrderedTokenSubsequence(queryTokens, symbolTokens) {
177
+ if (queryTokens.length === 0 || queryTokens.length > symbolTokens.length) {
178
+ return false;
179
+ }
180
+ let cursor = 0;
181
+ for (const token of symbolTokens) {
182
+ if (token === queryTokens[cursor]) {
183
+ cursor += 1;
184
+ if (cursor === queryTokens.length) {
185
+ return true;
186
+ }
187
+ }
188
+ }
189
+ return false;
190
+ }
191
+
192
+ // Character-level ordered containment on compacted forms ('checknull' ⊂ 'checkifnull').
193
+ export function isCompactCharSubsequence(queryCompact, symbolCompact) {
194
+ if (!queryCompact || queryCompact.length > symbolCompact.length) {
195
+ return false;
196
+ }
197
+ let cursor = 0;
198
+ for (const char of symbolCompact) {
199
+ if (char === queryCompact[cursor]) {
200
+ cursor += 1;
201
+ if (cursor === queryCompact.length) {
202
+ return true;
203
+ }
204
+ }
205
+ }
206
+ return false;
207
+ }
208
+
209
+ // Damerau-Levenshtein (with transpositions), bounded: anything farther than
210
+ // `maxDistance` short-circuits to maxDistance + 1 so callers can treat the
211
+ // result as "beyond threshold" without walking the full matrix.
212
+ export function boundedDamerauDistance(a, b, maxDistance = 2) {
213
+ const left = String(a ?? '');
214
+ const right = String(b ?? '');
215
+ if (left === right) {
216
+ return 0;
217
+ }
218
+ if (Math.abs(left.length - right.length) > maxDistance) {
219
+ return maxDistance + 1;
220
+ }
221
+
222
+ let prevPrev = null;
223
+ let prev = Array.from({ length: right.length + 1 }, (_, j) => j);
224
+ for (let i = 1; i <= left.length; i += 1) {
225
+ const current = [i];
226
+ let rowMin = i;
227
+ for (let j = 1; j <= right.length; j += 1) {
228
+ const substitutionCost = left[i - 1] === right[j - 1] ? 0 : 1;
229
+ let value = Math.min(
230
+ prev[j] + 1,
231
+ current[j - 1] + 1,
232
+ prev[j - 1] + substitutionCost,
233
+ );
234
+ if (i > 1 && j > 1 && left[i - 1] === right[j - 2] && left[i - 2] === right[j - 1]) {
235
+ value = Math.min(value, prevPrev[j - 2] + 1);
236
+ }
237
+ current[j] = value;
238
+ if (value < rowMin) {
239
+ rowMin = value;
240
+ }
241
+ }
242
+ if (rowMin > maxDistance) {
243
+ return maxDistance + 1;
244
+ }
245
+ prevPrev = prev;
246
+ prev = current;
247
+ }
248
+ return prev[right.length];
249
+ }
250
+
251
+ // Minimum length both compacts must reach before edit-distance fuzzy matching
252
+ // applies — short identifiers (< 6) produce too many false neighbors.
253
+ export const FUZZY_EDIT_DISTANCE_MIN_LENGTH = 6;
@@ -15,7 +15,11 @@ export const INDEX_ARTIFACTS = {
15
15
  codegraph: 'codegraph.json',
16
16
  };
17
17
 
18
- export const INDEX_SCHEMA_VERSION = 8;
18
+ // v9: symbols.json gains global-registered ('global') and Vue 'method' rows,
19
+ // SQL 'column' rows with parent/schema, and calls.json gains per-object SQL
20
+ // reference records — a v8 cached artifact must not be reused for unchanged
21
+ // files or the new extractions silently vanish.
22
+ export const INDEX_SCHEMA_VERSION = 9;
19
23
 
20
24
  export function getIndexDir(rootDir) {
21
25
  return path.join(rootDir, '.cache', 'index');
@@ -22,6 +22,7 @@ import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
24
 
25
+ import { isRunAllRequest } from '../../template_project/.claude/ukit/runtime/handoff-intent.mjs';
25
26
  import { deriveEscalationTriggers, deriveRiskFloor } from './routeResolver.js';
26
27
 
27
28
  const PLAYBOOK_NAME_RE = /\.md$/i;
@@ -48,7 +49,10 @@ export const PLAYBOOK_ID_ALIASES = Object.freeze({
48
49
  // Compiled once; every row's discriminator reads the same normalized signals so
49
50
  // confusable pairs split on the discriminator column, never trigger text.
50
51
  const CREATE_VERB_RE = /\b(create|add|write|build|implement|scaffold|tạo|thêm)\b/i;
51
- const CHANGE_VERB_RE = /\b(fix|update|modify|change|rename|refactor|restructure|remove|delete|move|migrate|improve|patch|rework|sửa|đổi|cập nhật|thay thế|xóa)\b/i;
52
+ // Vietnamese words never match `\b` (JS word chars are ASCII-only — 'sửa' ends
53
+ // in a non-word char, 'đổi' starts with one), so the diacritic alternates use
54
+ // explicit lookarounds instead. Same word list as the original literal.
55
+ const CHANGE_VERB_RE = /(?:\b(?:fix|update|modify|change|rename|refactor|restructure|remove|delete|move|migrate|improve|patch|rework)\b|(?<![A-Za-z0-9_])(?:sửa|sua|đổi|doi|cập\s+nhật|cap\s+nhat|thay\s+thế|thay\s+the|thay|chỉnh(?:\s+sửa|\s+sua)?|chinh(?:\s+sua)?|xóa|xoa)(?![A-Za-z0-9_]))/i;
52
56
  const INVESTIGATION_TRIGGER_RE = /\b(how does|how do|why does|why do|why is|why are|explain|what happens|investigate|understand how|validate the assumption|tại sao|như thế nào|thế nào)\b/i;
53
57
  const DEFECT_TRIGGER_RE = /\b(fix|bug|defect|broken|crash(?:es|ed)?|regression|not working|doesn['’]?t work|won['’]?t work|fails?|failing|không hoạt động|lỗi)\b/i;
54
58
  const CHECK_VERB_RE = /\b(verify|check|confirm|prove|validate|assert)\b/i;
@@ -77,6 +81,68 @@ const AUTONOMOUS_TRIGGER_RE = /\b(finish (?:this|the) backlog|bounded (?:long )?
77
81
  const RELEASE_TRIGGER_RE = /\b(publish|ship it|release|bump (?:the )?version|deploy|cut a release|tag (?:the )?release)\b/i;
78
82
  const HANDOFF_CYCLE_TRIGGER_RE = /\b(handoff|multi[- ]?task cycle|task pipeline|execute the plan|worktree)\b/i;
79
83
 
84
+ // --- Routing-orchestration signals (reference/clone/SQL/reuse/run-all) -------
85
+ // Shared vocabulary for the discriminators below AND for the additive route
86
+ // summary fields route-task.mjs emits (referenceTargets/cloneIntent/sqlIntent/
87
+ // reuseFirst/goalScope). Kept here so both router surfaces read one definition.
88
+ // Reference-based ask: "like the Customer page", "làm giống trang Customer",
89
+ // "same as module Y", "using validation from Z" — resolve the named source.
90
+ const REFERENCE_TARGET_RE = /(?:\b(?:like|same\s+as|similar\s+to|based\s+on|modell?ed\s+(?:on|after))\s+(?:the\s+|this\s+|that\s+|our\s+)?(?:existing\s+)?(?:[\w-]{1,32}\s){0,2}(?:page|screen|module|component|view|flow|dropdown|select|form|report)\b)|\b(?:page|screen|module|component|view|flow)\b[^\n?]{0,32}\b(?:like|same\s+as)\b|(?<![A-Za-z0-9_])(?:giống|giong|tương\s+tự|tuong\s+tu|như)\s+(?:cái\s+)?(?:trang|page|module|component|tính\s+năng|tinh\s+nang|man\s+hình|view)|(?<![A-Za-z0-9_])(?:trang|page|module|component|man\s+hình)\s+(?:giống|giong|tương\s+tự|tuong\s+tu)|\b(?:using|reuse(?:d)?)\s+(?:the\s+)?(?:validation|logic|pattern|structure|style)\b[^\n?]{0,32}\bfrom\b/i;
91
+ // Clone ask: duplicate a named surface ("clone the Customer page", "nhân bản
92
+ // trang này", "copy the login screen"). A bare "clone the repo" (git) and a
93
+ // bare "copy" without a surface noun stay unmatched.
94
+ const CLONE_INTENT_RE = /(?:\b(?:clone|duplicate|copy)\s+(?:the\s+|this\s+|that\s+|our\s+|my\s+)?(?:existing\s+)?[\w-]{0,32}\s?(?:page|screen|module|component|feature|view|flow|form)\b)|(?<![A-Za-z0-9_])(?:nhân\s+bản|nhan\s+ban|sao\s+chép|sao\s+chep)(?![A-Za-z0-9_])/i;
95
+ // SQL-work ask: the prompt lives in the database layer (a .sql file, a view,
96
+ // column, table, stored function). Vietnamese table words break \b, hence the
97
+ // lookarounds.
98
+ const SQL_INTENT_RE = /(?:\.sql\b)|(?<![A-Za-z0-9_])(?:view|column|table|schema|cột|bảng|cot|bang|stored\s+(?:procedure|function))(?![A-Za-z0-9_])/i;
99
+ // Bare UI-ambiguous words (view/column/table/schema/cột/bảng) only count as SQL work
100
+ // next to SQL context: a DB word, a DDL phrase, a snake_case identifier, or a .sql file.
101
+ // "View the logs" / "a column of buttons" stay ordinary UI language.
102
+ const SQL_STRONG_RE = /(?:\.sql\b)|stored\s+(?:procedure|function)|\b(?:sql|ddl|dml|plpgsql|postgres(?:ql)?|mysql|sqlite)\b|\b(?:create|alter|drop|truncate)\s+(?:or\s+replace\s+)?(?:(?:a|an|the|new)\s+)?(?:\w+\s+)?(?:table|view|schema|function|procedure)\b/i;
103
+ const SQL_WEAK_RE = /(?<![A-Za-z0-9_])(?:view|column|table|schema|cột|bảng|cot|bang)(?![A-Za-z0-9_])/gi;
104
+ const SQL_IDENT_RE = /\b[a-z]+(?:_[a-z0-9]+){1,}\b/;
105
+ function hasSqlIntent(text) {
106
+ const t = String(text ?? '');
107
+ if (SQL_STRONG_RE.test(t)) return true;
108
+ const weak = new Set((t.match(SQL_WEAK_RE) || []).map((w) => w.toLowerCase()));
109
+ if (weak.size === 0 || /\bcolumn\s+of\b/i.test(t)) return false;
110
+ // A column+table pair, or a bare DB word beside a snake_case identifier, is schema work.
111
+ const pair = (weak.has('column') || weak.has('cột') || weak.has('cot')) && (weak.has('table') || weak.has('bảng') || weak.has('bang'));
112
+ return pair || SQL_IDENT_RE.test(t);
113
+ }
114
+ // The SQL ask touches shape/data rather than a read-only query — routes the
115
+ // migration row instead of a feature build.
116
+ const SQL_SCHEMA_RE = /(?<![A-Za-z0-9_])(?:ddl|alter|drop|(?:create|alter|drop|truncate)\s+table|(?:add|drop|rename|change|modify|update|thêm|xóa|xoá|sửa|sua|đổi|doi)\s+(?:a\s+|the\s+|new\s+|một\s+)*(?:column|cột|cot)(?!\s+of\b)|kiểu\s+dữ\s+liệu|kieu\s+du\s+lieu|data\s?type|not\s+null|constraint|migration|migrate)(?![A-Za-z0-9_])/i;
117
+ // Reuse-first ask: "use the existing/core function", "dùng hàm có sẵn",
118
+ // "tái sử dụng" — search for the function before writing a new one.
119
+ const REUSE_FIRST_RE = /(?:\buse\s+(?:the\s+)?(?:existing|core|shared|same|current)\b)|(?:\buse\s+[\w.]+\s+from\s+(?:the\s+)?(?:core|shared|utils?|common|lib)\b)|\b(?:existing|core|shared)\s+(?:function|helper|util(?:ity|ities)?|service|component)\b|\breuse\b|(?<![A-Za-z0-9_])(?:tái\s+sử\s+dụng|tai\s+su\s+dung)|(?<![A-Za-z0-9_])(?:dùng|dung|dựa|dua)\s+(?:vào\s+|vao\s+)?(?:hàm|ham)(?:\s+(?:có\s+sẵn|co\s+san|sẵn\s+có|san\s+co|đã\s+có|da\s+co|cũ|cu))?(?![A-Za-z0-9_])/i;
120
+ // Run-all goal scope: the prompt queues the WHOLE remaining backlog, never a
121
+ // single named task (single-task refs veto below). A scope signal only — it
122
+ // must never set mutation permission by itself.
123
+
124
+ // One bounded derivation for the route summary signals. Pure — no I/O. Both
125
+ // router surfaces call this; the discriminators above read the same booleans
126
+ // through buildRouteSignals so the route fields and playbook row selection
127
+ // cannot diverge.
128
+ export function deriveRouteOrchestrationSignals({ promptText = '', commandText = '' } = {}) {
129
+ const signalText = [promptText, commandText]
130
+ .map((value) => String(value || '').trim().toLowerCase())
131
+ .filter(Boolean)
132
+ .join('\n');
133
+ if (!signalText) {
134
+ return { referenceTarget: false, cloneIntent: false, sqlIntent: false, sqlSchemaChange: false, reuseFirst: false, runAll: false };
135
+ }
136
+ return {
137
+ referenceTarget: REFERENCE_TARGET_RE.test(signalText),
138
+ cloneIntent: CLONE_INTENT_RE.test(signalText),
139
+ sqlIntent: hasSqlIntent(signalText),
140
+ sqlSchemaChange: hasSqlIntent(signalText) && SQL_SCHEMA_RE.test(signalText),
141
+ reuseFirst: REUSE_FIRST_RE.test(signalText),
142
+ runAll: isRunAllRequest(signalText),
143
+ };
144
+ }
145
+
80
146
  // session-pickup discriminator needs the resumable-record check. ASYNC scan
81
147
  // (W3-PB2): the resolve path runs inside hook processes where a sync
82
148
  // readdirSync parks the event loop and can starve the hook self-deadline.
@@ -105,18 +171,32 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
105
171
  // confusable pair 1: no escalation trigger AND single-surface scope. The
106
172
  // `!s.escalated` clause is the BL-010 fast-path guard — any derived
107
173
  // floor-raising trigger (the six listed triggers map to risk codes +
108
- // contract signals) escalates to feature-implementation.
109
- discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && s.changeVerbOrCreate,
174
+ // contract signals) escalates to feature-implementation. Reference-driven,
175
+ // clone, reuse-first and SQL-schema asks take the same escape: they name a
176
+ // source of truth or a data contract the small path must not gloss over.
177
+ discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && !s.sqlSchemaChange && !s.referenceTarget && !s.cloneIntent && !s.reuseFirst && s.changeVerbOrCreate,
110
178
  fastPath: 'small-feature',
111
179
  },
112
180
  {
113
181
  workGroup: 'feature-implementation',
114
182
  playbookId: 'feature-implementation',
115
183
  lanes: ['local-build', 'shared-edit', 'map-impact'],
116
- triggers: [/implement|add|create|build|integrat|ship|deliver|wir(?:e|ed|ing)/i],
117
- // confusable pairs 1/5/8: articulated contract OR ≥1 escalation trigger,
118
- // and the ask is not a disposable spike nor the ship chain itself.
119
- discriminator: (s) => (s.contractSignal || s.multiSurface || s.escalated || s.wiredShipAsk) && !s.disposable && !s.releaseAsk,
184
+ triggers: [
185
+ /implement|add|create|build|integrat|ship|deliver|wir(?:e|ed|ing)/i,
186
+ REFERENCE_TARGET_RE,
187
+ CLONE_INTENT_RE,
188
+ REUSE_FIRST_RE,
189
+ ],
190
+ // confusable pairs 1/5/8: articulated contract OR ≥1 escalation trigger OR
191
+ // a reference/clone/reuse-driven ask — and the ask is not a disposable
192
+ // spike nor the ship chain itself. The reference/clone/reuse clauses only
193
+ // claim when no defect verb leads the ask ("fix login like signup" stays
194
+ // bug-fix); an escalation raised by SQL schema work yields to the
195
+ // migration row instead of capturing the build lane.
196
+ discriminator: (s) => (
197
+ (s.contractSignal || s.multiSurface || (s.escalated && !s.sqlSchemaChange) || s.wiredShipAsk)
198
+ || ((s.referenceTarget || s.cloneIntent || s.reuseFirst) && s.defectIdx === -1)
199
+ ) && !s.disposable && !s.releaseAsk,
120
200
  },
121
201
  {
122
202
  workGroup: 'bug-fix',
@@ -191,9 +271,19 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
191
271
  workGroup: 'migration',
192
272
  playbookId: 'migration',
193
273
  lanes: ['shared-edit', 'map-impact'],
194
- triggers: [/migrat|move (?:the |this |all )?[\w-]*|new schema|port (?:the|to)/i],
274
+ triggers: [
275
+ /migrat|move (?:the |this |all )?[\w-]*|new schema|port (?:the|to)/i,
276
+ // SQL schema work pairs a change verb with a shape noun even without a
277
+ // "migrate" verb — "sửa cột so_created_date trong view", "alter the
278
+ // users table". A bare noun ("fix the users table") is not enough —
279
+ // defect-led asks stay bug-fix.
280
+ /(?:\b(?:alter|drop|add|remove|rename|modify|change|resize)\b[^\n]{0,24}?\b(?:column|table)s?\b)|(?<![A-Za-z0-9_])(?:sửa|sua|đổi|doi|thay|chỉnh|chinh|thêm|them|xóa|xoa|cập\s+nhật|cap\s+nhat)\s+(?:cột|bảng|cot|bang)/i,
281
+ ],
195
282
  // confusable pair 7: behavior change + data/contract move (rollback needed).
196
- discriminator: (s) => s.dataMove,
283
+ // A SQL ask that touches shape (column/table/schema vocabulary) is the same
284
+ // class even without an explicit move verb — unless a defect verb leads the
285
+ // ask ("fix the users table" stays bug-fix).
286
+ discriminator: (s) => s.dataMove || (s.sqlIntent && s.sqlSchemaChange && s.defectIdx === -1),
197
287
  },
198
288
  {
199
289
  workGroup: 'verification',
@@ -333,6 +423,10 @@ async function buildRouteSignals({
333
423
  || /(?:chưa|chua|được không|duoc khong|đúng không|dung khong|không nhỉ|khong nhi|thế nào|the nao|như thế nào|nhu the nao|tại sao|tai sao|là gì|la gi|bao giờ|bao gio|khi nào|khi nao|ở đâu|o dau|không biết|khong biet)/.test(signalText)
334
424
  || /(?:chưa|chua|đúng không|dung khong|không|khong)\s*[?.!]?\s*$/.test(signalText);
335
425
  const questionAsk = questionShaped && !changeRequested;
426
+ const referenceTarget = REFERENCE_TARGET_RE.test(signalText);
427
+ const cloneIntent = CLONE_INTENT_RE.test(signalText);
428
+ const sqlIntent = hasSqlIntent(signalText);
429
+ const reuseFirst = REUSE_FIRST_RE.test(signalText);
336
430
  return {
337
431
  signalText,
338
432
  executionMode,
@@ -347,6 +441,11 @@ async function buildRouteSignals({
347
441
  questionAsk,
348
442
  checkFirst,
349
443
  changeVerbOrCreate: changeIdx !== -1 || createIdx !== -1,
444
+ referenceTarget,
445
+ cloneIntent,
446
+ sqlIntent,
447
+ sqlSchemaChange: sqlIntent && SQL_SCHEMA_RE.test(signalText),
448
+ reuseFirst,
350
449
  contractSignal: CONTRACT_SIGNAL_RE.test(signalText) || /\boauth\b/i.test(signalText),
351
450
  multiSurface: MULTI_SURFACE_RE.test(signalText),
352
451
  escalated: Array.isArray(escalationTriggers) && escalationTriggers.length > 0,