@ngockhoale/ukit 3.4.28 → 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.
- package/CHANGELOG.md +23 -0
- package/manifests/documentation.yaml +88 -0
- package/package.json +1 -1
- package/src/core/buildPlan.js +11 -1
- package/src/core/permissionPolicy.js +1 -1
- package/src/core/status.js +3 -1
- 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/taskRouting.js +25 -3
- 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 +23 -0
- package/template_project/.claude/hooks/skill-router.sh +105 -7
- package/template_project/.claude/settings.json +21 -1
- package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
- 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/route-catalog.mjs +1 -1
- package/template_project/.claude/ukit/index/route-task.mjs +208 -31
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +455 -24
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +122 -6
- 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 +32 -4
- package/template_user/playbooks/feature-implementation.md +9 -0
- 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
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
-
|
|
3912
|
-
|
|
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
|
|