@ngockhoale/ukit 3.4.46 → 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.
- package/CHANGELOG.md +13 -5
- package/manifests/documentation.yaml +22 -0
- package/manifests/platform.full.yaml +27 -0
- package/package.json +4 -1
- package/scripts/dev/capture-omp-stall.mjs +246 -0
- package/scripts/dev/worktree-preflight.mjs +20 -1
- package/scripts/docs/render-instructions.mjs +29 -5
- package/src/core/agentRuntime/supervisor.js +15 -1
- package/src/core/diffPlan.js +38 -12
- package/src/core/executionContracts.js +67 -16
- package/src/core/metadata.js +4 -0
- package/src/core/runInstallPipeline.js +19 -2
- package/src/core/runtimeConfig.js +1 -1
- package/src/diagnostics/ompStallSnapshot.js +199 -0
- 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 +147 -6
- 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 +67 -15
- 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 +306 -20
- 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 +80 -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
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
writeRecentCacheEntry,
|
|
18
18
|
} from './cache-utils.mjs';
|
|
19
19
|
import { ROUTE_CATALOG } from './route-catalog.mjs';
|
|
20
|
+
import { buildTierLane, classifyModelTier, resolveMainTier } from './tier-lane.mjs';
|
|
21
|
+
// Public surface unchanged for tests and callers that import the lane helpers from the route CLI.
|
|
22
|
+
export { buildTierLane, classifyModelTier, resolveMainTier };
|
|
20
23
|
import { detectUnicGateway } from './unic-gateway.mjs';
|
|
21
24
|
import { withAsyncLock } from '../runtime/async-lock.mjs';
|
|
22
25
|
import { appendDecisionReceipt, recordLedgerEvent } from '../runtime/execution-ledger.mjs';
|
|
@@ -1068,6 +1071,7 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
|
|
|
1068
1071
|
recentOutputSnapshot,
|
|
1069
1072
|
previousRouteSummary: normalizedInput.previousRouteSummary ?? null,
|
|
1070
1073
|
escalationConfig,
|
|
1074
|
+
sessionId: normalizedInput.sessionId ?? null,
|
|
1071
1075
|
// BL-018: caller-injected seams — the in-process entry accepts a prior
|
|
1072
1076
|
// emission (exhaustion check) and a stubbed decision/registrar for tests.
|
|
1073
1077
|
escalationDecision: normalizedInput.escalationDecision ?? null,
|
|
@@ -1109,6 +1113,40 @@ export async function routeTask(input = {}, { signal = null, deadlineMs = null,
|
|
|
1109
1113
|
|
|
1110
1114
|
export { DEFAULT_ROUTE_DEADLINE_MS, DEFAULT_ROUTE_IO_BUDGET };
|
|
1111
1115
|
|
|
1116
|
+
const TASK_INSTANCE_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$/;
|
|
1117
|
+
// The prompt hook owns turn boundaries: every real user turn writes a fresh state (new id) with one of
|
|
1118
|
+
// these sources before the model can run this CLI. A CLI-only environment (Codex: "invoked by
|
|
1119
|
+
// instruction") has no such boundary, so there an identical prompt is a REPEATED request and mints a new id.
|
|
1120
|
+
const HOOK_STATE_SOURCES = ['skill-router', 'skill-router-provisional'];
|
|
1121
|
+
|
|
1122
|
+
// @decision-point task.instance-id-ownership — deterministic reuse-vs-mint ownership decision (the
|
|
1123
|
+
// task boundary every evidence receipt is filtered by); regression: tests/index/taskIdentityContinuity.test.js.
|
|
1124
|
+
/**
|
|
1125
|
+
* Same task => same id; genuinely new task => fresh id. Returns { id, origin }.
|
|
1126
|
+
*
|
|
1127
|
+
* An id is reused only from a previous state (already session-filtered by main()) that provably belongs
|
|
1128
|
+
* to the SAME hook-owned turn: it was written by the hook, or by this CLI refreshing that hook turn
|
|
1129
|
+
* (taskIdOrigin 'hook'), AND the prompt is identical (or the refresh is prompt-less: tool command / target
|
|
1130
|
+
* only, routed from the stored explicit prompt). The origin ends at a continue phrase, a different prompt,
|
|
1131
|
+
* or any state the hook did not own, so a repeated request still mints a new id and unrelated evidence
|
|
1132
|
+
* stays rejected. An explicit --task-instance-id is honored when well-formed.
|
|
1133
|
+
*/
|
|
1134
|
+
export function reuseOrMintTaskInstanceId({ previousState = {}, promptText = '', commandText = '', targetFile = null, explicitId = null, continuingTask = false } = {}) {
|
|
1135
|
+
const explicit = typeof explicitId === 'string' ? explicitId.trim() : '';
|
|
1136
|
+
if (explicit && TASK_INSTANCE_ID_SHAPE.test(explicit)) return { id: explicit, origin: null };
|
|
1137
|
+
const prevId = previousState?.taskInstanceId;
|
|
1138
|
+
if (prevId) {
|
|
1139
|
+
const hookTurn = HOOK_STATE_SOURCES.includes(previousState.source)
|
|
1140
|
+
|| (previousState.source === 'task-route' && previousState.taskIdOrigin === 'hook');
|
|
1141
|
+
const stored = previousState.routingContext?.lastExplicitUserPromptText;
|
|
1142
|
+
const samePrompt = Boolean(stored) && stored === promptText;
|
|
1143
|
+
const promptlessRefresh = Boolean(stored) && !promptText && Boolean(commandText || targetFile);
|
|
1144
|
+
if (hookTurn && (samePrompt || promptlessRefresh)) return { id: prevId, origin: 'hook' };
|
|
1145
|
+
if (continuingTask) return { id: prevId, origin: null };
|
|
1146
|
+
}
|
|
1147
|
+
return { id: `task-${crypto.randomBytes(16).toString('hex')}`, origin: null };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1112
1150
|
async function main() {
|
|
1113
1151
|
const args = process.argv.slice(2);
|
|
1114
1152
|
const rootDir = getRootDir(args);
|
|
@@ -1169,11 +1207,14 @@ async function main() {
|
|
|
1169
1207
|
const continuingTask = Boolean(previousState.taskInstanceId && lastPrompt
|
|
1170
1208
|
&& /^(?:continue|resume|keep going|go on|proceed|tiep tuc|lam tiep)(?:\s+(?:with\s+)?(?:the\s+|this\s+)?(?:previous\s+|last\s+|current\s+|same\s+)?(?:implementation\s+)?(?:task|work))?(?:\s+(?:thoi|di|đi|nhe|nao|nua|a|ha|please|pls|now|then))*\s*[.!]?$/i.test(
|
|
1171
1209
|
promptText.normalize('NFD').replace(/[̀-ͯ]/g, '').trim()));
|
|
1172
|
-
const taskInstanceId =
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1210
|
+
const { id: taskInstanceId, origin: taskIdOrigin } = reuseOrMintTaskInstanceId({
|
|
1211
|
+
previousState,
|
|
1212
|
+
promptText,
|
|
1213
|
+
commandText,
|
|
1214
|
+
targetFile,
|
|
1215
|
+
explicitId: readFlagValue(args, '--task-instance-id'),
|
|
1216
|
+
continuingTask,
|
|
1217
|
+
});
|
|
1177
1218
|
if (continuingTask) promptText = lastPrompt;
|
|
1178
1219
|
const preparedRoute = await prepareTaskRoute({
|
|
1179
1220
|
rootDir,
|
|
@@ -1269,7 +1310,7 @@ async function main() {
|
|
|
1269
1310
|
historySignals,
|
|
1270
1311
|
};
|
|
1271
1312
|
}
|
|
1272
|
-
const sharedState = { ...reusableState, taskInstanceId, source: 'task-route', ts: Date.now() };
|
|
1313
|
+
const sharedState = { ...reusableState, taskInstanceId, taskIdOrigin, source: 'task-route', ts: Date.now() };
|
|
1273
1314
|
if (canPersistRouteState) {
|
|
1274
1315
|
try { await persistSharedRouteState(sharedStatePath, sharedState); }
|
|
1275
1316
|
catch (error) { console.error(`route-task: state persist degraded — ${error?.message ?? error}`); }
|
|
@@ -1324,6 +1365,8 @@ async function main() {
|
|
|
1324
1365
|
applyEscalationToRouteSummary(rescuedRouteSummary, {
|
|
1325
1366
|
config: escalationConfig,
|
|
1326
1367
|
targetFile: cachedRouteState.routingContext?.targetFile ?? null,
|
|
1368
|
+
rootDir: rootDir,
|
|
1369
|
+
sessionId,
|
|
1327
1370
|
});
|
|
1328
1371
|
// BL-018: re-run the fix-loop transition on the rescued summary — a
|
|
1329
1372
|
// refreshed fixLoopCount may trip, escalate, or exhaust (BLOCKED); the
|
|
@@ -1357,6 +1400,7 @@ async function main() {
|
|
|
1357
1400
|
const sharedState = {
|
|
1358
1401
|
...cachedRouteState,
|
|
1359
1402
|
taskInstanceId,
|
|
1403
|
+
taskIdOrigin,
|
|
1360
1404
|
...(sessionId ? { sessionId } : {}),
|
|
1361
1405
|
source: 'task-route',
|
|
1362
1406
|
ts: Date.now(),
|
|
@@ -1420,6 +1464,7 @@ async function main() {
|
|
|
1420
1464
|
sessionId,
|
|
1421
1465
|
taskInstanceId,
|
|
1422
1466
|
});
|
|
1467
|
+
if (taskIdOrigin) sharedState.taskIdOrigin = taskIdOrigin;
|
|
1423
1468
|
if (canPersistRouteState) {
|
|
1424
1469
|
// Advisory persistence — an EACCES/ENOSPC under the lock must never lose
|
|
1425
1470
|
// the printed route; degrade to stderr and print anyway.
|
|
@@ -2504,6 +2549,8 @@ async function finalizeTaskRoute({
|
|
|
2504
2549
|
applyEscalationToRouteSummary(routeSummary, {
|
|
2505
2550
|
config: escalationConfig,
|
|
2506
2551
|
targetFile: routingContext.targetFile ?? null,
|
|
2552
|
+
rootDir: absoluteRoot,
|
|
2553
|
+
sessionId,
|
|
2507
2554
|
});
|
|
2508
2555
|
// BL-018 (SPEC FR-006): fix-loop escalation. historySignals.fixLoopCount is
|
|
2509
2556
|
// fingerprint-joined (same routeFingerprint/requestKey, never session-wide);
|
|
@@ -2694,6 +2741,9 @@ function printRouteState(state) {
|
|
|
2694
2741
|
}
|
|
2695
2742
|
if (state.routeSummary?.tierLane?.instruction) {
|
|
2696
2743
|
console.log(`tierLane: ${state.routeSummary.tierLane.instruction}`);
|
|
2744
|
+
if (state.routeSummary.tierLane.delegationRequired === true && state.taskInstanceId) {
|
|
2745
|
+
console.log(`handback: start each worker prompt with "[ukit-task ${state.taskInstanceId}]" so its result binds to this task`);
|
|
2746
|
+
}
|
|
2697
2747
|
}
|
|
2698
2748
|
if (state.routeSummary?.escalatedTier) {
|
|
2699
2749
|
console.log(`escalatedTier: ${state.routeSummary.escalatedTier}`);
|
|
@@ -3182,7 +3232,7 @@ function deriveIntentMode({ promptText = '', commandText = '', targetFile = null
|
|
|
3182
3232
|
}
|
|
3183
3233
|
|
|
3184
3234
|
if (concreteTask) {
|
|
3185
|
-
if (/\b(bug|debug|error|crash|broken|failing|stack trace|triage|fix|timeout)\b/.test(lower)) {
|
|
3235
|
+
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, ' '))) {
|
|
3186
3236
|
return 'debug-specific';
|
|
3187
3237
|
}
|
|
3188
3238
|
if (/\b(review|audit|diff|pr feedback|code review|kiem tra|soat)\b/.test(raw)) {
|
|
@@ -4186,7 +4236,7 @@ function deriveExecutionScores({
|
|
|
4186
4236
|
const buildSignal = /\b(build|create|add|implement|feature)\b/.test(raw);
|
|
4187
4237
|
const debugSignal = intentMode === 'debug-specific' || /\b(root cause|debug|triage|flaky|investigate|why)\b/.test(raw);
|
|
4188
4238
|
const reviewSignal = intentMode === 'review-specific' || /\b(review|audit|verify|release readiness)\b/.test(raw);
|
|
4189
|
-
const failureSignal = /\b(failing|failed|broken|error|crash|timeout|undefined|exception|eacces|trace)\b/.test(raw);
|
|
4239
|
+
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, ' '));
|
|
4190
4240
|
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);
|
|
4191
4241
|
const boundedEditSignal = /\b(one[- ]line|small|tiny|single|local|guard|log line|loading message|label)\b/.test(raw);
|
|
4192
4242
|
const implementSignal = /\b(implement|apply|update|modify|add|create|ship|deliver|fix)\b/.test(raw)
|
|
@@ -5026,10 +5076,16 @@ function resolveEscalationSettings(config = null) {
|
|
|
5026
5076
|
* Escalates exactly one rung above `modelTier` on `tierOrder`, capped at `cap`. Returns null when
|
|
5027
5077
|
* escalation is disabled, `repeatCount` hasn't reached `debugLoopThreshold`, or `modelTier` isn't
|
|
5028
5078
|
* on the cost ladder (e.g. no contract selected). Never returns 'vision'.
|
|
5029
|
-
* FR-005:
|
|
5030
|
-
*
|
|
5031
|
-
*
|
|
5032
|
-
* escalation
|
|
5079
|
+
* FR-005: `riskEscalation` (level 'high') is the Stop-gate carrier and has two kinds of evidence with
|
|
5080
|
+
* different meaning here:
|
|
5081
|
+
* - `stuck-risk` (verified failure evidence): the repeat-count threshold is treated as satisfied
|
|
5082
|
+
* (immediate one-rung escalation) and the effective cap rises to 'smart', overriding the
|
|
5083
|
+
* configured cap. This is a capability escalation, so it also counts as complexity evidence.
|
|
5084
|
+
* - `risk-floor` only (the task touches something dangerous): risk raises verification rigor, not the
|
|
5085
|
+
* model tier. It lifts a Tier 1 chore to the Tier 2 executor at most, so a code lane is never
|
|
5086
|
+
* relabelled smart and the Tier 3 orchestrator is never turned into the implementation worker.
|
|
5087
|
+
* The reason is prefixed `stuck-risk:` / `risk-floor:` so consumers can tell the kinds apart from
|
|
5088
|
+
* repeat-count escalation. `complexityEvidence` is the only input that may ask for High effort.
|
|
5033
5089
|
*/
|
|
5034
5090
|
function computeEscalation({
|
|
5035
5091
|
modelTier = null,
|
|
@@ -5042,8 +5098,11 @@ function computeEscalation({
|
|
|
5042
5098
|
return null;
|
|
5043
5099
|
}
|
|
5044
5100
|
const settings = resolveEscalationSettings(config);
|
|
5045
|
-
const
|
|
5046
|
-
|
|
5101
|
+
const riskHigh = riskEscalation?.level === 'high';
|
|
5102
|
+
const stuckDriven = riskHigh && (riskEscalation.sources ?? []).includes('stuck-risk');
|
|
5103
|
+
const floorDriven = riskHigh && !stuckDriven;
|
|
5104
|
+
const repeatDriven = Number.isFinite(repeatCount) && repeatCount >= settings.debugLoopThreshold;
|
|
5105
|
+
if (!settings.enabled || (!stuckDriven && !floorDriven && !repeatDriven)) {
|
|
5047
5106
|
return null;
|
|
5048
5107
|
}
|
|
5049
5108
|
const currentIndex = settings.tierOrder.indexOf(modelTier);
|
|
@@ -5052,22 +5111,30 @@ function computeEscalation({
|
|
|
5052
5111
|
}
|
|
5053
5112
|
const capIndex = settings.tierOrder.indexOf(settings.cap);
|
|
5054
5113
|
const smartIndex = settings.tierOrder.indexOf('smart');
|
|
5055
|
-
const
|
|
5114
|
+
const codeIndex = settings.tierOrder.indexOf('code');
|
|
5115
|
+
let effectiveCapIndex = stuckDriven
|
|
5056
5116
|
? (smartIndex === -1 ? settings.tierOrder.length - 1 : smartIndex)
|
|
5057
5117
|
: (capIndex === -1 ? settings.tierOrder.length - 1 : capIndex);
|
|
5118
|
+
if (floorDriven && !repeatDriven && codeIndex !== -1) {
|
|
5119
|
+
effectiveCapIndex = Math.min(effectiveCapIndex, codeIndex);
|
|
5120
|
+
}
|
|
5058
5121
|
const escalatedIndex = Math.min(currentIndex + 1, effectiveCapIndex);
|
|
5122
|
+
if (floorDriven && !repeatDriven && escalatedIndex <= currentIndex) {
|
|
5123
|
+
return null;
|
|
5124
|
+
}
|
|
5059
5125
|
const escalatedTier = settings.tierOrder[escalatedIndex];
|
|
5060
5126
|
|
|
5061
5127
|
return {
|
|
5062
5128
|
escalatedTier,
|
|
5063
|
-
|
|
5064
|
-
|
|
5129
|
+
complexityEvidence: stuckDriven || repeatDriven,
|
|
5130
|
+
escalationReason: stuckDriven || floorDriven
|
|
5131
|
+
? `${stuckDriven ? 'stuck-risk' : 'risk-floor'}: level=${riskEscalation.level} sources=${(riskEscalation.sources ?? []).join(',')} file=${targetFile ?? 'null'}`
|
|
5065
5132
|
: `repeat-count=${repeatCount} threshold=${settings.debugLoopThreshold} file=${targetFile ?? 'null'}`,
|
|
5066
5133
|
};
|
|
5067
5134
|
}
|
|
5068
5135
|
|
|
5069
5136
|
/** Attaches escalatedTier/escalationReason to routeSummary, additive alongside modelTier. */
|
|
5070
|
-
export function applyEscalationToRouteSummary(routeSummary = null, { config = null, targetFile = null } = {}) {
|
|
5137
|
+
export function applyEscalationToRouteSummary(routeSummary = null, { config = null, targetFile = null, rootDir = null, sessionId = null } = {}) {
|
|
5071
5138
|
if (!routeSummary || typeof routeSummary !== 'object') {
|
|
5072
5139
|
return routeSummary;
|
|
5073
5140
|
}
|
|
@@ -5089,9 +5156,17 @@ export function applyEscalationToRouteSummary(routeSummary = null, { config = nu
|
|
|
5089
5156
|
}
|
|
5090
5157
|
// Recomputed here (not only in buildRouteSummary) so the lane always reflects the final
|
|
5091
5158
|
// tier after escalation, on every path that emits a route summary.
|
|
5159
|
+
const mainTier = resolveMainTier({ rootDir, config, sessionId: sessionId ?? null });
|
|
5092
5160
|
routeSummary.tierLane = buildTierLane({
|
|
5093
5161
|
modelTier: routeSummary.executionContract?.modelTier ?? null,
|
|
5094
5162
|
escalatedTier: escalation?.escalatedTier ?? null,
|
|
5163
|
+
mainTier: mainTier.tier,
|
|
5164
|
+
mainTierSource: mainTier.source,
|
|
5165
|
+
delegationPolicy: routeSummary.executionContract?.delegationPolicy ?? null,
|
|
5166
|
+
executionMode: routeSummary.executionMode ?? null,
|
|
5167
|
+
complexityHigh: escalation?.complexityEvidence === true,
|
|
5168
|
+
riskHigh: routeSummary.riskFloor?.floor === 'high-risk',
|
|
5169
|
+
playbookId: routeSummary.playbookId ?? null,
|
|
5095
5170
|
});
|
|
5096
5171
|
|
|
5097
5172
|
return routeSummary;
|
|
@@ -5156,25 +5231,6 @@ function appendRiskEscalationSegment(routeSummary = null) {
|
|
|
5156
5231
|
return routeSummary;
|
|
5157
5232
|
}
|
|
5158
5233
|
|
|
5159
|
-
// WS-E tier-role wiring: a contract tier only takes effect when the executing session hands
|
|
5160
|
-
// the work to an agent whose definition binds that tier — otherwise every session behaves as
|
|
5161
|
-
// if it only had one model. `code` is the DEFAULT implementation lane, so a code-tier contract
|
|
5162
|
-
// produces no instruction (inline execution on a code session is already correct, and silence
|
|
5163
|
-
// keeps the hook flow smooth). Labels are stable tier roles (lite/code/smart), never provider
|
|
5164
|
-
// or model identity. Vision never appears here — it is a capability lane, not a cost tier.
|
|
5165
|
-
function buildTierLane({ modelTier = null, escalatedTier = null } = {}) {
|
|
5166
|
-
const tier = escalatedTier ?? modelTier;
|
|
5167
|
-
if (!tier || tier === 'vision') {
|
|
5168
|
-
return null;
|
|
5169
|
-
}
|
|
5170
|
-
return {
|
|
5171
|
-
tier,
|
|
5172
|
-
defaultLane: tier === 'code',
|
|
5173
|
-
instruction: tier === 'code'
|
|
5174
|
-
? null
|
|
5175
|
-
: `Hand this task to an agent bound to the ${tier} tier instead of doing it inline, unless this session already runs on the ${tier} tier.`,
|
|
5176
|
-
};
|
|
5177
|
-
}
|
|
5178
5234
|
|
|
5179
5235
|
function advanceContinuationState(continuationState = null, previousContinuationState = null, thresholds = null) {
|
|
5180
5236
|
if (!continuationState || typeof continuationState !== 'object') {
|
|
@@ -6028,6 +6084,27 @@ function shouldIncludeRecentOutput({
|
|
|
6028
6084
|
return true;
|
|
6029
6085
|
}
|
|
6030
6086
|
|
|
6087
|
+
// Orchestration telemetry for the audit row: what the route REQUESTED (tier lane / effort) next to
|
|
6088
|
+
// what the host evidences. Effective values are never inferred from an alias — the only host
|
|
6089
|
+
// evidence available to a hook child is the CLAUDE_EFFORT env; the model stays UNKNOWN.
|
|
6090
|
+
// skill-router.sh builds the same object (audit row parity).
|
|
6091
|
+
export function buildOrchestrationAudit(routeSummary = null, env = process.env) {
|
|
6092
|
+
const lane = routeSummary?.tierLane;
|
|
6093
|
+
if (!lane || typeof lane !== 'object') {
|
|
6094
|
+
return null;
|
|
6095
|
+
}
|
|
6096
|
+
return {
|
|
6097
|
+
mainTier: lane.mainTier ?? null,
|
|
6098
|
+
mainTierSource: lane.mainTierSource ?? null,
|
|
6099
|
+
requestedTier: lane.tier ?? null,
|
|
6100
|
+
lane: lane.instruction ? (lane.mainTier === 'smart' ? 'dispatch-worker' : 'handoff') : 'inline',
|
|
6101
|
+
requestedEffort: lane.effort ?? null,
|
|
6102
|
+
effortBinding: lane.effortBinding ?? null,
|
|
6103
|
+
effectiveEffort: env?.CLAUDE_EFFORT || 'UNKNOWN',
|
|
6104
|
+
effectiveModel: 'UNKNOWN',
|
|
6105
|
+
};
|
|
6106
|
+
}
|
|
6107
|
+
|
|
6031
6108
|
function buildRouteAuditEntry({ route = null, state = null } = {}) {
|
|
6032
6109
|
const routingContext = route?.routingContext ?? state?.routingContext ?? {};
|
|
6033
6110
|
const routeSummary = route?.routeSummary ?? state?.routeSummary ?? {};
|
|
@@ -6086,6 +6163,7 @@ function buildRouteAuditEntry({ route = null, state = null } = {}) {
|
|
|
6086
6163
|
// BL-013: bounded history counters on the audit row — same struct as the
|
|
6087
6164
|
// route record, identical to the hook's audit emission.
|
|
6088
6165
|
historySignals: routeSummary?.historySignals ?? null,
|
|
6166
|
+
orchestration: buildOrchestrationAudit(routeSummary),
|
|
6089
6167
|
};
|
|
6090
6168
|
}
|
|
6091
6169
|
|