@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
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// delegation-gate.mjs — PreToolUse chain step (in-proc `runHook(ctx)`, Claude Code).
|
|
2
|
+
//
|
|
3
|
+
// Turns the feature-implementation Playbook's "delegate the coding to Tier 2" requirement from
|
|
4
|
+
// advice into a host-native refusal. It fires ONLY when ALL of these hold:
|
|
5
|
+
// * the host is Claude Code (UKIT_HARNESS unset/'claude-code') — omp/codex worker identity is
|
|
6
|
+
// UNVERIFIED, so nothing is enforced there (the acceptance verdict stays UNVERIFIED);
|
|
7
|
+
// * the live route state is this task's `feature-implementation` run with
|
|
8
|
+
// `tierLane.delegationRequired === true` (Tier 3 main, Tier 2 executor);
|
|
9
|
+
// * the call is a source/test mutation (Edit/Write/MultiEdit/NotebookEdit, or a Bash command
|
|
10
|
+
// whose file write is reliably identifiable) from the MAIN thread — the host stamps
|
|
11
|
+
// `agent_id` on every subagent tool payload, so main vs worker is read from the host, never
|
|
12
|
+
// from model text.
|
|
13
|
+
// A main-thread mutation is refused until this task's ledger holds an evidenced native worker
|
|
14
|
+
// handback (the "valid delegation path"). Reads, test runs, docs/bookkeeping writes and every
|
|
15
|
+
// unrelated operation pass untouched.
|
|
16
|
+
//
|
|
17
|
+
// Bounded, never a bypass: each refused attempt is a task-owned `delegation-violation` receipt;
|
|
18
|
+
// the DELEGATION_VIOLATION_LIMIT-th one mints a visible sticky `delegation-required` blocker, so
|
|
19
|
+
// the Stop gate ends the run as BLOCKED. Further attempts stay refused. There is no
|
|
20
|
+
// "second attempt succeeds" path.
|
|
21
|
+
//
|
|
22
|
+
// Worker side: a worker's Bash heredoc/redirect/tee/sed -i/script write has no trustworthy file
|
|
23
|
+
// attribution (the ledger would only see an anonymous workspace delta), so it is refused with a
|
|
24
|
+
// pointer to the supported Edit/Write path.
|
|
25
|
+
//
|
|
26
|
+
// Boundary (documented, not hidden): Bash mutations are detected by a quote-aware scan of the
|
|
27
|
+
// command text. Writes made by a script FILE (`node build.js`), by variables/command
|
|
28
|
+
// substitution, or after a `cd` inside the command are not identifiable here. The Stop-time
|
|
29
|
+
// acceptance backstop still counts any anonymous workspace delta as an inline write
|
|
30
|
+
// (`delegation-required-not-performed`), so such work is never reported as PASS.
|
|
31
|
+
//
|
|
32
|
+
// Fail-open on any internal error (a broken advisory gate must not wedge the session). User
|
|
33
|
+
// overrides: `UKIT_DELEGATION_GATE=off` or `.ukit/storage/config.json` orchestration.delegationGate=false.
|
|
34
|
+
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
export const hookFailClosed = false;
|
|
39
|
+
|
|
40
|
+
const FEATURE_PLAYBOOK_ID = 'feature-implementation';
|
|
41
|
+
const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
42
|
+
const PASS = { code: 0, stdout: '', stderr: '' };
|
|
43
|
+
|
|
44
|
+
// First path segment under which a write is never "source/test" (tooling, build output, docs, scratch).
|
|
45
|
+
const EXEMPT_ROOTS = new Set([
|
|
46
|
+
'.git', '.claude', '.ukit', '.omp', '.codex', '.cache', 'node_modules', 'dist', 'build', 'coverage',
|
|
47
|
+
'.next', '.nuxt', '.turbo', 'tmp', 'docs',
|
|
48
|
+
]);
|
|
49
|
+
const EXEMPT_EXT = /\.(md|mdx|txt|log|tmp|lock)$/i;
|
|
50
|
+
|
|
51
|
+
const INTERPRETERS = /^(node|nodejs|deno|bun|python|python2|python3|ruby|perl|php)$/;
|
|
52
|
+
const WRAPPERS = new Set(['sudo', 'env', 'command', 'time', 'nice', 'nohup', 'exec', 'builtin']);
|
|
53
|
+
const SHELLS = new Set(['bash', 'sh', 'zsh', 'dash']);
|
|
54
|
+
// File-writing APIs of the interpreters above. `process.stdout.write` / `sys.stdout.write` are not file writes.
|
|
55
|
+
const WRITE_API = /\b(writeFile(?:Sync)?|appendFile(?:Sync)?|createWriteStream|copyFile(?:Sync)?|rename(?:Sync)?|writeFileSync|write_text|write_bytes|File\.write|file_put_contents)\b|\bopen\s*\([^)]*,\s*['"][wax][+b]*['"]/;
|
|
56
|
+
|
|
57
|
+
// ─── Path classification ────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** True when `raw` names a project source/test file (i.e. a path this gate protects). */
|
|
60
|
+
export function isSourceTarget(raw, { projectRoot, cwd } = {}) {
|
|
61
|
+
let text = String(raw ?? '').trim();
|
|
62
|
+
if (!text) return false;
|
|
63
|
+
if (/[$`]/.test(text) || text.startsWith('~')) return false; // not statically resolvable
|
|
64
|
+
if (text.startsWith('/dev/')) return false;
|
|
65
|
+
const root = path.resolve(projectRoot || '.');
|
|
66
|
+
const abs = path.isAbsolute(text) ? path.resolve(text) : path.resolve(cwd || root, text);
|
|
67
|
+
const rel = path.relative(root, abs);
|
|
68
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false; // outside the project
|
|
69
|
+
const parts = rel.split(path.sep);
|
|
70
|
+
if (EXEMPT_ROOTS.has(parts[0])) return false;
|
|
71
|
+
if (EXEMPT_EXT.test(parts[parts.length - 1])) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Bash scan ──────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
// Splits `command` into heredoc-free text plus the heredoc bodies (bodies may contain `>` etc.).
|
|
78
|
+
function extractHeredocs(command) {
|
|
79
|
+
const lines = String(command).split('\n');
|
|
80
|
+
const kept = [];
|
|
81
|
+
const bodies = [];
|
|
82
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
kept.push(line);
|
|
85
|
+
const re = /<<(-?)\s*(['"]?)([A-Za-z_][\w-]*)\2/g;
|
|
86
|
+
const delimiters = [];
|
|
87
|
+
for (let m = re.exec(line); m; m = re.exec(line)) {
|
|
88
|
+
if (line[m.index - 1] === '<') continue; // here-string `<<<`
|
|
89
|
+
delimiters.push({ strip: m[1] === '-', tag: m[3] });
|
|
90
|
+
}
|
|
91
|
+
for (const { strip, tag } of delimiters) {
|
|
92
|
+
const body = [];
|
|
93
|
+
let closed = false;
|
|
94
|
+
while (i + 1 < lines.length) {
|
|
95
|
+
i += 1;
|
|
96
|
+
const candidate = strip ? lines[i].replace(/^\t+/, '') : lines[i];
|
|
97
|
+
if (candidate.trim() === tag) { closed = true; break; }
|
|
98
|
+
body.push(lines[i]);
|
|
99
|
+
}
|
|
100
|
+
bodies.push(body.join('\n'));
|
|
101
|
+
if (!closed) break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { text: kept.join('\n'), bodies };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Quote-aware pass: returns segments (arrays of words) and file-redirect targets.
|
|
108
|
+
function tokenize(text) {
|
|
109
|
+
const segments = [[]];
|
|
110
|
+
const redirects = [];
|
|
111
|
+
let word = null;
|
|
112
|
+
const pushWord = () => { if (word !== null) { segments[segments.length - 1].push(word); word = null; } };
|
|
113
|
+
const newSegment = () => { pushWord(); if (segments[segments.length - 1].length) segments.push([]); };
|
|
114
|
+
const n = text.length;
|
|
115
|
+
let i = 0;
|
|
116
|
+
const readQuoted = (quote) => {
|
|
117
|
+
let out = '';
|
|
118
|
+
i += 1;
|
|
119
|
+
while (i < n && text[i] !== quote) {
|
|
120
|
+
if (quote === '"' && text[i] === '\\' && i + 1 < n) { out += text[i + 1]; i += 2; continue; }
|
|
121
|
+
out += text[i]; i += 1;
|
|
122
|
+
}
|
|
123
|
+
i += 1;
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
while (i < n) {
|
|
127
|
+
const ch = text[i];
|
|
128
|
+
if (ch === "'" || ch === '"') { word = (word ?? '') + readQuoted(ch); continue; }
|
|
129
|
+
if (ch === '\\' && i + 1 < n) { word = (word ?? '') + text[i + 1]; i += 2; continue; }
|
|
130
|
+
if (ch === '#' && word === null) { while (i < n && text[i] !== '\n') i += 1; continue; }
|
|
131
|
+
if (/\s/.test(ch)) { if (ch === '\n') newSegment(); else pushWord(); i += 1; continue; }
|
|
132
|
+
if (ch === ';' || ch === '|' || ch === '(' || ch === ')' || ch === '`') { newSegment(); i += 1; if (ch === '|' && text[i] === '|') i += 1; continue; }
|
|
133
|
+
if (ch === '&' && text[i + 1] === '&') { newSegment(); i += 2; continue; }
|
|
134
|
+
if (ch === '&' && text[i + 1] !== '>') { newSegment(); i += 1; continue; }
|
|
135
|
+
if (ch === '&' && text[i + 1] === '>') {
|
|
136
|
+
pushWord(); i += 2; if (text[i] === '>') i += 1;
|
|
137
|
+
const target = readRedirectTarget();
|
|
138
|
+
if (target !== null) redirects.push(target);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ch === '>' ) {
|
|
142
|
+
// `2>file` : the fd digit already landed in `word`; drop it when it is a bare fd number.
|
|
143
|
+
if (word !== null && /^\d$/.test(word)) word = null; else pushWord();
|
|
144
|
+
i += 1;
|
|
145
|
+
if (text[i] === '(') continue; // process substitution
|
|
146
|
+
if (text[i] === '&') { i += 1; while (i < n && /[\d-]/.test(text[i])) i += 1; continue; } // fd dup
|
|
147
|
+
if (text[i] === '>' || text[i] === '|') i += 1;
|
|
148
|
+
const target = readRedirectTarget();
|
|
149
|
+
if (target !== null) redirects.push(target);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
word = (word ?? '') + ch; i += 1;
|
|
153
|
+
}
|
|
154
|
+
pushWord();
|
|
155
|
+
function readRedirectTarget() {
|
|
156
|
+
while (i < n && (text[i] === ' ' || text[i] === '\t')) i += 1;
|
|
157
|
+
if (i >= n) return null;
|
|
158
|
+
if (text[i] === "'" || text[i] === '"') return readQuoted(text[i]);
|
|
159
|
+
let out = '';
|
|
160
|
+
while (i < n && !/[\s;|&<>()]/.test(text[i])) { out += text[i]; i += 1; }
|
|
161
|
+
return out || null;
|
|
162
|
+
}
|
|
163
|
+
return { segments: segments.filter((s) => s.length), redirects };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isFlag(word) { return /^-/.test(word); }
|
|
167
|
+
|
|
168
|
+
function stripLeading(words) {
|
|
169
|
+
const out = [...words];
|
|
170
|
+
while (out.length) {
|
|
171
|
+
if (/^[A-Za-z_]\w*=/.test(out[0]) || WRAPPERS.has(out[0])) out.shift(); else break;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function scriptWrites(script, ctx) {
|
|
177
|
+
if (!WRITE_API.test(script)) return null;
|
|
178
|
+
const literals = [...script.matchAll(/['"`]([^'"`\n]{1,200})['"`]/g)].map((m) => m[1])
|
|
179
|
+
.filter((value) => /[\\/]/.test(value) || /\.[A-Za-z0-9]{1,6}$/.test(value));
|
|
180
|
+
if (literals.length === 0) return { kind: 'script-write', target: null };
|
|
181
|
+
const hit = literals.find((value) => isSourceTarget(value, ctx));
|
|
182
|
+
return hit ? { kind: 'script-write', target: hit } : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the reliably identifiable source/test file mutations in a Bash command:
|
|
187
|
+
* [{ kind: 'redirect'|'heredoc'|'tee'|'sed-inplace'|'copy'|'patch'|'script-write', target }].
|
|
188
|
+
* Reads, test runs and writes to exempt paths yield []. Script FILES are not identifiable (see header).
|
|
189
|
+
*/
|
|
190
|
+
export function scanBashMutations(command, ctx = {}, depth = 0) {
|
|
191
|
+
const raw = String(command ?? '');
|
|
192
|
+
if (!raw.trim()) return [];
|
|
193
|
+
const hits = [];
|
|
194
|
+
const { text, bodies } = extractHeredocs(raw);
|
|
195
|
+
const hasHeredoc = bodies.length > 0;
|
|
196
|
+
const { segments, redirects } = tokenize(text);
|
|
197
|
+
for (const target of redirects) {
|
|
198
|
+
if (isSourceTarget(target, ctx)) hits.push({ kind: hasHeredoc ? 'heredoc' : 'redirect', target });
|
|
199
|
+
}
|
|
200
|
+
for (const rawWords of segments) {
|
|
201
|
+
const words = stripLeading(rawWords);
|
|
202
|
+
if (!words.length) continue;
|
|
203
|
+
const cmd = path.basename(words[0]);
|
|
204
|
+
const args = words.slice(1);
|
|
205
|
+
const plain = args.filter((a) => !isFlag(a));
|
|
206
|
+
if (cmd === 'tee') {
|
|
207
|
+
for (const target of plain) if (isSourceTarget(target, ctx)) hits.push({ kind: 'tee', target });
|
|
208
|
+
} else if (cmd === 'sed' || cmd === 'perl' || cmd === 'ruby') {
|
|
209
|
+
const inPlace = args.some((a) => (cmd === 'sed' ? /^-[A-Za-z]*i|^--in-place/ : /^-[A-Za-z]*i/).test(a));
|
|
210
|
+
if (inPlace) {
|
|
211
|
+
const eIndex = args.findIndex((a) => a === '-e' || a === '-f');
|
|
212
|
+
const files = args.filter((a, idx) => !isFlag(a) && !(eIndex >= 0 && idx === eIndex + 1));
|
|
213
|
+
const candidates = cmd === 'sed' && eIndex < 0 ? files.slice(1) : files;
|
|
214
|
+
for (const target of candidates) if (isSourceTarget(target, ctx)) hits.push({ kind: 'sed-inplace', target });
|
|
215
|
+
}
|
|
216
|
+
} else if (cmd === 'cp' || cmd === 'mv' || cmd === 'install') {
|
|
217
|
+
if (plain.length >= 2 && isSourceTarget(plain[plain.length - 1], ctx)) hits.push({ kind: 'copy', target: plain[plain.length - 1] });
|
|
218
|
+
} else if (cmd === 'patch' || (cmd === 'git' && /^(apply|am)$/.test(plain[0] || '') && !args.includes('--check') && !args.includes('--stat'))) {
|
|
219
|
+
hits.push({ kind: 'patch', target: null });
|
|
220
|
+
} else if (cmd === 'dd') {
|
|
221
|
+
const of = args.find((a) => a.startsWith('of='));
|
|
222
|
+
if (of && isSourceTarget(of.slice(3), ctx)) hits.push({ kind: 'copy', target: of.slice(3) });
|
|
223
|
+
} else if (INTERPRETERS.test(cmd)) {
|
|
224
|
+
const inline = [];
|
|
225
|
+
args.forEach((a, idx) => { if (/^-(e|c|p)$/.test(a) && args[idx + 1] !== undefined) inline.push(args[idx + 1]); });
|
|
226
|
+
const scripts = [...inline, ...(hasHeredoc && (plain.length === 0 || args.includes('-') || args.some((a) => /^-[A-Za-z]*$/.test(a))) ? bodies : [])];
|
|
227
|
+
for (const script of scripts) {
|
|
228
|
+
const found = scriptWrites(script, ctx);
|
|
229
|
+
if (found) hits.push(found);
|
|
230
|
+
}
|
|
231
|
+
} else if (SHELLS.has(cmd) && depth < 2) {
|
|
232
|
+
const cIndex = args.indexOf('-c');
|
|
233
|
+
if (cIndex >= 0 && args[cIndex + 1] !== undefined) hits.push(...scanBashMutations(args[cIndex + 1], ctx, depth + 1));
|
|
234
|
+
} else if (cmd === 'eval' && depth < 2) {
|
|
235
|
+
hits.push(...scanBashMutations(args.join(' '), ctx, depth + 1));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return hits;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Gate ───────────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function readJson(file) {
|
|
244
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function userDisabled(projectRoot, env) {
|
|
248
|
+
if (/^(off|0|false)$/i.test(String(env.UKIT_DELEGATION_GATE || ''))) return true;
|
|
249
|
+
const config = readJson(path.join(projectRoot, '.ukit', 'storage', 'config.json'));
|
|
250
|
+
return config?.orchestration?.delegationGate === false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function deny(reason, env) {
|
|
254
|
+
const inChain = Boolean(env.UKIT_HOOK_CHAIN_RUNNER);
|
|
255
|
+
return {
|
|
256
|
+
code: inChain ? 2 : 0,
|
|
257
|
+
stdout: `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } })}\n`,
|
|
258
|
+
stderr: `BLOCKED: ${reason}\n`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function describeTarget(mutation, projectRoot) {
|
|
263
|
+
if (!mutation.target) return 'a source file';
|
|
264
|
+
const text = String(mutation.target);
|
|
265
|
+
const rel = path.isAbsolute(text) && projectRoot ? path.relative(projectRoot, text) : text;
|
|
266
|
+
return `"${(rel && !rel.startsWith('..') ? rel : text).slice(0, 120)}"`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function mainMessage(attempt, limit, taskId, mutation, projectRoot) {
|
|
270
|
+
if (attempt >= limit) {
|
|
271
|
+
return `UKit delegation gate — BLOCKED: this task's feature-implementation Playbook requires the coding to be delegated to the Tier 2 worker, but the orchestrator tried ${attempt} times to write ${describeTarget(mutation, projectRoot)} itself. This run is now recorded as BLOCKED (delegation-required-not-performed). Do not retry the write; report the blocker to the user.`;
|
|
272
|
+
}
|
|
273
|
+
return `UKit delegation gate (attempt ${attempt} of ${limit}): this task's feature-implementation Playbook requires Tier 2 coding delegation, and you are the Tier 3 orchestrator — writing ${describeTarget(mutation, projectRoot)} yourself is refused. Dispatch a native worker now with the Agent tool (subagent_type "feature-implementer", the Tier 2 coding model) and start its prompt with "[ukit-task ${taskId}]", naming the files and behaviour to implement; the worker must edit with Edit/Write. When it returns, verify the integrated result yourself with the bare test runner (for example \`npm test\`) — no pipe, tail, grep or compound command — so the runner's own exit status is the evidence. Repeating this write will end the run as BLOCKED.`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function workerMessage(mutation, projectRoot) {
|
|
277
|
+
return `UKit delegation gate: a Bash ${mutation.kind} write to ${describeTarget(mutation, projectRoot)} has no trustworthy file attribution, so it is not accepted as worker evidence. Redo this change with the Edit or Write tool (the supported worker edit path), then run the tests with the bare runner command.`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function targetsOf(toolName, input) {
|
|
281
|
+
if (toolName === 'Bash') return null;
|
|
282
|
+
const file = input.file_path ?? input.notebook_path ?? input.path;
|
|
283
|
+
return typeof file === 'string' && file ? file : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function evaluate(ctx) {
|
|
287
|
+
const env = ctx.env || {};
|
|
288
|
+
const payload = ctx.payload || {};
|
|
289
|
+
const toolName = payload.tool_name;
|
|
290
|
+
if (toolName !== 'Bash' && !MUTATING_TOOLS.has(toolName)) return PASS;
|
|
291
|
+
if (env.UKIT_HARNESS && env.UKIT_HARNESS !== 'claude-code') return PASS;
|
|
292
|
+
const projectRoot = env.CLAUDE_PROJECT_DIR || ctx.projectRoot || process.cwd();
|
|
293
|
+
if (userDisabled(projectRoot, env)) return PASS;
|
|
294
|
+
|
|
295
|
+
const ledgerMod = await import(new URL('../ukit/runtime/execution-ledger.mjs', import.meta.url).href);
|
|
296
|
+
const playbook = await import(new URL('../ukit/runtime/playbook-execution.mjs', import.meta.url).href);
|
|
297
|
+
const state = await ledgerMod.readRouteState(projectRoot, payload);
|
|
298
|
+
const summary = state?.routeSummary;
|
|
299
|
+
if (!state?.taskInstanceId || summary?.playbookId !== FEATURE_PLAYBOOK_ID || summary.tierLane?.delegationRequired !== true) return PASS;
|
|
300
|
+
|
|
301
|
+
const input = payload.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
|
|
302
|
+
const scanCtx = { projectRoot, cwd: payload.cwd || projectRoot };
|
|
303
|
+
let mutation = null;
|
|
304
|
+
if (toolName === 'Bash') {
|
|
305
|
+
mutation = scanBashMutations(input.command, scanCtx)[0] || null;
|
|
306
|
+
} else {
|
|
307
|
+
const file = targetsOf(toolName, input);
|
|
308
|
+
if (file && isSourceTarget(file, scanCtx)) mutation = { kind: 'edit', target: file };
|
|
309
|
+
}
|
|
310
|
+
if (!mutation) return PASS;
|
|
311
|
+
|
|
312
|
+
if (payload.agent_id) {
|
|
313
|
+
// A worker: Edit/Write is the supported path; only anonymous Bash writes are refused.
|
|
314
|
+
return toolName === 'Bash' ? deny(workerMessage(mutation, projectRoot), env) : PASS;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ledger = await ledgerMod.readExecutionLedger(projectRoot, payload);
|
|
318
|
+
const owned = ledger && ledger.taskInstanceId === state.taskInstanceId ? ledger : null;
|
|
319
|
+
const workers = playbook.assessWorkers(owned?.playbookRun || null);
|
|
320
|
+
if (workers.workers.some((worker) => worker.state === 'evidenced')) return PASS; // valid delegation path exists
|
|
321
|
+
|
|
322
|
+
const limit = playbook.DELEGATION_VIOLATION_LIMIT;
|
|
323
|
+
const prior = Number(owned?.delegationViolations?.count) || 0;
|
|
324
|
+
const attempt = Math.min(prior + 1, limit);
|
|
325
|
+
if (prior < limit) {
|
|
326
|
+
await ledgerMod.recordLedgerEvent({
|
|
327
|
+
type: 'receipt',
|
|
328
|
+
receipt: {
|
|
329
|
+
ts: Date.now(), kind: 'delegation-violation', toolName, toolUseId: payload.tool_use_id || null,
|
|
330
|
+
success: false, mutation: mutation.kind, target: mutation.target ? String(mutation.target).slice(0, 160) : null,
|
|
331
|
+
},
|
|
332
|
+
}, { projectRoot, payload, harness: 'claude-code', routeState: state, deadlineMs: ctx.deadlineMs });
|
|
333
|
+
}
|
|
334
|
+
return deny(mainMessage(attempt, limit, state.taskInstanceId, mutation, projectRoot), env);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function runHook(ctx) {
|
|
338
|
+
try {
|
|
339
|
+
return await evaluate(ctx || {});
|
|
340
|
+
} catch {
|
|
341
|
+
return PASS; // advisory gate: an internal fault never wedges the session
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|