@ngockhoale/ukit 3.4.47 → 3.4.49
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 +15 -0
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/worktree-preflight.mjs +20 -1
- package/src/core/diffPlan.js +38 -12
- package/src/core/executionContracts.js +79 -18
- package/src/core/metadata.js +4 -0
- package/src/core/runInstallPipeline.js +19 -2
- package/src/core/runtimeConfig.js +1 -1
- package/src/index/playbookRegistry.js +22 -3
- package/src/index/taskRouting.js +2 -2
- package/template_project/.claude/agents/bug-debugger.md +1 -0
- package/template_project/.claude/agents/code-reviewer.md +1 -0
- package/template_project/.claude/agents/feature-implementer.md +1 -0
- package/template_project/.claude/agents/handoff-planner.md +1 -0
- package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.claude/hooks/completion-gate.sh +1 -1
- package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
- package/template_project/.claude/hooks/skill-router.sh +159 -8
- package/template_project/.claude/settings.json +8 -8
- package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +79 -17
- package/template_project/.claude/ukit/index/route-task.mjs +115 -37
- package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +331 -21
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
- package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
- package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +79 -3
- package/template_project/.omp/agents/bug-debugger.md +1 -0
- package/template_project/.omp/agents/code-reviewer.md +1 -0
- package/template_project/.omp/agents/feature-implementer.md +1 -0
- package/template_project/.omp/agents/handoff-planner.md +1 -0
- package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
- package/template_project/ukit/storage/config.json +1 -1
- package/scripts/bench/data-foundation.mjs +0 -929
- package/scripts/bench/decision-runtime-baseline.mjs +0 -427
- package/scripts/bench/decision-runtime-variant.mjs +0 -626
- package/scripts/bench/goldTasks.json +0 -38
- package/scripts/bench/memory-ablation.mjs +0 -495
- package/scripts/bench/memory-baseline.mjs +0 -596
- package/scripts/bench/memory-bench.mjs +0 -661
- package/scripts/bench/memory-canary.mjs +0 -331
- package/scripts/bench/memory-corpus.mjs +0 -354
- package/scripts/bench/memory-gate.mjs +0 -389
- package/scripts/bench/memory-metrics.mjs +0 -179
- package/scripts/bench/outline-savings.mjs +0 -107
- package/scripts/bench/parallel-agents.mjs +0 -224
- package/scripts/bench/recorder-overhead.mjs +0 -204
- package/scripts/bench/runGold.mjs +0 -238
- package/scripts/bench/sqlite-spike.mjs +0 -451
- package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
- package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
- package/scripts/bench/v3-ceremony.mjs +0 -220
- package/scripts/perf/audit-perf.mjs +0 -1195
- package/scripts/perf/diff-perf-findings.mjs +0 -136
- package/scripts/perf/perf-findings.json +0 -474
- package/scripts/perf/perf-measure.md +0 -271
|
@@ -407,6 +407,45 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
407
407
|
return tierSelectionModule;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
// Tier lane: who performs the edit for the routed task (worker hand-off vs inline) for the MAIN
|
|
411
|
+
// session's tier. Same lazy pattern; a missing module degrades to no tier lane (legacy behavior).
|
|
412
|
+
let tierLaneModule = null;
|
|
413
|
+
let tierLaneLoadAttempted = false;
|
|
414
|
+
async function loadTierLane() {
|
|
415
|
+
if (!tierLaneLoadAttempted) {
|
|
416
|
+
tierLaneLoadAttempted = true;
|
|
417
|
+
try {
|
|
418
|
+
const modulePath = path.join(projectRoot, '.claude', 'ukit', 'index', 'tier-lane.mjs');
|
|
419
|
+
if (await pathExists(modulePath)) {
|
|
420
|
+
tierLaneModule = await import(pathToFileURL(modulePath).href);
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
tierLaneModule = null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return tierLaneModule;
|
|
427
|
+
}
|
|
428
|
+
// The persisted `tierLane` is what the Stop-side playbook acceptance reads (delegationRequired);
|
|
429
|
+
// the printed lines are what tell the model to dispatch the worker and tag its prompt.
|
|
430
|
+
function writeTierLaneLines(summary, taskId) {
|
|
431
|
+
const lane = summary?.tierLane;
|
|
432
|
+
if (!lane?.instruction) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
process.stdout.write(`[ukit-skill-router] Tier lane: ${lane.instruction}\n`);
|
|
436
|
+
if (lane.delegationRequired === true && taskId) {
|
|
437
|
+
process.stdout.write(`[ukit-skill-router] Handback: start each worker prompt with "[ukit-task ${taskId}]" so its result binds to this task\n`);
|
|
438
|
+
const hostHarness = process.env.UKIT_HARNESS || 'claude-code';
|
|
439
|
+
if (summary.playbookId === 'feature-implementation' && hostHarness !== 'claude-code') {
|
|
440
|
+
// omp/codex expose no agent identity to hooks: the gate cannot refuse orchestrator writes there,
|
|
441
|
+
// so say what is actually enforced instead of promising a refusal.
|
|
442
|
+
process.stdout.write('[ukit-skill-router] Delegation required: dispatch the worker; do not implement inline. This host gives hooks no worker identity, so orchestrator writes are NOT refused and worker attribution is reported UNVERIFIED (never a full PASS). Verify with a bare runner command (no pipe/tail/grep) and read its exit status.\n');
|
|
443
|
+
} else if (summary.playbookId === 'feature-implementation') {
|
|
444
|
+
process.stdout.write('[ukit-skill-router] Delegation gate: source/test writes from this orchestrator (Edit/Write/Bash heredoc) are refused until a native Tier 2 worker has handed back; repeated attempts end the run BLOCKED. Verify with a bare runner command (no pipe/tail/grep) and read its exit status.\n');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
410
449
|
// BL-013: session-history extractor — same lazy pathToFileURL pattern as the
|
|
411
450
|
// shared resolver. Async extraction (fsp tail read ≤16KiB) so a stalled mount
|
|
412
451
|
// never parks the event loop under the armed deadline (BUG-C21-04 posture);
|
|
@@ -620,6 +659,27 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
620
659
|
}
|
|
621
660
|
}
|
|
622
661
|
|
|
662
|
+
// A PreToolUse/continuation pass routes from the tool call alone (no prompt text), so the playbook
|
|
663
|
+
// the originating prompt selected resolves to null and would erase the task's playbook + delegation
|
|
664
|
+
// requirement from state — leaving the acceptance NOT_APPLICABLE. Within the SAME task instance the
|
|
665
|
+
// playbook is task-owned: carry it (and its tier lane) forward; never across task instances.
|
|
666
|
+
function inheritTaskPlaybook(routeSummary, previousState, taskId) {
|
|
667
|
+
const prev = previousState?.routeSummary;
|
|
668
|
+
if (!routeSummary || !prev?.playbookId) return;
|
|
669
|
+
if (!taskId || previousState.taskInstanceId !== taskId) return;
|
|
670
|
+
if (!routeSummary.playbookId) {
|
|
671
|
+
for (const key of ['playbookId', 'workGroup', 'playbookReason', 'playbookLoad', 'path', 'fastPathKind']) {
|
|
672
|
+
if (prev[key] !== undefined && prev[key] !== null) routeSummary[key] = prev[key];
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// The delegation demand is task-owned too: a later per-tool route (e.g. a docs Edit) derives its own
|
|
676
|
+
// lane from its own contract, and must not erase the demand the feature prompt established.
|
|
677
|
+
if (routeSummary.playbookId === prev.playbookId && prev.tierLane?.delegationRequired === true
|
|
678
|
+
&& routeSummary.tierLane?.delegationRequired !== true) {
|
|
679
|
+
routeSummary.tierLane = prev.tierLane;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
623
683
|
async function existsSkill(projectRoot, relativePath) {
|
|
624
684
|
return pathExists(path.join(projectRoot, relativePath));
|
|
625
685
|
}
|
|
@@ -750,7 +810,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
|
750
810
|
}
|
|
751
811
|
|
|
752
812
|
if (concreteTask) {
|
|
753
|
-
if (/\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|timeout)\b/.test(lower)) {
|
|
813
|
+
if (/\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|timeout)\b/.test(lower.replace(/\b(?:throw|raise|emit|reject|return|log|show|surface)(?:s|es|ing)?\s+(?:an?\s+|the\s+)?(?:[a-z-]+\s+){0,2}[a-z]*(?:error|exception)s?\b/g, ' '))) {
|
|
754
814
|
return 'debug-specific';
|
|
755
815
|
}
|
|
756
816
|
if (/\b(review|audit|diff|pr feedback|code review|kiem tra|soat)\b/.test(raw)) {
|
|
@@ -1821,19 +1881,51 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
1821
1881
|
// Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
1822
1882
|
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
1823
1883
|
const NON_ORDER_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|restart|sua|viet|chinh|them|tao|xoa';
|
|
1884
|
+
// Declared verification purpose ("Mục tiêu: xác nhận ...", "Goal: verify ..."). Under that declaration a
|
|
1885
|
+
// conditional repair/release branch ("Nếu phát hiện lỗi ... sửa ...", "Neu co source fix ... publish") is a
|
|
1886
|
+
// branch the check MAY take, not an unconditional order, and disposable test-project setup ("Tạo project thử
|
|
1887
|
+
// nghiệm độc lập") is scaffolding of the check. Unconditional orders in the same request keep blocking.
|
|
1888
|
+
const VERIFICATION_GOAL = /(?:^|\n)[ \t*#_-]*(?:muc tieu|goal|purpose|objective)\s*:[^\n]*\b(?:xac nhan|verify|confirm|kiem tra|validate|double-?check|re-?run)\b/;
|
|
1889
|
+
const CONDITIONAL_BRANCH = /(?:^|\n|\.\s+)[ \t]*(?:\d+[.)]\s*)?(?:neu\s+(?:phat hien\s+(?:false-block|loi|bug|defect)|co\s+(?:source fix|sua doi))|if\s+(?:(?:a\s+)?(?:bug|defect|failure)\s+is\s+found|(?:you\s+)?find\s+(?:a\s+)?(?:bug|defect)))\b[^\n]*?(?=\.\s+|$|\n)/g;
|
|
1890
|
+
const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
|
|
1891
|
+
const DISPOSABLE_SETUP = /\b(?:tao|create)\s+(?:mot\s+)?(?:project|du an)\s+(?:thu nghiem|test|disposable|doc lap)\b(?:\s+(?:doc lap|independent))?/g;
|
|
1824
1892
|
function neutralizeNonOrders(text) {
|
|
1825
|
-
|
|
1893
|
+
let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
|
|
1894
|
+
if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
|
|
1895
|
+
return source
|
|
1826
1896
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1827
1897
|
.replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1828
1898
|
.replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
|
|
1829
|
-
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
1899
|
+
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
1830
1900
|
.replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
|
|
1831
|
-
.replace(/\bchinh\s+xac\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b/g, ' ')
|
|
1901
|
+
.replace(/\bchinh\s+(?:xac|sach)\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b|\b(?:diem|cho|muc|phan)\s+(?:phai|can|nen)\s+(?:sua|fix)\b/g, ' ')
|
|
1832
1902
|
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1833
1903
|
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1834
1904
|
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
1835
1905
|
}
|
|
1836
1906
|
|
|
1907
|
+
const ORDER_VERBS = 'implement|fix|apply|refactor|modify|edit|write|update|add|create|build|install|deploy|redeploy|restart|push|commit|rename|remove|delete|replace|change|sua|viet|chinh|them|tao|xoa|enable|disable|set|toggle|configure|migrate|upgrade|publish|release|trien khai|khoi dong lai|(?:run|execute)\\s+(?:git\\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)|(?:npm|yarn)\\s+publish)|git\\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)';
|
|
1908
|
+
const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
|
|
1909
|
+
const MODAL_ORDER = new RegExp(`\\b(?:please|hay|vui long|need to|needs to|must|should|want(?:\\s+you)? to|toi muon|can phai|yeu cau)\\s+(?:[\\w-]+\\s+){0,2}(?:${ORDER_VERBS})\\b`);
|
|
1910
|
+
function hasExplicitOrder(clauses) {
|
|
1911
|
+
return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
|
|
1912
|
+
}
|
|
1913
|
+
// Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
|
|
1914
|
+
// confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
|
|
1915
|
+
// "template/install", "source change") as mutation orders. Order is structural: a request whose
|
|
1916
|
+
// command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
|
|
1917
|
+
// change-leading clause, orders no change - it owes verification evidence, not a write.
|
|
1918
|
+
const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
|
|
1919
|
+
const CHANGE_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:make|rebuild|redo|rewrite|restore|revert|correct|clean|patch|resolve|ensure|run(?!(?=[^.]*\b(?:tests?|suite|gate|checks?|lint|typecheck|pilot|smoke|e2e|verif\w*|vitest|jest|audit)\b)))\b/;
|
|
1920
|
+
function isVerificationOnlyRequest(promptText) {
|
|
1921
|
+
const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
|
|
1922
|
+
.replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
|
|
1923
|
+
const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
|
|
1924
|
+
.map((clause) => clause.split(/\bwithout\b/)[0]);
|
|
1925
|
+
return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
|
|
1926
|
+
&& !clauses.some((clause) => CHANGE_LEAD.test(clause));
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1837
1929
|
function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1838
1930
|
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
1839
1931
|
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
@@ -1862,10 +1954,15 @@ function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
|
1862
1954
|
// runtime are separate domains with separate proof; a prompt naming both stays a coding
|
|
1863
1955
|
// task rather than being half-verified. A code-change verb anywhere keeps write evidence.
|
|
1864
1956
|
const gitOperation = actionable.some((clause) => /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
|
|
1865
|
-
|
|
1957
|
+
// 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
|
|
1958
|
+
// when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
|
|
1959
|
+
// "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
|
|
1960
|
+
const runtimeOperation = actionable.some((clause) => /(?<!\b(?:process|crash|hard|soft|graceful|after|upon|during|across|following)\s)\b(?:restart|redeploy|deploy|khoi dong lai|trien khai\s+(?:len\s+)?(?:service|server|production|prod|staging|dich vu|may chu|moi truong))\b/.test(clause)
|
|
1866
1961
|
&& !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
|
|
1867
1962
|
const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
|
|
1868
1963
|
const operation = Boolean(nativeDomain);
|
|
1964
|
+
// A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
|
|
1965
|
+
if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
|
|
1869
1966
|
// A config MENTION is not a config change: the same clause needs a mutation verb, and a
|
|
1870
1967
|
// request that also changes source/code is a coding task (config is part of its tests/fixtures).
|
|
1871
1968
|
const config = !sourceOrder && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause)
|
|
@@ -1895,7 +1992,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
1895
1992
|
completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
|
|
1896
1993
|
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
1897
1994
|
: semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
|
|
1898
|
-
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|
|
1995
|
+
: semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
|
|
1899
1996
|
}
|
|
1900
1997
|
|
|
1901
1998
|
function buildRouteSummary({
|
|
@@ -2002,7 +2099,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2002
2099
|
const buildSignal = /\b(build|create|add|implement|feature)\b/.test(raw);
|
|
2003
2100
|
const debugSignal = intentMode === 'debug-specific' || /\b(root cause|debug|triage|flaky|investigate|why)\b/.test(raw);
|
|
2004
2101
|
const reviewSignal = intentMode === 'review-specific' || /\b(review|audit|verify|release readiness)\b/.test(raw);
|
|
2005
|
-
const failureSignal = /\b(failing|failed|broken|error|crash|timeout|undefined|exception|eacces|trace)\b/.test(raw);
|
|
2102
|
+
const failureSignal = /\b(failing|failed|broken|error|crash|timeout|undefined|exception|eacces|trace)\b/.test(raw.replace(/\b(?:throw|raise|emit|reject|return|log|show|surface)(?:s|es|ing)?\s+(?:an?\s+|the\s+)?(?:[a-z-]+\s+){0,2}[a-z]*(?:error|exception)s?\b/g, ' '));
|
|
2006
2103
|
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);
|
|
2007
2104
|
const boundedEditSignal = /\b(one[- ]line|small|tiny|single|local|guard|log line|loading message|label)\b/.test(raw);
|
|
2008
2105
|
const implementSignal = /\b(implement|apply|update|modify|add|create|ship|deliver|fix)\b/.test(raw)
|
|
@@ -2294,6 +2391,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2294
2391
|
.replace(/\u0111/g, 'd');
|
|
2295
2392
|
const trimmed = folded.trim();
|
|
2296
2393
|
if (!trimmed) return false;
|
|
2394
|
+
if (!String(commandText ?? '').trim() && classifyTaskSemantics({ promptText }).verificationOnly === true) return false;
|
|
2297
2395
|
// C85-019c: clause strips must stop BEFORE a conjunction-joined imperative
|
|
2298
2396
|
// — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
|
|
2299
2397
|
// order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
|
|
@@ -3218,6 +3316,9 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3218
3316
|
// no-map routes keep the pre-BL-019 compact shape (identical to
|
|
3219
3317
|
// route-task.mjs compactRouteSummary).
|
|
3220
3318
|
...(routeSummary.measuredTier != null ? { measuredTier: routeSummary.measuredTier } : {}),
|
|
3319
|
+
// The tier lane survives compaction: the Stop-side playbook acceptance reads
|
|
3320
|
+
// tierLane.delegationRequired off the persisted state (identical to route-task.mjs).
|
|
3321
|
+
...(routeSummary.tierLane ? { tierLane: routeSummary.tierLane } : {}),
|
|
3221
3322
|
...(routeSummary.routeVersion !== undefined
|
|
3222
3323
|
? {
|
|
3223
3324
|
routeVersion: routeSummary.routeVersion,
|
|
@@ -3296,6 +3397,22 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3296
3397
|
// BL-013: bounded history counters on the audit row — identical to the
|
|
3297
3398
|
// helper's buildRouteAuditEntry emission.
|
|
3298
3399
|
historySignals: routeSummary.historySignals ?? null,
|
|
3400
|
+
// Orchestration telemetry — identical to the helper's buildOrchestrationAudit: requested
|
|
3401
|
+
// lane/effort next to host evidence; effective model is never inferred (UNKNOWN).
|
|
3402
|
+
orchestration: routeSummary?.tierLane && typeof routeSummary.tierLane === 'object'
|
|
3403
|
+
? {
|
|
3404
|
+
mainTier: routeSummary.tierLane.mainTier ?? null,
|
|
3405
|
+
mainTierSource: routeSummary.tierLane.mainTierSource ?? null,
|
|
3406
|
+
requestedTier: routeSummary.tierLane.tier ?? null,
|
|
3407
|
+
lane: routeSummary.tierLane.instruction
|
|
3408
|
+
? (routeSummary.tierLane.mainTier === 'smart' ? 'dispatch-worker' : 'handoff')
|
|
3409
|
+
: 'inline',
|
|
3410
|
+
requestedEffort: routeSummary.tierLane.effort ?? null,
|
|
3411
|
+
effortBinding: routeSummary.tierLane.effortBinding ?? null,
|
|
3412
|
+
effectiveEffort: process.env.CLAUDE_EFFORT || 'UNKNOWN',
|
|
3413
|
+
effectiveModel: 'UNKNOWN',
|
|
3414
|
+
}
|
|
3415
|
+
: null,
|
|
3299
3416
|
};
|
|
3300
3417
|
}
|
|
3301
3418
|
|
|
@@ -3849,6 +3966,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3849
3966
|
// (a prompt that lists "git push / npm publish" became a new MIXED git-integrity operation that a
|
|
3850
3967
|
// verification-only turn can never satisfy). Only transcript metadata set by the harness or the
|
|
3851
3968
|
// fixed compaction preamble counts - never the prompt's claims about itself.
|
|
3969
|
+
let coordinationTurn = false;
|
|
3852
3970
|
const systemOriginTurn = (() => {
|
|
3853
3971
|
const promptTrim = extractPrompt(payload).trim();
|
|
3854
3972
|
if (!promptTrim) return false;
|
|
@@ -3865,13 +3983,29 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3865
3983
|
const lines = buf.toString('utf8').split('\n');
|
|
3866
3984
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
3867
3985
|
const line = lines[i];
|
|
3868
|
-
if (!line
|
|
3986
|
+
if (!line) continue;
|
|
3869
3987
|
let entry; try { entry = JSON.parse(line); } catch { continue; }
|
|
3988
|
+
// Cross-session delivery is queued, not a user order. Require the host's
|
|
3989
|
+
// consumed delivery record, exact content and this session's owner. An
|
|
3990
|
+
// enqueue, a foreign delivery or a human-pasted envelope is not proof.
|
|
3991
|
+
if (entry.type === 'queue-operation' && entry.operation === 'remove'
|
|
3992
|
+
&& entry.reason === 'absorbed_mid_turn' && entry.deliveryId
|
|
3993
|
+
&& sessionId && entry.sessionId === sessionId
|
|
3994
|
+
&& typeof entry.content === 'string' && entry.content.trim() === promptTrim
|
|
3995
|
+
&& /^<cross-session-message\s/.test(promptTrim)) {
|
|
3996
|
+
coordinationTurn = true;
|
|
3997
|
+
return false;
|
|
3998
|
+
}
|
|
3870
3999
|
if (entry.type !== 'user' || entry.isSidechain === true) continue;
|
|
3871
4000
|
const content = entry.message?.content;
|
|
3872
4001
|
const text = (typeof content === 'string' ? content
|
|
3873
4002
|
: Array.isArray(content) ? content.map((b) => (b?.type === 'tool_result' ? '\u0000' : b?.text || '')).join('\n') : '').trim();
|
|
3874
4003
|
if (!text || text.includes('\u0000') || /^Stop hook feedback/i.test(text)) continue;
|
|
4004
|
+
if (entry.isMeta === true && entry.promptSource === 'system' && entry.turnOrigin === 'system'
|
|
4005
|
+
&& text === promptTrim && /^\[Cross-session (?:idle|delivery) notice\]/.test(text)) {
|
|
4006
|
+
coordinationTurn = true;
|
|
4007
|
+
return false;
|
|
4008
|
+
}
|
|
3875
4009
|
return entry.isMeta === true && (entry.turnOrigin === 'scheduled' || entry.promptSource === 'system')
|
|
3876
4010
|
&& text === promptTrim;
|
|
3877
4011
|
}
|
|
@@ -3879,6 +4013,9 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3879
4013
|
} catch {}
|
|
3880
4014
|
return false;
|
|
3881
4015
|
})();
|
|
4016
|
+
// A notification has no task boundary or routing authority. Leave even an
|
|
4017
|
+
// incomplete contract untouched; peer prose cannot clear or create its debt.
|
|
4018
|
+
if (coordinationTurn) process.exit(0);
|
|
3882
4019
|
const continuingTask = Boolean(previous.taskInstanceId && (payload.tool_name || continuationPrompt || systemOriginTurn));
|
|
3883
4020
|
const taskInstanceId = continuingTask
|
|
3884
4021
|
? previous.taskInstanceId
|
|
@@ -4168,6 +4305,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4168
4305
|
activeSkills: selected,
|
|
4169
4306
|
historySignals: routeHistorySignals,
|
|
4170
4307
|
}));
|
|
4308
|
+
inheritTaskPlaybook(provisionalRouteSummary, previous, taskInstanceId);
|
|
4171
4309
|
const provisionalState = {
|
|
4172
4310
|
...(sessionId ? { sessionId } : {}),
|
|
4173
4311
|
requestKey,
|
|
@@ -4237,6 +4375,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4237
4375
|
historySignals: routeHistorySignals,
|
|
4238
4376
|
});
|
|
4239
4377
|
mergeResolverFields(rescuedRouteSummary, reusedSharedFields, { onlyMissing: true });
|
|
4378
|
+
inheritTaskPlaybook(rescuedRouteSummary, previous, taskInstanceId);
|
|
4240
4379
|
}
|
|
4241
4380
|
const reusedState = {
|
|
4242
4381
|
...cachedRouteState,
|
|
@@ -4278,6 +4417,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4278
4417
|
if (reusedState.routeSummary?.delegateHint) {
|
|
4279
4418
|
process.stdout.write(`[ukit-skill-router] Delegate: ${reusedState.routeSummary.delegateHint}\n`);
|
|
4280
4419
|
}
|
|
4420
|
+
writeTierLaneLines(reusedState.routeSummary, reusedState.taskInstanceId || taskInstanceId);
|
|
4281
4421
|
if (
|
|
4282
4422
|
reusedState.routeSummary?.helperHint
|
|
4283
4423
|
&& !reusedState.routeSummary?.nextActionCommand
|
|
@@ -4335,6 +4475,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4335
4475
|
historySignals: routeHistorySignals,
|
|
4336
4476
|
});
|
|
4337
4477
|
mergeResolverFields(routeSummary, finalSharedFields);
|
|
4478
|
+
inheritTaskPlaybook(routeSummary, previous, taskInstanceId);
|
|
4338
4479
|
// BL-019 (SPEC FR-001): fold the persisted measured map into the route —
|
|
4339
4480
|
// identical semantics to route-task.mjs finalizeTaskRoute / taskRouting.js
|
|
4340
4481
|
// deriveTaskRoute: measuredTier emitted additively; modelTier follows the
|
|
@@ -4348,6 +4489,15 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4348
4489
|
} catch {
|
|
4349
4490
|
// static map stays authoritative
|
|
4350
4491
|
}
|
|
4492
|
+
try {
|
|
4493
|
+
const tierLane = await loadTierLane();
|
|
4494
|
+
if (tierLane && typeof tierLane.applyTierLaneToRouteSummary === 'function') {
|
|
4495
|
+
tierLane.applyTierLaneToRouteSummary(routeSummary, { rootDir: projectRoot, config: runtimeConfig, subagentSession: payload.ukit_subagent_session === true, sessionId: payload.session_id ?? null });
|
|
4496
|
+
}
|
|
4497
|
+
} catch {
|
|
4498
|
+
// no tier lane: the legacy inline behavior applies
|
|
4499
|
+
}
|
|
4500
|
+
inheritTaskPlaybook(routeSummary, previous, taskInstanceId);
|
|
4351
4501
|
|
|
4352
4502
|
|
|
4353
4503
|
const fingerprint = buildRouteStateFingerprint({
|
|
@@ -4416,6 +4566,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4416
4566
|
if (routeSummary.delegateHint) {
|
|
4417
4567
|
process.stdout.write(`[ukit-skill-router] Delegate: ${routeSummary.delegateHint}\n`);
|
|
4418
4568
|
}
|
|
4569
|
+
writeTierLaneLines(routeSummary, taskInstanceId);
|
|
4419
4570
|
if (
|
|
4420
4571
|
routeSummary.helperHint
|
|
4421
4572
|
&& !routeSummary.nextActionCommand
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"model": "
|
|
2
|
+
"model": "opus",
|
|
3
3
|
"fastMode": false,
|
|
4
4
|
"fastModePerSessionOptIn": false,
|
|
5
5
|
"alwaysThinkingEnabled": true,
|
|
6
|
-
"effortLevel": "
|
|
6
|
+
"effortLevel": "medium",
|
|
7
7
|
"includeGitInstructions": false,
|
|
8
8
|
"promptSuggestionEnabled": false,
|
|
9
9
|
"showThinkingSummaries": false,
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"hooks": [
|
|
75
75
|
{
|
|
76
76
|
"type": "command",
|
|
77
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/stale-spec-guard.sh\":11 \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-edit-backup.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-router.sh\":20 \"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-hardcap-gate.sh\":8",
|
|
78
|
-
"timeout":
|
|
77
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/stale-spec-guard.sh\":11 \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-edit-backup.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-router.sh\":20 \"$CLAUDE_PROJECT_DIR/.claude/hooks/delegation-gate.mjs\":10 \"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-hardcap-gate.sh\":8",
|
|
78
|
+
"timeout": 83
|
|
79
79
|
}
|
|
80
80
|
]
|
|
81
81
|
},
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
"hooks": [
|
|
85
85
|
{
|
|
86
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":
|
|
87
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/delegation-gate.mjs\":10 \"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\":8",
|
|
88
|
+
"timeout": 28
|
|
89
89
|
}
|
|
90
90
|
]
|
|
91
91
|
},
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
"hooks": [
|
|
95
95
|
{
|
|
96
96
|
"type": "command",
|
|
97
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/auto-allow-bash.sh\":15 \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.mjs\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/sensitive-data-guard.mjs\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-hardcap-gate.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/verification-guard.sh\":8",
|
|
98
|
-
"timeout":
|
|
97
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/ukit/runtime/hook-chain-runner.mjs\" --emit-verdict - \"$CLAUDE_PROJECT_DIR/.claude/hooks/auto-allow-bash.sh\":15 \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.mjs\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/sensitive-data-guard.mjs\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/delegation-gate.mjs\":10 \"$CLAUDE_PROJECT_DIR/.claude/hooks/handoff-model-guard.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-hardcap-gate.sh\":8 \"$CLAUDE_PROJECT_DIR/.claude/hooks/verification-guard.sh\":8",
|
|
98
|
+
"timeout": 75
|
|
99
99
|
}
|
|
100
100
|
]
|
|
101
101
|
}
|
|
@@ -113,6 +113,20 @@ function hasSqlIntent(text) {
|
|
|
113
113
|
// The SQL ask touches shape/data rather than a read-only query — routes the
|
|
114
114
|
// migration row instead of a feature build.
|
|
115
115
|
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;
|
|
116
|
+
// Explicit delegation order: the user themself asks for a worker/subagent to implement
|
|
117
|
+
// ("Delegate the implementation to a worker agent", "have a subagent implement it"). That is an
|
|
118
|
+
// opt-in to the orchestrated path (Tier 3 orchestrates, Tier 2 implements, integration verified by
|
|
119
|
+
// the orchestrator), so it lifts the small-feature fast path into feature-implementation. Live omp
|
|
120
|
+
// run: this exact order stayed on small-feature (explicit-local-target fast path, subagents
|
|
121
|
+
// suppressed) and the Tier 3 main implemented nothing of the delegation. Bare mentions of the
|
|
122
|
+
// words ("worker thread", "the delegate pattern") do not match; negated orders ("don't delegate")
|
|
123
|
+
// veto.
|
|
124
|
+
const DELEGATION_ORDER_RE = /\bdelegat(?:e|ing)\b[^.\n]{0,80}?\bto\s+(?:an?\s+|the\s+|your\s+)?(?:[\w-]+\s+){0,2}?(?:worker|sub-?agents?|agents?|implementer)\b|\b(?:use|spawn|dispatch|launch|assign|have|let)\s+(?:an?\s+|the\s+|your\s+)?(?:[\w-]+\s+)?(?:worker|sub-?agents?|implementer)(?:\s+agent)?\s+(?:to|for)\b|(?<![A-Za-z0-9_])giao\s+(?:việc\s+)?cho\s+(?:worker|sub-?agent|agent)/i;
|
|
125
|
+
const DELEGATION_NEGATED_RE = /\b(?:don['’]?t|do\s+not|never|without|avoid|no\s+need\s+to)\s+(?:[\w-]+\s+){0,2}?(?:delegat\w*|(?:use|spawn|dispatch)\s+(?:an?\s+|any\s+)?(?:worker|sub-?agents?))|\bno\s+(?:worker|sub-?agents?|delegation)\b|\b(?:inline|yourself)\s+(?:only|not)\b/i;
|
|
126
|
+
export function hasDelegationOrder(text) {
|
|
127
|
+
const t = String(text ?? '');
|
|
128
|
+
return DELEGATION_ORDER_RE.test(t) && !DELEGATION_NEGATED_RE.test(t);
|
|
129
|
+
}
|
|
116
130
|
// Reuse-first ask: "use the existing/core function", "dùng hàm có sẵn",
|
|
117
131
|
// "tái sử dụng" — search for the function before writing a new one.
|
|
118
132
|
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;
|
|
@@ -173,7 +187,7 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
173
187
|
// contract signals) escalates to feature-implementation. Reference-driven,
|
|
174
188
|
// clone, reuse-first and SQL-schema asks take the same escape: they name a
|
|
175
189
|
// source of truth or a data contract the small path must not gloss over.
|
|
176
|
-
discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && !s.sqlSchemaChange && !s.referenceTarget && !s.cloneIntent && !s.reuseFirst && s.changeVerbOrCreate,
|
|
190
|
+
discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && !s.sqlSchemaChange && !s.referenceTarget && !s.cloneIntent && !s.reuseFirst && !s.delegationOrdered && s.changeVerbOrCreate,
|
|
177
191
|
fastPath: 'small-feature',
|
|
178
192
|
},
|
|
179
193
|
{
|
|
@@ -194,7 +208,7 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
194
208
|
// migration row instead of capturing the build lane.
|
|
195
209
|
discriminator: (s) => (
|
|
196
210
|
(s.contractSignal || s.multiSurface || (s.escalated && !s.sqlSchemaChange) || s.wiredShipAsk)
|
|
197
|
-
|| ((s.referenceTarget || s.cloneIntent || s.reuseFirst) && s.defectIdx === -1)
|
|
211
|
+
|| ((s.referenceTarget || s.cloneIntent || s.reuseFirst || s.delegationOrdered) && s.defectIdx === -1)
|
|
198
212
|
) && !s.disposable && !s.releaseAsk,
|
|
199
213
|
},
|
|
200
214
|
{
|
|
@@ -290,7 +304,9 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
290
304
|
lanes: ['local-fix', 'review-release'],
|
|
291
305
|
triggers: [/\bverify\b|\bcheck\b|\bconfirm\b|\bprove\b|\bvalidate\b|\bassert\b/i],
|
|
292
306
|
// the ask IS a check — a check verb leads, or no defect verb exists at all.
|
|
293
|
-
|
|
307
|
+
// A build/create verb that leads the ask makes the check its closing step ("implement X … then run the
|
|
308
|
+
// tests and confirm"), not the ask itself — the build row owns it instead of tying into ambiguity.
|
|
309
|
+
discriminator: (s) => (s.checkFirst || s.defectIdx === -1) && !s.buildLeadsCheck,
|
|
294
310
|
},
|
|
295
311
|
{
|
|
296
312
|
workGroup: 'skill-evaluation',
|
|
@@ -408,6 +424,7 @@ async function buildRouteSignals({
|
|
|
408
424
|
const explanationAsk = explainIdx !== -1 && (firstVerbIdx === -1 || explainIdx < firstVerbIdx);
|
|
409
425
|
const changeRequested = !consultLead && firstVerbIdx !== -1 && (explainIdx === -1 || firstVerbIdx < explainIdx);
|
|
410
426
|
const checkFirst = checkIdx !== -1 && (defectIdx === -1 || checkIdx < defectIdx);
|
|
427
|
+
const buildLeadsCheck = changeRequested && checkIdx !== -1 && firstVerbIdx !== -1 && firstVerbIdx < checkIdx;
|
|
411
428
|
// C85-019: a QUESTION about a defect is not a defect order. The bug-fix floor
|
|
412
429
|
// demands repro+root-cause receipts — demanding them on a "did it get fixed
|
|
413
430
|
// yet?" turn forces edits nobody asked for (user-reported lock). Question
|
|
@@ -439,12 +456,14 @@ async function buildRouteSignals({
|
|
|
439
456
|
changeRequested,
|
|
440
457
|
questionAsk,
|
|
441
458
|
checkFirst,
|
|
459
|
+
buildLeadsCheck,
|
|
442
460
|
changeVerbOrCreate: changeIdx !== -1 || createIdx !== -1,
|
|
443
461
|
referenceTarget,
|
|
444
462
|
cloneIntent,
|
|
445
463
|
sqlIntent,
|
|
446
464
|
sqlSchemaChange: sqlIntent && SQL_SCHEMA_RE.test(signalText),
|
|
447
465
|
reuseFirst,
|
|
466
|
+
delegationOrdered: hasDelegationOrder(signalText) && changeRequested,
|
|
448
467
|
contractSignal: CONTRACT_SIGNAL_RE.test(signalText) || /\boauth\b/i.test(signalText),
|
|
449
468
|
multiSurface: MULTI_SURFACE_RE.test(signalText),
|
|
450
469
|
escalated: Array.isArray(escalationTriggers) && escalationTriggers.length > 0,
|
|
@@ -249,36 +249,39 @@ export function buildExecutionContract(executionMode = null) {
|
|
|
249
249
|
// base row; resolveModelTier adds the (riskFloor, hostCapabilities) columns.
|
|
250
250
|
// Deterministic — no registry, no leases, no outbound capability calls.
|
|
251
251
|
// Literal mirror of src/core/executionContracts.js resolveModelTier.
|
|
252
|
-
|
|
252
|
+
// Tier, effort and risk are independent decisions. Effort is Medium for every tier (Low for chores);
|
|
253
|
+
// High needs complexity evidence (`complexityEvidence` below), never risk or tier alone.
|
|
253
254
|
const MODEL_TIER_EFFORT = {
|
|
254
255
|
lite: 'low',
|
|
255
256
|
code: 'medium',
|
|
256
|
-
smart: '
|
|
257
|
+
smart: 'medium',
|
|
257
258
|
};
|
|
258
259
|
|
|
259
260
|
/**
|
|
260
|
-
* Resolves {tier, effort, advisoryOnly} for a route. A 'high-risk' floor
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
261
|
+
* Resolves {tier, effort, advisoryOnly} for a route. A 'high-risk' floor lifts a Tier 1 chore to the
|
|
262
|
+
* Tier 2 executor (lite→code) and goes no further: smart is the orchestrator / acceptance tier and is
|
|
263
|
+
* reached only through its own contract (review-release) or failure evidence, never through risk, so
|
|
264
|
+
* risk cannot turn the orchestrator into the implementation worker. Risk raises verification rigor
|
|
265
|
+
* (the route's riskFloor / rigor), not the tier ceiling or the effort. Effort is 'high' only with
|
|
266
|
+
* `complexityEvidence`. Unknown mode → {tier:null, effort:null, advisoryOnly:true}. Missing/unknown
|
|
267
|
+
* riskFloor → 'none'. Missing hostCapabilities → advisoryOnly:true (the route text is all the host
|
|
268
|
+
* gets when it cannot bind model and effort).
|
|
265
269
|
*/
|
|
266
270
|
export function resolveModelTier({
|
|
267
271
|
executionMode = null,
|
|
268
272
|
riskFloor = null,
|
|
269
273
|
hostCapabilities = null,
|
|
274
|
+
complexityEvidence = false,
|
|
270
275
|
} = {}) {
|
|
271
276
|
const baseTier = buildExecutionContract(executionMode)?.modelTier ?? null;
|
|
272
277
|
if (!baseTier) {
|
|
273
278
|
return { tier: null, effort: null, advisoryOnly: true };
|
|
274
279
|
}
|
|
275
280
|
const highRisk = riskFloor?.floor === 'high-risk';
|
|
276
|
-
const tier = highRisk
|
|
277
|
-
? MODEL_TIER_ORDER[Math.min(MODEL_TIER_ORDER.indexOf(baseTier) + 1, MODEL_TIER_ORDER.length - 1)]
|
|
278
|
-
: baseTier;
|
|
281
|
+
const tier = highRisk && baseTier === 'lite' ? 'code' : baseTier;
|
|
279
282
|
return {
|
|
280
283
|
tier,
|
|
281
|
-
effort:
|
|
284
|
+
effort: complexityEvidence === true ? 'high' : MODEL_TIER_EFFORT[tier],
|
|
282
285
|
advisoryOnly: !(hostCapabilities?.canBindModel === true && hostCapabilities?.canBindEffort === true),
|
|
283
286
|
};
|
|
284
287
|
}
|
|
@@ -1093,19 +1096,72 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
1093
1096
|
// Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
1094
1097
|
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
1095
1098
|
const NON_ORDER_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|restart|sua|viet|chinh|them|tao|xoa';
|
|
1099
|
+
// Declared verification purpose ("Mục tiêu: xác nhận ...", "Goal: verify ..."). Under that declaration a
|
|
1100
|
+
// conditional repair/release branch ("Nếu phát hiện lỗi ... sửa ...", "Neu co source fix ... publish") is a
|
|
1101
|
+
// branch the check MAY take, not an unconditional order, and disposable test-project setup ("Tạo project thử
|
|
1102
|
+
// nghiệm độc lập") is scaffolding of the check. Unconditional orders in the same request keep blocking.
|
|
1103
|
+
const VERIFICATION_GOAL = /(?:^|\n)[ \t*#_-]*(?:muc tieu|goal|purpose|objective)\s*:[^\n]*\b(?:xac nhan|verify|confirm|kiem tra|validate|double-?check|re-?run)\b/;
|
|
1104
|
+
const CONDITIONAL_BRANCH = /(?:^|\n|\.\s+)[ \t]*(?:\d+[.)]\s*)?(?:neu\s+(?:phat hien\s+(?:false-block|loi|bug|defect)|co\s+(?:source fix|sua doi))|if\s+(?:(?:a\s+)?(?:bug|defect|failure)\s+is\s+found|(?:you\s+)?find\s+(?:a\s+)?(?:bug|defect)))\b[^\n]*?(?=\.\s+|$|\n)/g;
|
|
1105
|
+
const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
|
|
1106
|
+
const DISPOSABLE_SETUP = /\b(?:tao|create)\s+(?:mot\s+)?(?:project|du an)\s+(?:thu nghiem|test|disposable|doc lap)\b(?:\s+(?:doc lap|independent))?/g;
|
|
1096
1107
|
function neutralizeNonOrders(text) {
|
|
1097
|
-
|
|
1108
|
+
let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
|
|
1109
|
+
if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
|
|
1110
|
+
return source
|
|
1098
1111
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1099
1112
|
.replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1100
1113
|
.replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
|
|
1101
|
-
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
1114
|
+
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
1102
1115
|
.replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
|
|
1103
|
-
.replace(/\bchinh\s+xac\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b/g, ' ')
|
|
1116
|
+
.replace(/\bchinh\s+(?:xac|sach)\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b|\b(?:diem|cho|muc|phan)\s+(?:phai|can|nen)\s+(?:sua|fix)\b/g, ' ')
|
|
1104
1117
|
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1105
1118
|
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1106
1119
|
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
1107
1120
|
}
|
|
1108
1121
|
|
|
1122
|
+
// Explicit scope declaration (intent ownership): when the request itself declares it is read-only
|
|
1123
|
+
// ("READ-ONLY audit", "chi doc", "make no source changes") and carries no explicit order to change
|
|
1124
|
+
// anything, verbs and operation names inside it are descriptions (a "git stash" that already ran, "a
|
|
1125
|
+
// worklog edit", "confirmed fixes"), not orders - so they must not demand write or operation proof.
|
|
1126
|
+
// An explicit order (imperative-initial or modal request) keeps the request MIXED/IMPLEMENTATION:
|
|
1127
|
+
// the declaration never overrides a real instruction, and fenced/quoted/blockquoted text is never one.
|
|
1128
|
+
// A bare verb with no object is the tail of a negated list ("no commit, release, publish"), not an order.
|
|
1129
|
+
const READ_ONLY_DECLARATION = /\bread[\s-]*only\s+(?:audit|investigation|review|analysis|inspection|task|request|session|pass|check|diagnos\w+|verification|forensics?)\b|(?:^|\n)[\s*#>_-]*read[\s-]*only\s*[.:!—-]|\b(?:audit|investigation|review|analysis)[\s-]*only\b|[(,]\s*read[\s-]*only\s*[).]|\bchi\s+(?:doc|audit|kiem tra|phan tich|dieu tra)\b|\b(?:make|with)\s+no\s+(?:source\s+|code\s+|file\s+)?(?:changes|edits|modifications)\b/;
|
|
1130
|
+
const ORDER_VERBS = 'implement|fix|apply|refactor|modify|edit|write|update|add|create|build|install|deploy|redeploy|restart|push|commit|rename|remove|delete|replace|change|sua|viet|chinh|them|tao|xoa|enable|disable|set|toggle|configure|migrate|upgrade|publish|release|trien khai|khoi dong lai|(?:run|execute)\\s+(?:git\\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)|(?:npm|yarn)\\s+publish)|git\\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)';
|
|
1131
|
+
const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
|
|
1132
|
+
const MODAL_ORDER = new RegExp(`\\b(?:please|hay|vui long|need to|needs to|must|should|want(?:\\s+you)? to|toi muon|can phai|yeu cau)\\s+(?:[\\w-]+\\s+){0,2}(?:${ORDER_VERBS})\\b`);
|
|
1133
|
+
function hasExplicitOrder(clauses) {
|
|
1134
|
+
return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
|
|
1135
|
+
}
|
|
1136
|
+
function declaredReadOnly(promptText, releasePolicy) {
|
|
1137
|
+
if (releasePolicy) return false;
|
|
1138
|
+
const plain = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
|
|
1139
|
+
.replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
|
|
1140
|
+
if (!READ_ONLY_DECLARATION.test(plain)) return false;
|
|
1141
|
+
const text = neutralizeNonOrders(plain);
|
|
1142
|
+
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
|
|
1143
|
+
.map((clause) => clause.split(/\bwithout\b/)[0])
|
|
1144
|
+
.filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause));
|
|
1145
|
+
return /\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|investigation|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text)
|
|
1146
|
+
&& !hasExplicitOrder(clauses);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
|
|
1150
|
+
// confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
|
|
1151
|
+
// "template/install", "source change") as mutation orders. Order is structural: a request whose
|
|
1152
|
+
// command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
|
|
1153
|
+
// change-leading clause, orders no change - it owes verification evidence, not a write.
|
|
1154
|
+
const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
|
|
1155
|
+
const CHANGE_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:make|rebuild|redo|rewrite|restore|revert|correct|clean|patch|resolve|ensure|run(?!(?=[^.]*\b(?:tests?|suite|gate|checks?|lint|typecheck|pilot|smoke|e2e|verif\w*|vitest|jest|audit)\b)))\b/;
|
|
1156
|
+
export function isVerificationOnlyRequest(promptText) {
|
|
1157
|
+
const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
|
|
1158
|
+
.replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
|
|
1159
|
+
const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
|
|
1160
|
+
.map((clause) => clause.split(/\bwithout\b/)[0]);
|
|
1161
|
+
return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
|
|
1162
|
+
&& !clauses.some((clause) => CHANGE_LEAD.test(clause));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1109
1165
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1110
1166
|
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
1111
1167
|
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
@@ -1117,6 +1173,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1117
1173
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
1118
1174
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
1119
1175
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
1176
|
+
if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY' };
|
|
1120
1177
|
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1121
1178
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
1122
1179
|
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
@@ -1134,10 +1191,15 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1134
1191
|
// runtime are separate domains with separate proof; a prompt naming both stays a coding
|
|
1135
1192
|
// task rather than being half-verified. A code-change verb anywhere keeps write evidence.
|
|
1136
1193
|
const gitOperation = actionable.some((clause) => /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
|
|
1137
|
-
|
|
1194
|
+
// 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
|
|
1195
|
+
// when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
|
|
1196
|
+
// "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
|
|
1197
|
+
const runtimeOperation = actionable.some((clause) => /(?<!\b(?:process|crash|hard|soft|graceful|after|upon|during|across|following)\s)\b(?:restart|redeploy|deploy|khoi dong lai|trien khai\s+(?:len\s+)?(?:service|server|production|prod|staging|dich vu|may chu|moi truong))\b/.test(clause)
|
|
1138
1198
|
&& !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
|
|
1139
1199
|
const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
|
|
1140
1200
|
const operation = Boolean(nativeDomain);
|
|
1201
|
+
// A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
|
|
1202
|
+
if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
|
|
1141
1203
|
// A config MENTION is not a config change: the same clause needs a mutation verb, and a
|
|
1142
1204
|
// request that also changes source/code is a coding task (config is part of its tests/fixtures).
|
|
1143
1205
|
const config = !sourceOrder && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause)
|
|
@@ -1148,7 +1210,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1148
1210
|
const mutationWord = /\b(?:implement|fix|refactor|modify|edit|write|update|add|create|build|make|install|deploy|remove|delete|rename|replace|change|sua|viet|chinh|them|tao|xoa|doi|cai|cap nhat)\b/.test(text);
|
|
1149
1211
|
const statusReport = !mutationWord && !operation && !config
|
|
1150
1212
|
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text);
|
|
1151
|
-
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|debug|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text) || statusReport)
|
|
1213
|
+
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|debug|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text.replace(/\bread[\s-]*only\b/g, ' ')) || statusReport)
|
|
1152
1214
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
1153
1215
|
const taskKind = operation ? (sourceOrder || codeChangeVerb ? 'MIXED' : 'OPERATIONAL')
|
|
1154
1216
|
: config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
@@ -1170,5 +1232,5 @@ export function applyTaskSemantics(contract, semantic) {
|
|
|
1170
1232
|
completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
|
|
1171
1233
|
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
1172
1234
|
: semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
|
|
1173
|
-
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|
|
1235
|
+
: semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
|
|
1174
1236
|
}
|