@ngockhoale/ukit 3.4.29 → 3.4.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/manifests/documentation.yaml +100 -0
- package/package.json +1 -1
- package/src/core/executionContracts.js +32 -0
- package/src/core/permissionPolicy.js +1 -1
- package/src/decision/client.js +15 -0
- package/src/decision/protocol.js +12 -14
- package/src/index/buildIndex.js +244 -4
- package/src/index/impactContext.js +91 -11
- package/src/index/languageTools.js +126 -0
- package/src/index/paths.js +5 -1
- package/src/index/playbookRegistry.js +108 -9
- package/src/index/queryIndex.js +415 -1
- package/src/index/routeCatalog.js +1 -1
- package/src/index/routeResolver.js +2 -2
- package/src/index/taskRouting.js +28 -4
- package/template_project/.claude/commands/ukit/handoff-create.md +5 -0
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +43 -4
- package/template_project/.claude/hooks/block-dangerous.mjs +7 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +235 -21
- package/template_project/.claude/hooks/record-execution.mjs +21 -0
- package/template_project/.claude/hooks/skill-router.sh +187 -16
- package/template_project/.claude/settings.json +43 -1
- package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
- package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +10 -4
- package/template_project/.claude/ukit/index/impact-context.mjs +70 -15
- package/template_project/.claude/ukit/index/land-worktree.mjs +2 -2
- package/template_project/.claude/ukit/index/lib/index-core.mjs +883 -14
- package/template_project/.claude/ukit/index/playbook-registry.mjs +108 -9
- package/template_project/.claude/ukit/index/query-index.mjs +100 -7
- package/template_project/.claude/ukit/index/review-verdict.mjs +14 -4
- package/template_project/.claude/ukit/index/route-catalog.mjs +1 -1
- package/template_project/.claude/ukit/index/route-resolver.mjs +34 -2
- package/template_project/.claude/ukit/index/route-task.mjs +243 -38
- package/template_project/.claude/ukit/index/sidecar-decision.mjs +3 -59
- package/template_project/.claude/ukit/index/unic-decision.mjs +27 -14
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +43 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +612 -56
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +122 -6
- package/template_project/.claude/ukit/runtime/loop-state.mjs +1 -1
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +87 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +417 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +635 -28
- package/template_project/.omp/hooks/pre/ukit-bridge.js +40 -5
- package/template_project/AGENTS.md +1 -1
- package/template_project/CLAUDE.md +1 -1
- package/template_project/instructions/core.md +1 -1
- package/template_user/playbooks/feature-implementation.md +9 -0
- package/template_user/playbooks/migration.md +4 -0
|
@@ -1132,6 +1132,35 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
1132
1132
|
}
|
|
1133
1133
|
}
|
|
1134
1134
|
|
|
1135
|
+
// Reuse/reference/clone/SQL asks name an EXISTING source (function, approved page, DDL
|
|
1136
|
+
// column). Live runs showed agents guessing without it (OMP: "use checknull from Core" ->
|
|
1137
|
+
// `import … from 'Core'`, an invented module). Emit one bounded line per detected signal
|
|
1138
|
+
// naming the deterministic resolver. Advisory only — never blocks, never throws.
|
|
1139
|
+
async function emitSourceResolutionAdvisory({ promptText, commandText, skip }) {
|
|
1140
|
+
try {
|
|
1141
|
+
if (skip) return;
|
|
1142
|
+
const registry = await loadPlaybookRegistry();
|
|
1143
|
+
if (!registry || typeof registry.deriveRouteOrchestrationSignals !== 'function') return;
|
|
1144
|
+
const sig = registry.deriveRouteOrchestrationSignals({ promptText, commandText });
|
|
1145
|
+
const text = String(promptText || '');
|
|
1146
|
+
const advice = [];
|
|
1147
|
+
if (sig.reuseFirst) {
|
|
1148
|
+
const named = text.match(/\buse\s+([A-Za-z_][\w.]*)\s+from\b/i)?.[1];
|
|
1149
|
+
advice.push(`Reuse-first: the ask names an existing function${named ? ` ("${named}")` : ''} — resolve the REAL definition with \`node .claude/ukit/index/query-index.mjs --symbol ${named || '<name>'}\`, read its signature/semantics, import it; never invent a module or re-implement it. A needs-confirmation candidate is a lookup result, not a substitution: state which symbol you chose.`);
|
|
1150
|
+
}
|
|
1151
|
+
if (sig.referenceTarget || sig.cloneIntent) {
|
|
1152
|
+
advice.push('Reference/clone: locate the named source page/component with `node .claude/ukit/index/resolve-context.mjs "<ask>"`, read it and its API/loader dependencies first, reuse the existing component, change only target-specific parts, and leave the reference untouched.');
|
|
1153
|
+
}
|
|
1154
|
+
if (sig.sqlIntent) {
|
|
1155
|
+
const ids = [...new Set(text.match(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g) || [])].slice(0, 3);
|
|
1156
|
+
advice.push(`SQL: resolve the identifier in the real DDL${ids.length ? ` (${ids.map((i) => `\`query-index.mjs --symbol ${i}\``).join(', ')})` : ''}; an exact alias match is not proof of a source column — verify table/schema, never invent a column, keep the requested output alias distinct from the source column, then check dependent objects.`);
|
|
1157
|
+
}
|
|
1158
|
+
for (const line of advice) {
|
|
1159
|
+
process.stdout.write(`[ukit-skill-router] ${line}\n`);
|
|
1160
|
+
}
|
|
1161
|
+
} catch {}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1135
1164
|
async function readProjectPackage(projectRoot) {
|
|
1136
1165
|
return readJson(path.join(projectRoot, 'package.json'), null);
|
|
1137
1166
|
}
|
|
@@ -1761,6 +1790,39 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
1761
1790
|
return null;
|
|
1762
1791
|
}
|
|
1763
1792
|
|
|
1793
|
+
|
|
1794
|
+
// Task semantics are independent of source execution strategy and observed progress.
|
|
1795
|
+
function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1796
|
+
const text = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase();
|
|
1797
|
+
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
1798
|
+
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
1799
|
+
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
1800
|
+
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1801
|
+
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
1802
|
+
const operation = actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy|ensure|dam bao)\b/.test(clause)
|
|
1803
|
+
&& /\bcron\b|scheduled\s+(?:cron\s+)?(?:task|job)|tac vu\s+(?:cron|dinh ky)/.test(clause));
|
|
1804
|
+
const config = actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause));
|
|
1805
|
+
const repository = Boolean(targetFile) || /[\w.-]+\.(?:json|ya?ml|toml|ini)\b/.test(text);
|
|
1806
|
+
const readOnly = /\b(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(text)
|
|
1807
|
+
&& !sourceOrder && !operation && !config;
|
|
1808
|
+
const taskKind = operation ? (sourceOrder ? 'MIXED' : 'OPERATIONAL')
|
|
1809
|
+
: config ? 'CONFIGURATION' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
1810
|
+
const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
|
|
1811
|
+
return { taskKind, ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
1812
|
+
...(operation ? { operationGoal: { domain: 'scheduled-job', target: id, desiredState: /\b(?:delete|remove|cancel|xoa|huy|absent|removed|deleted|no longer|khong con)\b/.test(actionable.join(' ')) ? 'absent' : 'present',
|
|
1813
|
+
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text) ? 'ensure-state' : 'execute-and-verify' } } : {}) };
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function applyTaskSemantics(contract, semantic) {
|
|
1817
|
+
const evidence = contract?.completionEvidence || [];
|
|
1818
|
+
const operationOnly = semantic.taskKind === 'OPERATIONAL'
|
|
1819
|
+
|| (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
|
|
1820
|
+
return { ...(contract || {}), ...semantic,
|
|
1821
|
+
completionEvidence: operationOnly ? ['operation-verification']
|
|
1822
|
+
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
1823
|
+
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1764
1826
|
function buildRouteSummary({
|
|
1765
1827
|
activeSkills = [],
|
|
1766
1828
|
routingContext = {},
|
|
@@ -1801,9 +1863,10 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
1801
1863
|
executionScores,
|
|
1802
1864
|
executionCandidates,
|
|
1803
1865
|
});
|
|
1804
|
-
const executionContract = buildExecutionContract(executionMode);
|
|
1866
|
+
const executionContract = applyTaskSemantics(buildExecutionContract(executionMode), classifyTaskSemantics({ promptText: routingContext.lastExplicitUserPromptText || routingContext.promptText, targetFile: routingContext.targetFile }));
|
|
1805
1867
|
const completionState = buildCompletionState({
|
|
1806
1868
|
executionMode,
|
|
1869
|
+
executionContract,
|
|
1807
1870
|
verificationRecommendation,
|
|
1808
1871
|
});
|
|
1809
1872
|
const continuationState = buildContinuationState({
|
|
@@ -1990,6 +2053,38 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
1990
2053
|
return questionSignal || consultShape;
|
|
1991
2054
|
}
|
|
1992
2055
|
|
|
2056
|
+
// Greeting fast-path (parity with route-task.mjs isPureGreetingPrompt): a
|
|
2057
|
+
// bare "hi" / "xin chào" / "thanks" (≤6 tokens, no command, no target) is an
|
|
2058
|
+
// informational turn — no mutating lane, no task debt. Any non-greeting
|
|
2059
|
+
// word ("hi, fix the login bug") disqualifies the prompt.
|
|
2060
|
+
function isPureGreetingPrompt({ promptText = '', commandText = '', targetFile = null } = {}) {
|
|
2061
|
+
const raw = String(promptText ?? '').trim();
|
|
2062
|
+
if (!raw || String(commandText ?? '').trim() || targetFile) return false;
|
|
2063
|
+
if (raw.length > 64) return false;
|
|
2064
|
+
const tokens = raw.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\u0111/g, 'd')
|
|
2065
|
+
.replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(Boolean);
|
|
2066
|
+
if (tokens.length === 0 || tokens.length > 6) return false;
|
|
2067
|
+
const core = new Set([
|
|
2068
|
+
'hi', 'hello', 'hey', 'yo', 'chao', 'xin', 'good',
|
|
2069
|
+
'morning', 'afternoon', 'evening',
|
|
2070
|
+
'thanks', 'thank', 'cam', 'on', 'bye', 'tam', 'biet',
|
|
2071
|
+
]);
|
|
2072
|
+
const filler = new Set([
|
|
2073
|
+
'you', 'u', 'there', 'guys', 'everyone', 'all',
|
|
2074
|
+
'ban', 'cac', 'minh', 'moi', 'nguoi', 'nhe', 'nha', 'nhieu',
|
|
2075
|
+
'ok', 'okay', 'again', 'still',
|
|
2076
|
+
]);
|
|
2077
|
+
let coreHit = false;
|
|
2078
|
+
for (const token of tokens) {
|
|
2079
|
+
if (core.has(token)) {
|
|
2080
|
+
coreHit = true;
|
|
2081
|
+
} else if (!filler.has(token)) {
|
|
2082
|
+
return false; // any non-greeting word means this is a task wearing a greeting
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return coreHit;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1993
2088
|
// TASK-C85-019: an explicit no-change/advisory assertion overrides the signal
|
|
1994
2089
|
// scores. A report or advisory question carrying fix vocabulary as SUBJECT
|
|
1995
2090
|
// MATTER ("không tạo thay đổi", "giữ nguyên mã", "do not change anything",
|
|
@@ -2053,6 +2148,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2053
2148
|
// — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
|
|
2054
2149
|
// order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
|
|
2055
2150
|
// TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
|
|
2151
|
+
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';
|
|
2056
2152
|
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*$)';
|
|
2057
2153
|
// C89-008: WAIT_TAIL ends a waiting-clause strip before any coordinating
|
|
2058
2154
|
// conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything
|
|
@@ -2061,7 +2157,12 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2061
2157
|
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*$)';
|
|
2062
2158
|
const residue = trimmed
|
|
2063
2159
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
2160
|
+
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
2161
|
+
.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, ' ')
|
|
2064
2162
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
2163
|
+
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
2164
|
+
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
2165
|
+
.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'), ' ')
|
|
2065
2166
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
2066
2167
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
2067
2168
|
.replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
@@ -2081,7 +2182,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2081
2182
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
2082
2183
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
2083
2184
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
2084
|
-
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|not|never|no)\\s+(?!only\\b)${TAIL}`, 'g'), ' ')
|
|
2085
2185
|
.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'), ' ')
|
|
2086
2186
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
2087
2187
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
@@ -2111,6 +2211,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2111
2211
|
// — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
|
|
2112
2212
|
// order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
|
|
2113
2213
|
// TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
|
|
2214
|
+
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';
|
|
2114
2215
|
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*$)';
|
|
2115
2216
|
// C89-008: WAIT_TAIL ends a waiting-clause strip before a coordinating
|
|
2116
2217
|
// conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything
|
|
@@ -2118,10 +2219,15 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2118
2219
|
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*$)';
|
|
2119
2220
|
const residue = trimmed
|
|
2120
2221
|
.replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
|
|
2222
|
+
// "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
|
|
2223
|
+
.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, ' ')
|
|
2121
2224
|
// Descriptive state, not an order: "đã/vừa [được] + verb" ("hooks vừa được cài đặt") reports
|
|
2122
2225
|
// something already done. Only the verb is stripped, so a later imperative still survives.
|
|
2123
2226
|
.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, ' ')
|
|
2124
2227
|
.replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
|
|
2228
|
+
// A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
|
|
2229
|
+
// verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
|
|
2230
|
+
.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'), ' ')
|
|
2125
2231
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
2126
2232
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
2127
2233
|
.replace(new RegExp(`\\bkhong\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
@@ -2143,7 +2249,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2143
2249
|
.replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
|
|
2144
2250
|
.replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
|
|
2145
2251
|
.replace(new RegExp(`\\b(?:demand(?:ed|s)?|require(?:d|s)?|forcing|forced|forcibly|must|has\\s+to|have\\s+to)\\b${TAIL}`, 'g'), ' ')
|
|
2146
|
-
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|not|never|no)\\s+(?!only\\b)${TAIL}`, 'g'), ' ')
|
|
2147
2252
|
.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'), ' ')
|
|
2148
2253
|
.replace(/\bco\s+(?:can|nen)\s+sua\b[^.,;\n?]*/g, ' ')
|
|
2149
2254
|
.replace(/(?:^|[.,;\n]\s*)(?:should|shall)\s+(?:i|we|you)\b[^.,;\n?]*/g, ' ')
|
|
@@ -2239,6 +2344,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2239
2344
|
/\b(?:chi|just|only)\s+(?:len|lap|vach|lam)\b[^.,;\n]{0,40}\b(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|blueprint)\b/.test(trimmed)
|
|
2240
2345
|
|| /\b(?:len|lap|vach|de\s+xuat|draft)\s+(?:ke\s+hoach|plan|phuong\s+an|lo\s+trinh|roadmap)\b/.test(trimmed)
|
|
2241
2346
|
|| /\b(?:plan|planning)\s+(?:only|first|mode)\b/.test(trimmed)
|
|
2347
|
+
|| /\b(?:chi|just|only)\s+(?:tao|create|make|write|draft)\s+(?:a\s+|an\s+|1\s+|mot\s+)?(?:plan|ke\s+hoach|phuong\s+an|roadmap)\b/.test(trimmed)
|
|
2242
2348
|
|| /\b(?:ke\s+hoach|plan|phuong\s+an|lo\s+trinh|roadmap)\b[^.,;\n]{0,40}\b(?:khong|chua|khong\s+can)\s+(?:implement|code|thuc\s+hien|thuc\s+thi|sua|lam|build|viet)\b/.test(trimmed)
|
|
2243
2349
|
|| (/\b(?:khong|chua|dung)\s+(?:implement|code|thuc\s+hien|thuc\s+thi|sua|lam|viet\s+code)\b/.test(trimmed)
|
|
2244
2350
|
&& /\b(?:plan|ke\s+hoach|phuong\s+an|roadmap)\b/.test(trimmed));
|
|
@@ -2626,12 +2732,12 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
2626
2732
|
};
|
|
2627
2733
|
}
|
|
2628
2734
|
|
|
2629
|
-
function buildCompletionState({ executionMode = null, verificationRecommendation = null } = {}) {
|
|
2735
|
+
function buildCompletionState({ executionMode = null, verificationRecommendation = null, executionContract = null } = {}) {
|
|
2630
2736
|
if (!executionMode || executionMode === 'informational') {
|
|
2631
2737
|
return null;
|
|
2632
2738
|
}
|
|
2633
2739
|
|
|
2634
|
-
const contract = buildExecutionContract(executionMode);
|
|
2740
|
+
const contract = executionContract || buildExecutionContract(executionMode);
|
|
2635
2741
|
const missingEvidence = [...(contract?.completionEvidence ?? [])];
|
|
2636
2742
|
const requiresVerification = missingEvidence.includes('verification-evidence');
|
|
2637
2743
|
if (
|
|
@@ -3509,14 +3615,68 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3509
3615
|
const sessionId = sessionCandidate === undefined || sessionCandidate === null
|
|
3510
3616
|
? null
|
|
3511
3617
|
: String(sessionCandidate).trim() || null;
|
|
3618
|
+
|
|
3619
|
+
// Per-session sidecar next to the shared single-slot state: a concurrent session routing
|
|
3620
|
+
// after this one overwrites the shared file, and this session's Stop would then see a
|
|
3621
|
+
// foreign owner and report its route as lost. Best-effort — the shared write is authoritative.
|
|
3622
|
+
async function persistRouteState(text) {
|
|
3623
|
+
const persisted = JSON.parse(text);
|
|
3624
|
+
persisted.taskInstanceId = taskInstanceId;
|
|
3625
|
+
if (continuingTask && previous.routingContext?.lastExplicitUserPromptText) {
|
|
3626
|
+
persisted.routingContext ||= {};
|
|
3627
|
+
persisted.routingContext.lastExplicitUserPromptText = previous.routingContext.lastExplicitUserPromptText;
|
|
3628
|
+
}
|
|
3629
|
+
const semantic = continuingTask && previous.routeSummary?.executionContract?.taskKind
|
|
3630
|
+
? previous.routeSummary.executionContract
|
|
3631
|
+
: classifyTaskSemantics({ promptText: extractPrompt(payload) || persisted.routingContext?.lastExplicitUserPromptText, targetFile: persisted.routingContext?.targetFile });
|
|
3632
|
+
const informationalTurn = !continuingTask && persisted.routingContext?.executionMode === 'informational'
|
|
3633
|
+
&& semantic.taskKind === 'CODE_IMPLEMENTATION';
|
|
3634
|
+
if (informationalTurn) {
|
|
3635
|
+
delete persisted.routeSummary;
|
|
3636
|
+
delete persisted.requestKey;
|
|
3637
|
+
} else if (semantic.taskKind !== 'CODE_IMPLEMENTATION' || persisted.routeSummary) {
|
|
3638
|
+
persisted.routeSummary ||= { executionMode: 'local-build' };
|
|
3639
|
+
persisted.routeSummary.executionContract = applyTaskSemantics(
|
|
3640
|
+
persisted.routeSummary.executionContract || buildExecutionContract(persisted.routeSummary.executionMode === 'informational' ? 'local-build' : persisted.routeSummary.executionMode), semantic);
|
|
3641
|
+
if (semantic.taskKind === 'OPERATIONAL' || semantic.taskKind === 'READ_ONLY'
|
|
3642
|
+
|| (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external')) {
|
|
3643
|
+
persisted.routeSummary.completionState = buildCompletionState({
|
|
3644
|
+
executionMode: persisted.routeSummary.executionMode === 'informational' ? 'local-build' : persisted.routeSummary.executionMode,
|
|
3645
|
+
executionContract: persisted.routeSummary.executionContract,
|
|
3646
|
+
});
|
|
3647
|
+
persisted.routeSummary.continuationState = buildContinuationState({ completionState: persisted.routeSummary.completionState });
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
text = JSON.stringify(persisted);
|
|
3651
|
+
await writeJsonFileAtomic(statePath, text);
|
|
3652
|
+
if (!sessionId) return;
|
|
3653
|
+
try {
|
|
3654
|
+
const dir = path.join(projectRoot, '.ukit', 'storage', 'cache', 'route-state');
|
|
3655
|
+
await writeJsonFileAtomic(path.join(dir, `${String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'default'}.json`), text);
|
|
3656
|
+
if (Math.random() < 0.05) {
|
|
3657
|
+
const cutoff = Date.now() - 14 * 24 * 3600 * 1000;
|
|
3658
|
+
for (const name of await fsp.readdir(dir)) {
|
|
3659
|
+
const file = path.join(dir, name);
|
|
3660
|
+
const st = await fsp.stat(file).catch(() => null);
|
|
3661
|
+
if (st && st.mtimeMs < cutoff) await fsp.rm(file, { force: true });
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
} catch {}
|
|
3665
|
+
}
|
|
3512
3666
|
const storedPrevious = await readJson(statePath, {});
|
|
3513
|
-
//
|
|
3514
|
-
|
|
3667
|
+
// The shared slot may belong to another session; recover only our owned sidecar.
|
|
3668
|
+
const sidecarPrevious = sessionId && String(storedPrevious?.sessionId || '').trim() !== sessionId
|
|
3669
|
+
? await readJson(path.join(projectRoot, '.ukit', 'storage', 'cache', 'route-state', `${sessionId.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'default'}.json`), {})
|
|
3670
|
+
: {};
|
|
3515
3671
|
const previous = sessionId
|
|
3516
|
-
?
|
|
3517
|
-
? storedPrevious
|
|
3518
|
-
: {})
|
|
3672
|
+
? [storedPrevious, sidecarPrevious].find((state) => String(state?.sessionId || '').trim() === sessionId) || {}
|
|
3519
3673
|
: storedPrevious;
|
|
3674
|
+
const continuationPrompt = /^(?:continue|resume|keep going|proceed|tiep tuc)(?:\s+(?:the task|this task|previous task))?[.!]?$/i.test(
|
|
3675
|
+
extractPrompt(payload).normalize('NFD').replace(/[̀-ͯ]/g, '').trim());
|
|
3676
|
+
const continuingTask = Boolean(previous.taskInstanceId && (payload.tool_name || continuationPrompt));
|
|
3677
|
+
const taskInstanceId = continuingTask
|
|
3678
|
+
? previous.taskInstanceId
|
|
3679
|
+
: `task-${crypto.randomBytes(16).toString("hex")}`;
|
|
3520
3680
|
const runtimeConfig = await loadRuntimeConfig(projectRoot);
|
|
3521
3681
|
// BL-013: the hook/bridge payload's transcript pointer (ukit-bridge.js:217,228
|
|
3522
3682
|
// forwards transcriptPath; Claude Code payloads carry transcript_path). The
|
|
@@ -3584,6 +3744,9 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3584
3744
|
const previousTs = Number(previous?.ts || 0);
|
|
3585
3745
|
const informationalNoTargetPrompt = (isInformationalPrompt({ promptText, commandText })
|
|
3586
3746
|
|| hasAdvisoryOnlyAssertion({ promptText, commandText })) && !filePath;
|
|
3747
|
+
// Greeting fast-path: a pure greeting gets the same read-only stamp as an
|
|
3748
|
+
// informational question — never a mutating lane, never task debt.
|
|
3749
|
+
const greetingFastPath = !filePath && isPureGreetingPrompt({ promptText, commandText });
|
|
3587
3750
|
// A delivery-only prompt ("push this to git", "đẩy bộ này lên git") starts a new,
|
|
3588
3751
|
// mutation-free action and typically matches no skill. It must not inherit the
|
|
3589
3752
|
// previous request's completion contract — carrying find-cause/map-impact debt
|
|
@@ -3629,7 +3792,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3629
3792
|
targetFile: filePath,
|
|
3630
3793
|
contextIntent: '',
|
|
3631
3794
|
taskType: '',
|
|
3632
|
-
...((informationalNoTargetPrompt || deliveryOnlyNoRoute) ? { executionMode: 'informational' } : {}),
|
|
3795
|
+
...((informationalNoTargetPrompt || deliveryOnlyNoRoute || greetingFastPath) ? { executionMode: 'informational' } : {}),
|
|
3633
3796
|
// Read-only/carried contexts: stamp whether THIS prompt ordered a
|
|
3634
3797
|
// mutation so the Stop gate can release a misrouted write-debt instead
|
|
3635
3798
|
// of looping "make an Edit" on an answer-only question.
|
|
@@ -3650,7 +3813,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3650
3813
|
});
|
|
3651
3814
|
|
|
3652
3815
|
if (
|
|
3653
|
-
previous?.fingerprint === fingerprint
|
|
3816
|
+
payload.tool_name && previous?.fingerprint === fingerprint
|
|
3654
3817
|
&& !previous?.routeSummary
|
|
3655
3818
|
&& Array.isArray(previous?.activeSkills)
|
|
3656
3819
|
&& previous.activeSkills.length === 0
|
|
@@ -3659,12 +3822,14 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3659
3822
|
process.exit(0);
|
|
3660
3823
|
}
|
|
3661
3824
|
|
|
3825
|
+
await emitSourceResolutionAdvisory({ promptText, commandText, skip: informationalNoTargetPrompt || greetingFastPath || deliveryOnlyNoRoute });
|
|
3826
|
+
|
|
3662
3827
|
await ensureDir(statePath);
|
|
3663
3828
|
// A no-route prompt (short nudge, informational question) must not wipe the last full
|
|
3664
3829
|
// route: completion-gate.sh reads routeSummary/requestKey from this state to decide
|
|
3665
3830
|
// whether a Stop is premature. Downgrading to a route-less state here silently disarms
|
|
3666
3831
|
// the gate mid-task, so carry the previous route forward until a real route replaces it.
|
|
3667
|
-
await
|
|
3832
|
+
await persistRouteState(JSON.stringify({
|
|
3668
3833
|
...(sessionId ? { sessionId } : {}),
|
|
3669
3834
|
fingerprint,
|
|
3670
3835
|
ts: now,
|
|
@@ -3809,7 +3974,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3809
3974
|
};
|
|
3810
3975
|
provisionalState.fingerprint = buildRouteStateFingerprint(provisionalState);
|
|
3811
3976
|
await ensureDir(statePath);
|
|
3812
|
-
await
|
|
3977
|
+
await persistRouteState(JSON.stringify(provisionalState));
|
|
3813
3978
|
|
|
3814
3979
|
let cacheUtils = null;
|
|
3815
3980
|
if (await pathExists(cacheUtilsPath)) {
|
|
@@ -3882,7 +4047,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3882
4047
|
}
|
|
3883
4048
|
|
|
3884
4049
|
await ensureDir(statePath);
|
|
3885
|
-
await
|
|
4050
|
+
await persistRouteState(JSON.stringify(reusedState));
|
|
3886
4051
|
await appendRouteAuditEntry(routeAuditPath, buildRouteAuditEntry({
|
|
3887
4052
|
routingContext: reusedState.routingContext || {},
|
|
3888
4053
|
routeSummary: rescuedRouteSummary || {},
|
|
@@ -3987,6 +4152,11 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
3987
4152
|
});
|
|
3988
4153
|
|
|
3989
4154
|
if (previous?.fingerprint === fingerprint && now - previousTs < debounceMs) {
|
|
4155
|
+
await appendRouteAuditEntry(routeAuditPath, buildRouteAuditEntry({
|
|
4156
|
+
routingContext,
|
|
4157
|
+
routeSummary,
|
|
4158
|
+
state: { requestKey, fingerprint, source: 'skill-router' },
|
|
4159
|
+
}));
|
|
3990
4160
|
process.exit(0);
|
|
3991
4161
|
}
|
|
3992
4162
|
|
|
@@ -4007,7 +4177,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
4007
4177
|
...(helpers ? { helpers } : {}),
|
|
4008
4178
|
routeSummary: compactRouteSummary(routeSummary),
|
|
4009
4179
|
};
|
|
4010
|
-
await
|
|
4180
|
+
await persistRouteState(JSON.stringify(sharedState));
|
|
4011
4181
|
await appendRouteAuditEntry(routeAuditPath, buildRouteAuditEntry({
|
|
4012
4182
|
routingContext,
|
|
4013
4183
|
routeSummary,
|
|
@@ -4032,6 +4202,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
4032
4202
|
|
|
4033
4203
|
process.stdout.write(`[ukit-skill-router] Read skills: ${getActiveSkillPaths(selected).join(', ')}\n`);
|
|
4034
4204
|
process.stdout.write(`[ukit-skill-router] Route: ${formatDisplayRouteSummary(routeSummary, routingContext)}\n`);
|
|
4205
|
+
await emitSourceResolutionAdvisory({ promptText, commandText, skip: false });
|
|
4035
4206
|
const nextDisplay = formatDisplayNextAction(routeSummary);
|
|
4036
4207
|
if (nextDisplay) {
|
|
4037
4208
|
process.stdout.write(`[ukit-skill-router] Next: ${nextDisplay}\n`);
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"Bash(rm -rf ..:*)",
|
|
50
50
|
"Bash(rm -rf .:*)",
|
|
51
51
|
"Bash(dd if=/dev/:*)",
|
|
52
|
-
"Bash(*> /dev/sda
|
|
52
|
+
"Bash(*> /dev/sda*)",
|
|
53
53
|
"Bash(mkfs:*)",
|
|
54
54
|
"Bash(:(){ :|:& };::*)",
|
|
55
55
|
"Bash(gh pr create:*)",
|
|
@@ -79,6 +79,16 @@
|
|
|
79
79
|
}
|
|
80
80
|
]
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"matcher": "MultiEdit|NotebookEdit|Agent|Task",
|
|
84
|
+
"hooks": [
|
|
85
|
+
{
|
|
86
|
+
"type": "command",
|
|
87
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\":8",
|
|
88
|
+
"timeout": 18
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
},
|
|
82
92
|
{
|
|
83
93
|
"matcher": "Bash",
|
|
84
94
|
"hooks": [
|
|
@@ -91,6 +101,16 @@
|
|
|
91
101
|
}
|
|
92
102
|
],
|
|
93
103
|
"PostToolUse": [
|
|
104
|
+
{
|
|
105
|
+
"matcher": "CronDelete|CronList",
|
|
106
|
+
"hooks": [
|
|
107
|
+
{
|
|
108
|
+
"type": "command",
|
|
109
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/record-execution.mjs\":8",
|
|
110
|
+
"timeout": 18
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
94
114
|
{
|
|
95
115
|
"matcher": "Read|Grep|Glob",
|
|
96
116
|
"hooks": [
|
|
@@ -111,6 +131,16 @@
|
|
|
111
131
|
}
|
|
112
132
|
]
|
|
113
133
|
},
|
|
134
|
+
{
|
|
135
|
+
"matcher": "MultiEdit|NotebookEdit|Agent|Task",
|
|
136
|
+
"hooks": [
|
|
137
|
+
{
|
|
138
|
+
"type": "command",
|
|
139
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/record-execution.mjs\":8",
|
|
140
|
+
"timeout": 18
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
114
144
|
{
|
|
115
145
|
"matcher": "Bash",
|
|
116
146
|
"hooks": [
|
|
@@ -122,6 +152,18 @@
|
|
|
122
152
|
]
|
|
123
153
|
}
|
|
124
154
|
],
|
|
155
|
+
"PostToolUseFailure": [
|
|
156
|
+
{
|
|
157
|
+
"matcher": ".*",
|
|
158
|
+
"hooks": [
|
|
159
|
+
{
|
|
160
|
+
"type": "command",
|
|
161
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/record-execution.mjs\":8",
|
|
162
|
+
"timeout": 18
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
],
|
|
125
167
|
"UserPromptSubmit": [
|
|
126
168
|
{
|
|
127
169
|
"hooks": [
|
|
@@ -167,7 +167,7 @@ const APPROVAL_KEYS = [
|
|
|
167
167
|
'schemaVersion', 'taskId', 'receiptSha256', 'verdict', 'executorModel',
|
|
168
168
|
'reviewerModel', 'reviewedResultTree',
|
|
169
169
|
];
|
|
170
|
-
const TASK_TOKEN = /^TASK-
|
|
170
|
+
const TASK_TOKEN = /^TASK-(?:[A-Z]\d{1,3}-)?\d{3,4}$/;
|
|
171
171
|
const OID = /^[0-9a-f]{40}$/;
|
|
172
172
|
|
|
173
173
|
function readJson(p, taskId, label) {
|
|
@@ -394,14 +394,20 @@ export async function applyFixLoopEscalation({
|
|
|
394
394
|
: null;
|
|
395
395
|
// A model lane that refuses to deepen is not an escalation answer — the
|
|
396
396
|
// deterministic deeper lane wins so the loop actually exits its lane.
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
// Lanes are capabilities, not an ordinal ladder. Without a validated policy
|
|
398
|
+
// relation only the deterministic destination is admissible.
|
|
399
|
+
const usableModelLane = planeStage !== 'shadow' && modelLane === deterministicLane
|
|
400
|
+
? modelLane : null;
|
|
401
|
+
const lane = deterministicLane;
|
|
399
402
|
const endpointDown = batchResult !== null
|
|
400
403
|
&& batchResult.status !== 'accepted'
|
|
401
404
|
&& batchResult.status !== 'partial'
|
|
402
405
|
&& !usableModelLane;
|
|
403
|
-
const
|
|
404
|
-
|
|
406
|
+
const depthFloor = resolveEscalationVerificationDepth({ routeSummary, config });
|
|
407
|
+
const depthOrder = ['none', 'runnable', 'review-round', 'escalate'];
|
|
408
|
+
const depth = planeStage !== 'shadow'
|
|
409
|
+
&& depthOrder.indexOf(modelDepth) > depthOrder.indexOf(depthFloor)
|
|
410
|
+
? modelDepth : depthFloor;
|
|
405
411
|
|
|
406
412
|
const reason = [
|
|
407
413
|
`fix-loop: fixLoopCount=${fixLoopCount} threshold=${settings.debugLoopThreshold} fingerprint-joined`,
|
|
@@ -34,30 +34,85 @@ async function main() {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
37
|
+
let impact = null;
|
|
38
|
+
try {
|
|
39
|
+
await ensureFreshIndex(rootDir);
|
|
40
|
+
impact = resolveImpactContext
|
|
41
|
+
? await resolveImpactContext({ rootDir, changedFiles, changedSymbols })
|
|
42
|
+
: {
|
|
43
|
+
changedFiles,
|
|
44
|
+
changedSymbols,
|
|
45
|
+
riskLabels: [],
|
|
46
|
+
callees: [],
|
|
47
|
+
callers: [],
|
|
48
|
+
importers: [],
|
|
49
|
+
mirrorCounterparts: [],
|
|
50
|
+
relatedTests: [],
|
|
51
|
+
recommendedTestFiles: [],
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Index missing, rebuild failed, or lock timed out — answer from source
|
|
55
|
+
// with a bounded search instead of exiting 1.
|
|
56
|
+
const answered = await runFallbackSearch(rootDir, changedFiles, changedSymbols);
|
|
57
|
+
if (!answered) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
52
62
|
|
|
53
63
|
printCompact(impact);
|
|
54
64
|
|
|
65
|
+
if ((impact.unverifiedImpact ?? []).length > 0) {
|
|
66
|
+
console.log(`unverified-impact: ${impact.unverifiedImpact.slice(0, CALLER_CAP).join(', ')}`);
|
|
67
|
+
console.log('note: name-only matches without import proof — verify before acting');
|
|
68
|
+
}
|
|
69
|
+
const truncated = impact.truncated ?? {};
|
|
70
|
+
const truncatedParts = Object.entries(truncated).filter(([, count]) => count > 0).map(([key, count]) => `${key}:+${count}`);
|
|
71
|
+
if (truncatedParts.length > 0) {
|
|
72
|
+
console.log(`truncated: ${truncatedParts.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
if (formatImpactConfidenceSummary) {
|
|
56
76
|
console.log('');
|
|
57
77
|
console.log(formatImpactConfidenceSummary(impact));
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
|
|
81
|
+
// Bounded source search over the changed file basenames and symbol names.
|
|
82
|
+
// Returns true when it produced output; false when the fallback itself is
|
|
83
|
+
// unavailable so the original index error can surface.
|
|
84
|
+
async function runFallbackSearch(rootDir, changedFiles, changedSymbols) {
|
|
85
|
+
if (typeof indexCore.boundedSourceSearch !== 'function') {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const queries = [
|
|
89
|
+
...changedFiles.map((filePath) => String(filePath).split('/').pop().replace(/\.[^.]+$/, '')),
|
|
90
|
+
...changedSymbols,
|
|
91
|
+
].filter(Boolean).slice(0, 4);
|
|
92
|
+
try {
|
|
93
|
+
console.log('source: fallback-search');
|
|
94
|
+
let printed = 0;
|
|
95
|
+
for (const searchQuery of queries) {
|
|
96
|
+
const rows = await indexCore.boundedSourceSearch({ rootDir, query: searchQuery, limit: 5 });
|
|
97
|
+
console.log(`[ukit:impact:fallback] ${rows.length} source matches for: ${searchQuery}`);
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
console.log(` ${row.filePath}${row.line ? `:${row.line}` : ''}`);
|
|
100
|
+
}
|
|
101
|
+
if (rows.truncated?.length) {
|
|
102
|
+
console.log(`[ukit:impact:fallback] search truncated (${rows.truncated.join(',')}) — NOT exhaustive`);
|
|
103
|
+
}
|
|
104
|
+
printed += rows.length;
|
|
105
|
+
}
|
|
106
|
+
if (printed === 0) {
|
|
107
|
+
console.log('[ukit:impact:fallback] no source matches — verify changed files manually');
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
} catch (fallbackError) {
|
|
111
|
+
console.error(`fallback-search failed: ${fallbackError && fallbackError.message ? fallbackError.message : fallbackError}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
61
116
|
async function ensureFreshIndex(rootDir) {
|
|
62
117
|
if (typeof indexCore.ensureIndexFresh === 'function') {
|
|
63
118
|
return indexCore.ensureIndexFresh({ rootDir });
|
|
@@ -168,8 +168,8 @@ function readScope() {
|
|
|
168
168
|
}
|
|
169
169
|
if (parsed.schemaVersion !== 1) refuse(parsed.taskId, 'invalid-scope', 'schemaVersion must be 1');
|
|
170
170
|
const { taskId, baseCommit, sourceRoot, destinationRoot, sourceHead, destinationHead, targetPaths } = parsed;
|
|
171
|
-
if (typeof taskId !== 'string' || !/^TASK-
|
|
172
|
-
refuse(taskId, 'invalid-scope', 'taskId must be an exact TASK-C97-009
|
|
171
|
+
if (typeof taskId !== 'string' || !/^TASK-(?:[A-Z]\d{1,3}-)?\d{3,4}$/.test(taskId)) {
|
|
172
|
+
refuse(taskId, 'invalid-scope', 'taskId must be an exact TASK-001 or TASK-C97-009 style token');
|
|
173
173
|
}
|
|
174
174
|
for (const [k, v] of Object.entries({ baseCommit, sourceRoot, destinationRoot, sourceHead, destinationHead })) {
|
|
175
175
|
if (typeof v !== 'string' || v.length === 0) refuse(taskId, 'invalid-scope', `${k} must be a nonempty string`);
|