@ngockhoale/ukit 3.4.47 → 3.4.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/manifests/documentation.yaml +22 -0
  3. package/manifests/platform.full.yaml +27 -0
  4. package/package.json +4 -1
  5. package/scripts/dev/worktree-preflight.mjs +20 -1
  6. package/src/core/diffPlan.js +38 -12
  7. package/src/core/executionContracts.js +67 -16
  8. package/src/core/metadata.js +4 -0
  9. package/src/core/runInstallPipeline.js +19 -2
  10. package/src/core/runtimeConfig.js +1 -1
  11. package/src/index/playbookRegistry.js +22 -3
  12. package/src/index/taskRouting.js +2 -2
  13. package/template_project/.claude/agents/bug-debugger.md +1 -0
  14. package/template_project/.claude/agents/code-reviewer.md +1 -0
  15. package/template_project/.claude/agents/feature-implementer.md +1 -0
  16. package/template_project/.claude/agents/handoff-planner.md +1 -0
  17. package/template_project/.claude/agents/ukit-small-task-maintainer.md +1 -0
  18. package/template_project/.claude/hooks/completion-gate.sh +1 -1
  19. package/template_project/.claude/hooks/delegation-gate.mjs +343 -0
  20. package/template_project/.claude/hooks/skill-router.sh +147 -6
  21. package/template_project/.claude/settings.json +8 -8
  22. package/template_project/.claude/ukit/index/playbook-registry.mjs +22 -3
  23. package/template_project/.claude/ukit/index/route-resolver.mjs +67 -15
  24. package/template_project/.claude/ukit/index/route-task.mjs +115 -37
  25. package/template_project/.claude/ukit/index/tier-lane.mjs +202 -0
  26. package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +3 -1
  27. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +306 -20
  28. package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +8 -0
  29. package/template_project/.claude/ukit/runtime/loop-state.mjs +3 -1
  30. package/template_project/.claude/ukit/runtime/observability-emit.mjs +38 -0
  31. package/template_project/.claude/ukit/runtime/playbook-execution.mjs +704 -0
  32. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +79 -3
  33. package/template_project/.omp/agents/bug-debugger.md +1 -0
  34. package/template_project/.omp/agents/code-reviewer.md +1 -0
  35. package/template_project/.omp/agents/feature-implementer.md +1 -0
  36. package/template_project/.omp/agents/handoff-planner.md +1 -0
  37. package/template_project/.omp/agents/ukit-small-task-maintainer.md +1 -0
  38. package/template_project/.omp/hooks/pre/ukit-bridge.js +254 -7
  39. package/template_project/ukit/storage/config.json +1 -1
  40. package/scripts/bench/data-foundation.mjs +0 -929
  41. package/scripts/bench/decision-runtime-baseline.mjs +0 -427
  42. package/scripts/bench/decision-runtime-variant.mjs +0 -626
  43. package/scripts/bench/goldTasks.json +0 -38
  44. package/scripts/bench/memory-ablation.mjs +0 -495
  45. package/scripts/bench/memory-baseline.mjs +0 -596
  46. package/scripts/bench/memory-bench.mjs +0 -661
  47. package/scripts/bench/memory-canary.mjs +0 -331
  48. package/scripts/bench/memory-corpus.mjs +0 -354
  49. package/scripts/bench/memory-gate.mjs +0 -389
  50. package/scripts/bench/memory-metrics.mjs +0 -179
  51. package/scripts/bench/outline-savings.mjs +0 -107
  52. package/scripts/bench/parallel-agents.mjs +0 -224
  53. package/scripts/bench/recorder-overhead.mjs +0 -204
  54. package/scripts/bench/runGold.mjs +0 -238
  55. package/scripts/bench/sqlite-spike.mjs +0 -451
  56. package/scripts/bench/subagent-orchestrator-corpus.mjs +0 -275
  57. package/scripts/bench/subagent-orchestrator-eval.mjs +0 -565
  58. package/scripts/bench/v3-ceremony.mjs +0 -220
  59. package/scripts/perf/audit-perf.mjs +0 -1195
  60. package/scripts/perf/diff-perf-findings.mjs +0 -136
  61. package/scripts/perf/perf-findings.json +0 -474
  62. 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)) {
@@ -1828,12 +1888,34 @@ function neutralizeNonOrders(text) {
1828
1888
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
1829
1889
  .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1830
1890
  .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, ' ')
1891
+ .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
1892
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
1833
1893
  .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
1834
1894
  .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
1835
1895
  }
1836
1896
 
1897
+ 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)';
1898
+ const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
1899
+ 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`);
1900
+ function hasExplicitOrder(clauses) {
1901
+ return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
1902
+ }
1903
+ // Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
1904
+ // confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
1905
+ // "template/install", "source change") as mutation orders. Order is structural: a request whose
1906
+ // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
1907
+ // change-leading clause, orders no change - it owes verification evidence, not a write.
1908
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|validate|double-?check)\b/;
1909
+ 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/;
1910
+ function isVerificationOnlyRequest(promptText) {
1911
+ const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
1912
+ .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1913
+ const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
1914
+ .map((clause) => clause.split(/\bwithout\b/)[0]);
1915
+ return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
1916
+ && !clauses.some((clause) => CHANGE_LEAD.test(clause));
1917
+ }
1918
+
1837
1919
  function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
1838
1920
  // A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
1839
1921
  const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
@@ -1862,10 +1944,15 @@ function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
1862
1944
  // runtime are separate domains with separate proof; a prompt naming both stays a coding
1863
1945
  // task rather than being half-verified. A code-change verb anywhere keeps write evidence.
1864
1946
  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
- const runtimeOperation = actionable.some((clause) => /\b(?:restart|redeploy|deploy|khoi dong lai|trien khai)\b/.test(clause)
1947
+ // 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
1948
+ // when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
1949
+ // "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
1950
+ 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
1951
  && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
1867
1952
  const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
1868
1953
  const operation = Boolean(nativeDomain);
1954
+ // A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
1955
+ if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
1869
1956
  // A config MENTION is not a config change: the same clause needs a mutation verb, and a
1870
1957
  // request that also changes source/code is a coding task (config is part of its tests/fixtures).
1871
1958
  const config = !sourceOrder && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause)
@@ -1895,7 +1982,7 @@ function applyTaskSemantics(contract, semantic) {
1895
1982
  completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
1896
1983
  : semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
1897
1984
  : semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
1898
- : semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
1985
+ : semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
1899
1986
  }
1900
1987
 
1901
1988
  function buildRouteSummary({
@@ -2002,7 +2089,7 @@ function applyTaskSemantics(contract, semantic) {
2002
2089
  const buildSignal = /\b(build|create|add|implement|feature)\b/.test(raw);
2003
2090
  const debugSignal = intentMode === 'debug-specific' || /\b(root cause|debug|triage|flaky|investigate|why)\b/.test(raw);
2004
2091
  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);
2092
+ 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
2093
  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
2094
  const boundedEditSignal = /\b(one[- ]line|small|tiny|single|local|guard|log line|loading message|label)\b/.test(raw);
2008
2095
  const implementSignal = /\b(implement|apply|update|modify|add|create|ship|deliver|fix)\b/.test(raw)
@@ -2294,6 +2381,7 @@ function applyTaskSemantics(contract, semantic) {
2294
2381
  .replace(/\u0111/g, 'd');
2295
2382
  const trimmed = folded.trim();
2296
2383
  if (!trimmed) return false;
2384
+ if (!String(commandText ?? '').trim() && classifyTaskSemantics({ promptText }).verificationOnly === true) return false;
2297
2385
  // C85-019c: clause strips must stop BEFORE a conjunction-joined imperative
2298
2386
  // — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
2299
2387
  // order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
@@ -3218,6 +3306,9 @@ function applyTaskSemantics(contract, semantic) {
3218
3306
  // no-map routes keep the pre-BL-019 compact shape (identical to
3219
3307
  // route-task.mjs compactRouteSummary).
3220
3308
  ...(routeSummary.measuredTier != null ? { measuredTier: routeSummary.measuredTier } : {}),
3309
+ // The tier lane survives compaction: the Stop-side playbook acceptance reads
3310
+ // tierLane.delegationRequired off the persisted state (identical to route-task.mjs).
3311
+ ...(routeSummary.tierLane ? { tierLane: routeSummary.tierLane } : {}),
3221
3312
  ...(routeSummary.routeVersion !== undefined
3222
3313
  ? {
3223
3314
  routeVersion: routeSummary.routeVersion,
@@ -3296,6 +3387,22 @@ function applyTaskSemantics(contract, semantic) {
3296
3387
  // BL-013: bounded history counters on the audit row — identical to the
3297
3388
  // helper's buildRouteAuditEntry emission.
3298
3389
  historySignals: routeSummary.historySignals ?? null,
3390
+ // Orchestration telemetry — identical to the helper's buildOrchestrationAudit: requested
3391
+ // lane/effort next to host evidence; effective model is never inferred (UNKNOWN).
3392
+ orchestration: routeSummary?.tierLane && typeof routeSummary.tierLane === 'object'
3393
+ ? {
3394
+ mainTier: routeSummary.tierLane.mainTier ?? null,
3395
+ mainTierSource: routeSummary.tierLane.mainTierSource ?? null,
3396
+ requestedTier: routeSummary.tierLane.tier ?? null,
3397
+ lane: routeSummary.tierLane.instruction
3398
+ ? (routeSummary.tierLane.mainTier === 'smart' ? 'dispatch-worker' : 'handoff')
3399
+ : 'inline',
3400
+ requestedEffort: routeSummary.tierLane.effort ?? null,
3401
+ effortBinding: routeSummary.tierLane.effortBinding ?? null,
3402
+ effectiveEffort: process.env.CLAUDE_EFFORT || 'UNKNOWN',
3403
+ effectiveModel: 'UNKNOWN',
3404
+ }
3405
+ : null,
3299
3406
  };
3300
3407
  }
3301
3408
 
@@ -3849,6 +3956,7 @@ function applyTaskSemantics(contract, semantic) {
3849
3956
  // (a prompt that lists "git push / npm publish" became a new MIXED git-integrity operation that a
3850
3957
  // verification-only turn can never satisfy). Only transcript metadata set by the harness or the
3851
3958
  // fixed compaction preamble counts - never the prompt's claims about itself.
3959
+ let coordinationTurn = false;
3852
3960
  const systemOriginTurn = (() => {
3853
3961
  const promptTrim = extractPrompt(payload).trim();
3854
3962
  if (!promptTrim) return false;
@@ -3865,13 +3973,29 @@ function applyTaskSemantics(contract, semantic) {
3865
3973
  const lines = buf.toString('utf8').split('\n');
3866
3974
  for (let i = lines.length - 1; i >= 0; i -= 1) {
3867
3975
  const line = lines[i];
3868
- if (!line || !line.includes('"type":"user"')) continue;
3976
+ if (!line) continue;
3869
3977
  let entry; try { entry = JSON.parse(line); } catch { continue; }
3978
+ // Cross-session delivery is queued, not a user order. Require the host's
3979
+ // consumed delivery record, exact content and this session's owner. An
3980
+ // enqueue, a foreign delivery or a human-pasted envelope is not proof.
3981
+ if (entry.type === 'queue-operation' && entry.operation === 'remove'
3982
+ && entry.reason === 'absorbed_mid_turn' && entry.deliveryId
3983
+ && sessionId && entry.sessionId === sessionId
3984
+ && typeof entry.content === 'string' && entry.content.trim() === promptTrim
3985
+ && /^<cross-session-message\s/.test(promptTrim)) {
3986
+ coordinationTurn = true;
3987
+ return false;
3988
+ }
3870
3989
  if (entry.type !== 'user' || entry.isSidechain === true) continue;
3871
3990
  const content = entry.message?.content;
3872
3991
  const text = (typeof content === 'string' ? content
3873
3992
  : Array.isArray(content) ? content.map((b) => (b?.type === 'tool_result' ? '\u0000' : b?.text || '')).join('\n') : '').trim();
3874
3993
  if (!text || text.includes('\u0000') || /^Stop hook feedback/i.test(text)) continue;
3994
+ if (entry.isMeta === true && entry.promptSource === 'system' && entry.turnOrigin === 'system'
3995
+ && text === promptTrim && /^\[Cross-session (?:idle|delivery) notice\]/.test(text)) {
3996
+ coordinationTurn = true;
3997
+ return false;
3998
+ }
3875
3999
  return entry.isMeta === true && (entry.turnOrigin === 'scheduled' || entry.promptSource === 'system')
3876
4000
  && text === promptTrim;
3877
4001
  }
@@ -3879,6 +4003,9 @@ function applyTaskSemantics(contract, semantic) {
3879
4003
  } catch {}
3880
4004
  return false;
3881
4005
  })();
4006
+ // A notification has no task boundary or routing authority. Leave even an
4007
+ // incomplete contract untouched; peer prose cannot clear or create its debt.
4008
+ if (coordinationTurn) process.exit(0);
3882
4009
  const continuingTask = Boolean(previous.taskInstanceId && (payload.tool_name || continuationPrompt || systemOriginTurn));
3883
4010
  const taskInstanceId = continuingTask
3884
4011
  ? previous.taskInstanceId
@@ -4168,6 +4295,7 @@ function applyTaskSemantics(contract, semantic) {
4168
4295
  activeSkills: selected,
4169
4296
  historySignals: routeHistorySignals,
4170
4297
  }));
4298
+ inheritTaskPlaybook(provisionalRouteSummary, previous, taskInstanceId);
4171
4299
  const provisionalState = {
4172
4300
  ...(sessionId ? { sessionId } : {}),
4173
4301
  requestKey,
@@ -4237,6 +4365,7 @@ function applyTaskSemantics(contract, semantic) {
4237
4365
  historySignals: routeHistorySignals,
4238
4366
  });
4239
4367
  mergeResolverFields(rescuedRouteSummary, reusedSharedFields, { onlyMissing: true });
4368
+ inheritTaskPlaybook(rescuedRouteSummary, previous, taskInstanceId);
4240
4369
  }
4241
4370
  const reusedState = {
4242
4371
  ...cachedRouteState,
@@ -4278,6 +4407,7 @@ function applyTaskSemantics(contract, semantic) {
4278
4407
  if (reusedState.routeSummary?.delegateHint) {
4279
4408
  process.stdout.write(`[ukit-skill-router] Delegate: ${reusedState.routeSummary.delegateHint}\n`);
4280
4409
  }
4410
+ writeTierLaneLines(reusedState.routeSummary, reusedState.taskInstanceId || taskInstanceId);
4281
4411
  if (
4282
4412
  reusedState.routeSummary?.helperHint
4283
4413
  && !reusedState.routeSummary?.nextActionCommand
@@ -4335,6 +4465,7 @@ function applyTaskSemantics(contract, semantic) {
4335
4465
  historySignals: routeHistorySignals,
4336
4466
  });
4337
4467
  mergeResolverFields(routeSummary, finalSharedFields);
4468
+ inheritTaskPlaybook(routeSummary, previous, taskInstanceId);
4338
4469
  // BL-019 (SPEC FR-001): fold the persisted measured map into the route —
4339
4470
  // identical semantics to route-task.mjs finalizeTaskRoute / taskRouting.js
4340
4471
  // deriveTaskRoute: measuredTier emitted additively; modelTier follows the
@@ -4348,6 +4479,15 @@ function applyTaskSemantics(contract, semantic) {
4348
4479
  } catch {
4349
4480
  // static map stays authoritative
4350
4481
  }
4482
+ try {
4483
+ const tierLane = await loadTierLane();
4484
+ if (tierLane && typeof tierLane.applyTierLaneToRouteSummary === 'function') {
4485
+ tierLane.applyTierLaneToRouteSummary(routeSummary, { rootDir: projectRoot, config: runtimeConfig, subagentSession: payload.ukit_subagent_session === true, sessionId: payload.session_id ?? null });
4486
+ }
4487
+ } catch {
4488
+ // no tier lane: the legacy inline behavior applies
4489
+ }
4490
+ inheritTaskPlaybook(routeSummary, previous, taskInstanceId);
4351
4491
 
4352
4492
 
4353
4493
  const fingerprint = buildRouteStateFingerprint({
@@ -4416,6 +4556,7 @@ function applyTaskSemantics(contract, semantic) {
4416
4556
  if (routeSummary.delegateHint) {
4417
4557
  process.stdout.write(`[ukit-skill-router] Delegate: ${routeSummary.delegateHint}\n`);
4418
4558
  }
4559
+ writeTierLaneLines(routeSummary, taskInstanceId);
4419
4560
  if (
4420
4561
  routeSummary.helperHint
4421
4562
  && !routeSummary.nextActionCommand
@@ -1,9 +1,9 @@
1
1
  {
2
- "model": "sonnet",
2
+ "model": "opus",
3
3
  "fastMode": false,
4
4
  "fastModePerSessionOptIn": false,
5
5
  "alwaysThinkingEnabled": true,
6
- "effortLevel": "high",
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": 73
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": 18
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": 65
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
- discriminator: (s) => s.checkFirst || s.defectIdx === -1,
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
- const MODEL_TIER_ORDER = ['lite', 'code', 'smart'];
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: 'high',
257
+ smart: 'medium',
257
258
  };
258
259
 
259
260
  /**
260
- * Resolves {tier, effort, advisoryOnly} for a route. A 'high-risk' floor escalates
261
- * the tier one band (lite→code→smart, capped at smart) and forces effort 'high'.
262
- * Unknown mode → {tier:null, effort:null, advisoryOnly:true}. Missing/unknown
263
- * riskFloor → 'none'. Missing hostCapabilities → advisoryOnly:true (the route text
264
- * is all the host gets when it cannot bind model and effort).
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: highRisk ? 'high' : MODEL_TIER_EFFORT[tier],
284
+ effort: complexityEvidence === true ? 'high' : MODEL_TIER_EFFORT[tier],
282
285
  advisoryOnly: !(hostCapabilities?.canBindModel === true && hostCapabilities?.canBindEffort === true),
283
286
  };
284
287
  }
@@ -1100,12 +1103,55 @@ function neutralizeNonOrders(text) {
1100
1103
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
1101
1104
  .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1102
1105
  .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, ' ')
1106
+ .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
1107
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
1105
1108
  .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
1106
1109
  .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
1107
1110
  }
1108
1111
 
1112
+ // Explicit scope declaration (intent ownership): when the request itself declares it is read-only
1113
+ // ("READ-ONLY audit", "chi doc", "make no source changes") and carries no explicit order to change
1114
+ // anything, verbs and operation names inside it are descriptions (a "git stash" that already ran, "a
1115
+ // worklog edit", "confirmed fixes"), not orders - so they must not demand write or operation proof.
1116
+ // An explicit order (imperative-initial or modal request) keeps the request MIXED/IMPLEMENTATION:
1117
+ // the declaration never overrides a real instruction, and fenced/quoted/blockquoted text is never one.
1118
+ // A bare verb with no object is the tail of a negated list ("no commit, release, publish"), not an order.
1119
+ 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/;
1120
+ 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)';
1121
+ const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
1122
+ 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`);
1123
+ function hasExplicitOrder(clauses) {
1124
+ return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
1125
+ }
1126
+ function declaredReadOnly(promptText, releasePolicy) {
1127
+ if (releasePolicy) return false;
1128
+ const plain = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
1129
+ .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1130
+ if (!READ_ONLY_DECLARATION.test(plain)) return false;
1131
+ const text = neutralizeNonOrders(plain);
1132
+ const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
1133
+ .map((clause) => clause.split(/\bwithout\b/)[0])
1134
+ .filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause));
1135
+ 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)
1136
+ && !hasExplicitOrder(clauses);
1137
+ }
1138
+
1139
+ // Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
1140
+ // confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
1141
+ // "template/install", "source change") as mutation orders. Order is structural: a request whose
1142
+ // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
1143
+ // change-leading clause, orders no change - it owes verification evidence, not a write.
1144
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|validate|double-?check)\b/;
1145
+ 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/;
1146
+ export function isVerificationOnlyRequest(promptText) {
1147
+ const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
1148
+ .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1149
+ const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
1150
+ .map((clause) => clause.split(/\bwithout\b/)[0]);
1151
+ return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
1152
+ && !clauses.some((clause) => CHANGE_LEAD.test(clause));
1153
+ }
1154
+
1109
1155
  export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
1110
1156
  // A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
1111
1157
  const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
@@ -1117,6 +1163,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
1117
1163
  const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
1118
1164
  const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
1119
1165
  && !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
1166
+ if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY' };
1120
1167
  const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
1121
1168
  && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
1122
1169
  const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
@@ -1134,10 +1181,15 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
1134
1181
  // runtime are separate domains with separate proof; a prompt naming both stays a coding
1135
1182
  // task rather than being half-verified. A code-change verb anywhere keeps write evidence.
1136
1183
  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
- const runtimeOperation = actionable.some((clause) => /\b(?:restart|redeploy|deploy|khoi dong lai|trien khai)\b/.test(clause)
1184
+ // 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
1185
+ // when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
1186
+ // "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
1187
+ 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
1188
  && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
1139
1189
  const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
1140
1190
  const operation = Boolean(nativeDomain);
1191
+ // A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
1192
+ if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
1141
1193
  // A config MENTION is not a config change: the same clause needs a mutation verb, and a
1142
1194
  // request that also changes source/code is a coding task (config is part of its tests/fixtures).
1143
1195
  const config = !sourceOrder && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh)\b/.test(clause)
@@ -1148,7 +1200,7 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
1148
1200
  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
1201
  const statusReport = !mutationWord && !operation && !config
1150
1202
  && /\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)
1203
+ 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
1204
  && !sourceOrder && !codeChangeVerb && !operation && !config;
1153
1205
  const taskKind = operation ? (sourceOrder || codeChangeVerb ? 'MIXED' : 'OPERATIONAL')
1154
1206
  : config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
@@ -1170,5 +1222,5 @@ export function applyTaskSemantics(contract, semantic) {
1170
1222
  completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
1171
1223
  : semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
1172
1224
  : semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
1173
- : semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
1225
+ : semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
1174
1226
  }