@ngockhoale/ukit 3.4.69 → 3.4.70

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.
@@ -197,27 +197,545 @@ const os = require('os');
197
197
  const { pathToFileURL } = require('url');
198
198
  const { spawn } = require('child_process');
199
199
 
200
- // Wall-clock watchdog. This hook runs on the UserPromptSubmit/PreToolUse hot path but has
201
- // no host-side process-group kill: when indexed-context resolution hangs (a wedged import,
202
- // a stalled mount), the host kills only THIS bash wrapper, the node grandchild below is
203
- // reparented to launchd, and it spins forever. Verified: exactly one orphan leaked per run
204
- // of the router timeout test, 200+ accumulated over days. Self-exit at the deadline so a
205
- // hang can never outlive the hook. Advisory only — an early exit just drops the routing
206
- // hint, the same posture as a cache miss. Same watchdog task-watchdog.sh/handoff-resume.sh
207
- // already carry; keep this one aligned with them.
208
200
  const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
209
201
  setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
210
202
 
211
203
  (async () => {
204
+ // BEGIN GENERATED SEMANTIC ENGINE sha256=4a3ee8d8789589bab57ab21ad404a2a09a1ff45b31e5d5204c87a2604e305fd2
205
+ // Canonical task semantics; intent never grants authorization or proves completion.
206
+ const SEMANTIC_ENGINE_VERSION = 'round4-semantic-1:1dcaa0253e7bc7f4356b02fe5de33b1edda6f0e00d9f2022d9c8cdbe907003a2';
207
+
208
+ function semanticContractIdentity({ promptText = '', targetFile = '', taskInstanceId = null } = {}) {
209
+ return { taskInstanceId, targetFile, engineVersion: SEMANTIC_ENGINE_VERSION,
210
+ promptFingerprint: crypto.createHash('sha256').update(String(promptText)).digest('hex') };
211
+ }
212
+
213
+ const semanticContractShape = (contract) => JSON.stringify([
214
+ contract?.taskKind, contract?.publication === true, contract?.operationGoal ?? null,
215
+ contract?.configurationScope ?? null, contract?.releasePolicy ?? null, contract?.conditionalObligations ?? null,
216
+ contract?.reportArtifact ?? null, contract?.verificationOnly === true, contract?.statusReport === true,
217
+ [...(contract?.completionEvidence || [])].sort(),
218
+ ]);
219
+ function compareSemanticContracts({ stored, fresh, previousIdentity, identity }) {
220
+ if (semanticContractShape(stored) === semanticContractShape(fresh)) return { status: 'equal', identity };
221
+ const sameInput = previousIdentity && previousIdentity.taskInstanceId === identity.taskInstanceId
222
+ && previousIdentity.promptFingerprint === identity.promptFingerprint && previousIdentity.targetFile === identity.targetFile;
223
+ const status = sameInput && previousIdentity.engineVersion === identity.engineVersion ? 'invariant-violation'
224
+ : sameInput ? 'engine-migration' : previousIdentity ? 'input-change' : 'legacy-migration';
225
+ return { status, identity, previousIdentity, stored, rederived: fresh };
226
+ }
227
+ const foldIntent = (text) => String(text).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
228
+
229
+ function analyzeTaskIntent({ promptText = '', targetFile = '', taskInstanceId = null, sessionId = null } = {}) {
230
+ const original = String(promptText);
231
+ const spans = [];
232
+ let fence = null;
233
+ let description = false;
234
+ let offset = 0;
235
+ for (const rawLine of original.split(/(?<=\n)/)) {
236
+ const label = foldIntent(rawLine).replace(/^[\s#*\d.)-]+/, '').trim();
237
+ const bullet = /^\s*(?:[-*+•]|\d+[.)])\s+/.test(rawLine);
238
+ const marker = rawLine.match(/^\s*(`{3,}|~{3,})/);
239
+ if (marker) fence = fence ? null : marker[1][0];
240
+ const execution = /^(?:now |then |please )*(?:execute|perform|follow|run)\b.*\b(?:steps?|workflow|instructions)\b|^(?:instructions|commands|yeu cau)\s*:/.test(label);
241
+ if (execution) description = false;
242
+ else if (/\b(?:examples?|cases|inputs?|reproductions?|history|historical|observed|expected|distinguish|compare|checklist|findings)\b[^\n]*:\s*$/.test(label)) description = true;
243
+ else if (!bullet && label && !/:\s*$/.test(label)) description = false;
244
+ const parts = [...rawLine.replace(/\r/g, ' ').matchAll(/.+?(?:[;\n]|\.(?=\s|$)|(?=\s*(?:,\s*)?(?:but|then|however|nhưng|sau đó)\s+(?:(?:now|please|run)\s+)*(?:implement|fix|modify|edit|apply|restart|deploy|publish|git|sửa)\b)|$)/gi)];
245
+ for (const part of parts) {
246
+ const raw = rawLine.slice(part.index, part.index + part[0].length);
247
+ const text = foldIntent(raw);
248
+ const quoted = Boolean(marker || fence || /^\s*>/.test(raw));
249
+ const context = quoted ? 'quotation' : description && bullet ? 'description' : 'instruction';
250
+ const polarity = /\b(?:do not|don't|never|must not|no|khong|chua)\b|đừng/i.test(raw) || /\b(?:prohibited|forbidden|banned)\b/.test(text) ? 'prohibited' : 'affirmative';
251
+ const condition = text.match(/\b(?:if|neu|only if|only when)\b[^,;.\n]*/)?.[0] || null;
252
+ spans.push({ start: offset + part.index, end: offset + part.index + raw.length, raw,
253
+ context, polarity, condition, activation: condition ? 'pending' : context !== 'instruction' || polarity === 'prohibited' ? 'inactive' : 'eligible' });
254
+ }
255
+ offset += rawLine.length;
256
+ }
257
+ for (let index = 1; index < spans.length; index += 1) {
258
+ const span = spans[index];
259
+ if (!span.condition && /^\s*otherwise\b/.test(foldIntent(span.raw)) && spans[index - 1].condition) {
260
+ span.condition = `otherwise:${spans[index - 1].condition}`;
261
+ span.activation = 'pending';
262
+ }
263
+ }
264
+ let semanticInput = original;
265
+ // Preserve the existing native preflight and defect-repair lifecycles. Other
266
+ // leading predicates have no receipt resolver yet: retain them as pending
267
+ // obligations rather than upgrading their consequence into an immediate order.
268
+ const nativeReleasePolicy = classifySemanticText({ promptText: original, targetFile }).releasePolicy;
269
+ const deferredSpans = new Set(spans.filter(span => span.context === 'instruction'
270
+ && span.polarity === 'affirmative' && span.condition
271
+ && /^(?:[-*+•\d.)\s]*)(?:if|neu|only if|only when)\b/.test(foldIntent(span.raw))
272
+ && !nativeReleasePolicy && !VERIFICATION_GOAL.test(foldIntent(original))
273
+ && !defectRepairBranches(foldIntent(span.raw)).length
274
+ && ordersPublicationText(foldIntent(span.raw).replace(/^[^,]*,\s*/, ''))));
275
+ // An explicit alternative links both consequences. Keep the unconditional
276
+ // review prefix, but do not require a repair before its review has found one.
277
+ const alternatives = spans.filter(span => span.condition?.startsWith('otherwise:'));
278
+ for (const alternative of alternatives) {
279
+ const preceding = spans[spans.indexOf(alternative) - 1];
280
+ if (preceding.context === 'instruction' && preceding.polarity === 'affirmative'
281
+ && alternative.context === 'instruction' && alternative.polarity === 'affirmative') {
282
+ deferredSpans.add(preceding);
283
+ deferredSpans.add(alternative);
284
+ }
285
+ }
286
+ for (const span of [...spans].reverse()) {
287
+ const approvalPending = span.condition && /\b(?:approv\w*|permission|authori[sz]\w*)\b/.test(span.condition);
288
+ if (span.context === 'quotation' || span.context === 'description' || approvalPending || deferredSpans.has(span)) {
289
+ // Retain the prefix before a comma-introduced postfix repair condition.
290
+ // Leading conditions and otherwise consequences are masked in full.
291
+ const prefix = deferredSpans.has(span) && !/^(?:\s*)(?:if|neu|only if|only when|otherwise)\b/.test(foldIntent(span.raw))
292
+ ? span.raw.match(/^[^,]*,/)?.[0] || '' : '';
293
+ const masked = prefix + span.raw.slice(prefix.length).replace(/[^\r\n]/g, ' ');
294
+ semanticInput = semanticInput.slice(0, span.start) + masked + semanticInput.slice(span.end);
295
+ }
296
+ }
297
+ const semantic = classifySemanticText({ promptText: semanticInput.replace(/\r\n?/g, '\n'), targetFile });
298
+ const actions = spans.flatMap((span, spanIndex) => {
299
+ const text = foldIntent(span.raw);
300
+ // Categories are independent obligations, not mutually exclusive labels. Keep
301
+ // pending actions in the inventory even though no predicate or permission is proven.
302
+ const categories = [
303
+ ['publication', /\b(?:publish|npm publish|phat hanh)\b/],
304
+ ['git', /\bgit\s+(?:push|commit|tag|reset|clean|merge|rebase)\b|\b(?:commit|tag|push)(?:\/(?:commit|tag|publish|push))+\b/],
305
+ ['runtime', /\b(?:restart|deploy|redeploy)\b/],
306
+ ['source', /\b(?:implement|fix|modify|edit|sua|refactor|apply)\b/],
307
+ ['verification', /\b(?:verify|test|rerun|confirm|validate)\b/],
308
+ ].filter(([, pattern]) => pattern.test(text)).map(([category]) => category);
309
+ const sourceTarget = span.raw.match(/\b((?:[\w.-]+\/)+[\w.-]+\.[cm]?[jt]sx?)\b/)?.[1];
310
+ return categories.map(category => {
311
+ const permittedBySemantic = category === 'publication' ? semantic.publication === true
312
+ : category === 'git' || category === 'runtime' ? Boolean(semantic.operationGoal)
313
+ : category === 'source' ? ['CODE_IMPLEMENTATION', 'MIXED', 'CONFIGURATION'].includes(semantic.taskKind) : true;
314
+ return { spanIndex, category, target: category === 'source' ? sourceTarget || targetFile || null : semantic.operationGoal || null,
315
+ condition: span.condition, authorization: 'not-evaluated',
316
+ active: span.activation === 'eligible' && permittedBySemantic,
317
+ explanation: span.activation === 'eligible' ? 'canonical classification of current owner prompt' : `${span.context}/${span.polarity}/${span.activation}` };
318
+ });
319
+ });
320
+ const conditionalObligations = actions.filter(action => deferredSpans.has(spans[action.spanIndex])
321
+ && ['publication', 'git', 'runtime', 'source'].includes(action.category))
322
+ .map(action => ({ ...action, activation: 'pending' }));
323
+ if (conditionalObligations.length) {
324
+ semantic.conditionalObligations = conditionalObligations;
325
+ if (semantic.taskKind === 'READ_ONLY') {
326
+ delete semantic.statusReport;
327
+ semantic.verificationOnly = true;
328
+ }
329
+ }
330
+ return { engineVersion: SEMANTIC_ENGINE_VERSION, identity: { taskInstanceId, sessionId }, original, spans, actions, semantic };
331
+ }
332
+ const PUBLISH_ORDER_NEGATOR = /\b(?:do not|don't|dont|never|without|not yet|no|not|avoid|skip|exclud\w*|forbid\w*|prohibit\w*|khong|chua)\b|--dry-run/;
333
+ const PUBLISH_ORDER_IMPERATIVE = /^(?:please\s+|just\s+|now\s+|also\s+)*(?:run|do|execute|perform|go|ship|publish|release|push|tag|commit|verify|make|cut|chay)\b/;
334
+ const PUBLISH_ORDER_HEADER = /prohibit|forbidden|out of scope|non-goal|scope control/;
335
+ function defectRepairBranches(text) {
336
+ return [...text.matchAll(/(?:^|\n|\.\s+)[ \t]*(?:[-*]|\d+[.)])?\s*((?:neu\s+(?:gap|phat hien|tim thay|co)|if\b)[^.!?]*(?:\.(?!\w)|$))/g)]
337
+ .map((match) => match[1])
338
+ .filter((branch) => !/\n\s*\n/.test(branch)
339
+ && /^(?:neu\s+(?:gap|phat hien|tim thay|co)\s+(?:bug|loi)|if\s+[^,\n]*(?:bug|defect|error)\b)/.test(branch)
340
+ && /\b(?:sua|fix|repair)\b/.test(branch));
341
+ }
342
+
343
+ function publicationScope(promptText = '') {
344
+ const plain = String(promptText).normalize('NFC')
345
+ .replace(/\bdùng\b/giu, 'su dung').replace(/đừng/giu, 'khong')
346
+ .replace(/(^|[\n;.]\s*)([ \t]*(?:[-*+•]|\d+[.)])?[ \t]*)dung(?=\s+(?:publish|release|npm|git|run|fix|sua)\b)/gi, '$1$2khong')
347
+ .normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
348
+ .replace(/```[^]*?```|~~~[^]*?~~~/g, '').replace(/^[ \t]*>.*$/gm, '')
349
+ .replace(/[“"][^”"\n]*[”"]|‘[^’\n]*’/g, '')
350
+ .replace(/\b(?:not a request to|khong phai yeu cau)\b[^.;\n]*(?=[.;\n]|$)/g, 'review')
351
+ .replace(/^([ \t]*(?:[-*+•]|\d+[.)])?[ \t]*)(?:(?:hay|vui long|xin)\s+)+/gm, '$1')
352
+ .replace(/\bout of scope\s*:[^.;\n]*/g, 'review')
353
+ .replace(/(?:^|(?<=[.;\n]))\s*(?:to\s+[^.;\n]{0,80},\s*you can\s+|(?:the\s+)?reviewer\s+suggested\s*:)\s*[^.;\n]*(?=[.;\n]|$)/g, '');
354
+ let section = null;
355
+ let review = false;
356
+ let described = false;
357
+ let reproduction = false;
358
+ let exampleIndent = null;
359
+ const lines = [];
360
+ for (const line of plain.split('\n')) {
361
+ if (!line.trim()) { lines.push(''); continue; }
362
+ const label = line.replace(/^[\s#*\d.)-]+/, '').trim();
363
+ const bullet = /^\s*(?:[-*+•]|\d+[.)]|[a-z][.)])\s+/.test(line);
364
+ const execution = /^(?:(?:please|now|then)\s+)*(?:execute|perform|follow|run|thuc hien|chay)\b.*\b(?:steps?|workflow|process|instructions|cac buoc)\b/.test(label)
365
+ || /^(?:instructions|commands|yeu cau)\s*:/.test(label);
366
+ if (/^\s*\d+[.)]\s+release\b.*\b(?:bat buoc|mandatory)\b/.test(line)) { section = 'execute'; lines.push('instructions:'); continue; }
367
+ if (execution) section = 'execute';
368
+ else if (section === 'execute' && /:\s*$/.test(label)) section = 'execute';
369
+ else if (/^(?:do not|prohibitions?|forbidden)\s*:\s*$/.test(label)) section = 'prohibit';
370
+ else if (section === 'prohibit' && bullet) { lines.push(`do not ${label}`); continue; }
371
+ else if (/^(?:distinguish|compare)(?:\s+between)?\s*:\s*$/.test(label)) { section = 'describe'; lines.push('review distinctions:'); continue; }
372
+ else if (/\b(?:review|audit|inspect|checklist)\b.*:\s*$/.test(label)
373
+ || (review && (/\b(?:steps?|usage|process|workflow)\s*:\s*$/.test(label)
374
+ || /^\s*[a-z][.)]\s+[^\n]*:\s*$/.test(line)))) section = 'describe';
375
+ if (/^(?:the\s+)?(?:docs?|documentation|manual|readme)\s+(?:says?|states?|shows?|describes?)\s*:/.test(label)) {
376
+ lines.push('review described documentation');
377
+ continue;
378
+ }
379
+ if (section === 'describe' && bullet && !execution) continue;
380
+ if (!bullet && !/:\s*$/.test(label)) section = null;
381
+ if (/\b(?:review|audit|read[_ -]*only)\b/.test(label)) review = true;
382
+ if (/\b(?:false negatives?|findings|regression matrix|test matrix)\s*:\s*$/.test(label)
383
+ || /^(?:exact\s+)?(?:(?:positive|negative)\s+)?reproductions\b[^:]*:\s*$/.test(label)) { section = 'describe'; lines.push('review described context'); continue; }
384
+ if (label === 'reproduction:') { reproduction = true; continue; }
385
+ if (reproduction) { reproduction = false; continue; }
386
+ const indent = line.search(/\S/);
387
+ if (exampleIndent !== null && indent > exampleIndent && bullet) continue;
388
+ exampleIndent = null;
389
+ if (/^(?:them (?:positive|negative) fixtures|nhung van nhan dung imperative that)\s*:/.test(label)) {
390
+ exampleIndent = indent;
391
+ continue;
392
+ }
393
+ if (/^\s*\d+[.)]\s+release\b.*—\s*(?:dieu kien|prerequisites)\b/.test(line)) continue;
394
+ if (/\b(?:prompt|request|task)\b[^.;\n]*\b(?:prohibit\w*|cam)\b/.test(label)) { lines.push(`do not ${label}`); continue; }
395
+ const example = /^(?:(?:cac|the)\s+)?(?:(?:positive|negative|test)\s+(?:cases?|inputs?|examples?|controls?)|examples?|inputs?|observed|actual|expected|bug report|confirm|historical (?:evidence|input)|vi du)\s*:|^required replay matrix\s*$/.test(label);
396
+ if (example) { described = /:\s*$|^required replay matrix\s*$/.test(label); continue; }
397
+ if (described && bullet) continue;
398
+ if (!bullet) described = false;
399
+ if (/^(?:input|observed|actual|expected|bug description|history|checklist)\s*:/.test(label)) continue;
400
+ // Inspecting an operation names its subject, not an execution order. Keep later clauses active.
401
+ if (/^(?:check|inspect|review|audit|compare)\s+git\b/.test(label)) {
402
+ lines.push(line.replace(/^[^.;\n]*?(?=\s+(?:then|but|instead)\s+|\s+and\s+(?:run|execute|implement|modify|edit|fix|publish)\b|[.;\n]|$)/, 'review subject'));
403
+ continue;
404
+ }
405
+ lines.push(line);
406
+ }
407
+ const text = lines.join('\n');
408
+ let prohibited = false;
409
+ let prohibitionBlock = false;
410
+ for (const line of text.split('\n')) {
411
+ if (!line.trim()) continue;
412
+ const bullet = /^\s*(?:[-*•]|\d+[.)])\s+/.test(line);
413
+ if (!bullet) prohibitionBlock = PUBLISH_ORDER_NEGATOR.test(line) && /:\s*$/.test(line);
414
+ for (const clause of line.split(/;|\.\s+|\bthen\b|\bbut\b|\binstead\b/)) {
415
+ let negated = bullet && prohibitionBlock;
416
+ for (const raw of clause.split(/,|\band\b|\bor\b|\bnor\b/)) {
417
+ const item = raw.replace(/^[\s*•-]+/, '').trim();
418
+ if (PUBLISH_ORDER_NEGATOR.test(item)) negated = true;
419
+ else if (/^(?:please\s+|now\s+|also\s+)*(?:run|execute|perform|chay)\s+\S/.test(item)) negated = false;
420
+ if (negated && /\b(?:release|publish|publication)\b/.test(item)) prohibited = true;
421
+ }
422
+ }
423
+ }
424
+ return { text, prohibited };
425
+ }
426
+
427
+ function ordersPublication(promptText = '') {
428
+ return analyzeTaskIntent({ promptText }).semantic.publication === true;
429
+ }
430
+
431
+ function ordersPublicationText(promptText = '') {
432
+ let active = publicationScope(promptText).text.replace(/```[^]*?```|~~~[^]*?~~~/g, '')
433
+ .replace(/^[ \t]*>.*$/gm, '').replace(/[“"][^”"\n]*[”"]|‘[^’\n]*’/g, '');
434
+ for (const branch of defectRepairBranches(active)) active = active.replace(branch, '');
435
+ let ordered = false;
436
+ let blockNegated = false;
437
+ for (const line of active.split('\n')) {
438
+ const bullet = /^\s*(?:[-*\u2022]|\d+[.)])\s+/.test(line);
439
+ if (!line.trim()) { blockNegated = false; continue; }
440
+ if (!bullet) blockNegated = line.trim().length <= 80 && (PUBLISH_ORDER_NEGATOR.test(line) || PUBLISH_ORDER_HEADER.test(line)) && /:\s*$|^[^.]{0,40}$/.test(line.trim()) && !/\bnpm\s+publish\b/.test(line);
441
+ for (const sentence of line.split(/\s*;\s*|\.\s+|\.$/)) {
442
+ for (const hard of sentence.split(/\bthen\b|\bbut\b|\binstead\b/)) {
443
+ let negated = bullet && blockNegated;
444
+ for (const raw of hard.split(/,|\band\b|\bor\b|\bnor\b/)) {
445
+ const item = raw.replace(/^[\s\-*\u2022]+|^\d+[.)]\s*/g, '').trim();
446
+ if (PUBLISH_ORDER_NEGATOR.test(item)) negated = true;
447
+ else if (/^(?:please\s+|now\s+|also\s+)*(?:run|execute|perform|chay)\s+\S/.test(item)) negated = false;
448
+ const commandItem = item.replace(/^(?:please|just|now|also|then)\s+/, '');
449
+ const publication = /\bnpm\s+publish\b/.test(commandItem) && /^(?:npm\s+publish|run|do|execute|perform|go|ship|publish|release|push|tag|commit|verify|make|cut|chay)\b/.test(commandItem);
450
+ const releaseOrder = /^(?:please\s+|just\s+|now\s+|also\s+)*(?:release|ship|publish)\s+(?!instructions?\b|obligations?\b|policies\b|boundary\b|safety\b|progress\b|candidate\s+review\b|readiness\b|verifier\b|verification\b|evidence\b|policy\b|intent\b|notes\b|terminology\b)\S/.test(item);
451
+ const politeItem = item.replace(/^(?:(?:please|just|now|also|then|vui long|hay)\s+)+/, '');
452
+ const viPublish = /^(?:(?:su dung|phat hanh bang)\s+`?npm\s+publish\b|(?:dua|phat hanh|xuat ban|deploy|day)\b(?:[^.;\n]|\.(?=\d)){0,60}\b(?:len|to|vao)\s+npm\b)/.test(politeItem);
453
+ if (!negated && (releaseOrder || viPublish || (publication && (
454
+ (/^npm\s+publish\b/.test(politeItem) && (/^(?:[\s*•-]|\d+[.)])*(?:(?:please|just|now|also)\s+)*(?:npm\s+publish|run|execute|perform|commit|push|tag)\b/.test(hard.trim()) || /\bcontinue the release workflow:/.test(hard))) || PUBLISH_ORDER_IMPERATIVE.test(item)
455
+ || /\b(?:chay|run)\s+`?npm\s+publish\b/.test(item)
456
+ || /continue the release workflow:/.test(item))))) ordered = true;
457
+ }
458
+ }
459
+ }
460
+ }
461
+ return ordered;
462
+ }
463
+
464
+ const NOT_A_SERVICE = new Set(['it', 'this', 'that', 'them', 'the', 'all', 'everything', 'service', 'services', 'again', 'now', 'to', 'and', 'then']);
465
+ function nativeOperationGoal(domain, actionable, text) {
466
+ const goal = { domain, target: null, desiredState: 'verified', completionSemantics: 'execute-and-verify', requiredTargets: [] };
467
+ const clauses = actionable.join(' . ');
468
+ if (domain === 'git-integrity') {
469
+ const verbs = [...clauses.matchAll(/\bgit\s+(push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b/g)].map((m) => m[1]);
470
+ if (/\bpush\s+(?:to\s+)?origin\b/.test(clauses)) verbs.push('push');
471
+ for (const clause of actionable) {
472
+ const bare = clause.match(GIT_ORDER_LEAD);
473
+ if (bare) verbs.push(bare[1]);
474
+ }
475
+ goal.requiredTargets = [...new Set(verbs)];
476
+ goal.check = /\bfsck\b/.test(text) ? 'fsck' : /\bdiff\s+--check\b/.test(text) ? 'diff-check' : null;
477
+ if (actionable.some((clause) => PRIVATE_ORDER.test(String(clause).replace(/"[^"\n]*"|`[^`\n]*`/g, ' ')))) goal.visibility = 'PRIVATE';
478
+ return goal;
479
+ }
480
+ const services = [...clauses.matchAll(/\b(?:restart|redeploy|deploy|khoi dong lai|trien khai)\s+(?:the\s+|lai\s+)?([a-z0-9][\w.-]*)/g)]
481
+ .map((m) => m[1]).filter((name) => !NOT_A_SERVICE.has(name));
482
+ goal.requiredTargets = [...new Set(services)];
483
+ const probePath = text.match(/(?:^|\s)(\/[a-z0-9_.-]+(?:\/[a-z0-9_.-]+)*)/)?.[1] || null;
484
+ const probeStatus = text.match(/\b(?:returns?|returning|responds?(?:\s+with)?|http|status(?:\s+code)?)\s+([1-5]\d\d)\b/)?.[1] || null;
485
+ if (probePath || probeStatus) goal.probe = { path: probePath, status: probeStatus };
486
+ return goal;
487
+ }
488
+
489
+ const NON_ORDER_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|restart|sua|viet|chinh|them|tao|xoa';
490
+ const VERIFICATION_GOAL = /(?:^|\n)[ \t*#_-]*(?:muc tieu|goal|purpose|objective)\s*:[^\n]*\b(?:xac nhan|verify|confirm|kiem tra|validate|double-?check|re-?run)\b/;
491
+ const CONDITIONAL_BRANCH = /(?:^|\n|\.\s+)[ \t]*(?:\d+[.)]\s*)?(?:neu\s+(?:phat hien\s+(?:false-block|loi|bug|defect)|co\s+(?:source fix|sua doi))|if\s+(?:(?:a\s+)?(?:bug|defect|failure)\s+is\s+found|(?:you\s+)?find\s+(?:a\s+)?(?:bug|defect)))\b[^\n]*?(?=\.\s+|$|\n)/g;
492
+ const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
493
+ const DISPOSABLE_SETUP = /\b(?:tao|create)\s+(?:mot\s+)?(?:project|du an)\s+(?:thu nghiem|test|disposable|doc lap)\b(?:\s+(?:doc lap|independent))?/g;
494
+ const noVerbList = () => new RegExp(`\\bno\\s+(?:${ORDER_VERBS})\\b(?:\\s*,\\s*(?:${ORDER_VERBS})\\b)*`, 'g');
495
+ function neutralizeNonOrders(text) {
496
+ let source = publicationScope(text).text.replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
497
+ if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
498
+ // A labeled goal/acceptance span enumerates desired end states, so it is an order only when one of its own
499
+ // sentence fragments is imperative ("goal: ship it, then update the docs" orders; "goal: clean install and
500
+ // normal ukit update pass" does not — "update" there is a noun inside the acceptance criterion).
501
+ source = source.replace(/(^|\n|[.;]\s[ \t]*)([ \t*#_-]*(?:(?:final\s+)?(?:product\s+)?(?:muc tieu|goal|purpose|objective|acceptance criteria|tieu chi)\s*:))([^\n]*)/g,
502
+ (m, pre, label, content) => {
503
+ const frags = content.split(/\s*(?:[;,.]|\band\b|\bor\b|\bthen\b|\bva\b)\s*/).filter(Boolean);
504
+ return frags.some((f) => IMPERATIVE_ORDER.test(f) || MODAL_ORDER.test(f)) ? m : pre + label;
505
+ });
506
+ source = source
507
+ .replace(/\bno\s+git\s+(?:push|tag)\b(?:\s*\/\s*tag\b)?/g, ' ')
508
+ .replace(/\brelease\s+verifier\b/g, 'verification topic')
509
+ .replace(/\b(?:but|however)\s+(?=(?:run|execute|implement|modify|edit)\b)/g, '; ')
510
+ .replace(/\b(?:add|create|write)\s+(?:bounded\s+)?(?:independent\s+)?(?:adversarial\s+)?(?:replay\s+)?(?:variants|cases|tests|fixtures|evidence)\s+in\s+scratch\s+only\b/g, 'review replay');
511
+ const publicationList = '(?:ci|commit|push|tag|release|(?:npm\\s+)?publish)';
512
+ source = source.replace(new RegExp(`\\b(?:no|forbidden|prohibited|banned)\\s*:?\\s+${publicationList}\\b(?:\\s*(?:,|and|or)\\s*${publicationList}\\b)*`, 'g'), ' ');
513
+ return source.replace(noVerbList(), ' ')
514
+ .replace(/\brelease-(?:publication|verification)\b|\brelease\s+(?:instructions?|authorization|obligations?|policies|readiness)\b/g, 'publication topic')
515
+ .replace(/\brelease\s+(?:safety|verification\s+plan)\b/g, 'publication topic')
516
+ .replace(/^[ \t]*[-*+•][ \t]*owner\s+yeu\s+cau\b[^\n]*/gm, ' ')
517
+ .replace(/^([ \t]*[-*+•][ \t]*)release\s+progress\s+reporter(?=[ \t]*(?:,|$))/gm, '$1progress reporter')
518
+ .replace(/^[ \t]*(?:[-*+\u2022]|\d+[.)])[ \t]*(?:negative|positive)\s+controls?\b[^\n]*(?:\n(?![ \t]*(?:[-*+\u2022]|\d+[.)])|[ \t]*\n)[^\n]*)*/gm, ' ')
519
+ .replace(/(?:^|(?<=[.;][ \t]))[^.;\n]*\b(?:khong\s+(?:cho\s+phep|duoc|lam)|(?:does|do|did|is|are)\s+not\s+(?:allow|permit|authori[sz]e)\w*|not\s+(?:allowed|permitted|authori[sz]ed)|prohibitions?|prohibited(?:\s+actions?)?|forbidden(?:\s+actions?)?|banned|must\s+not\s+do)\s*:[ \t]*(?:\n[ \t]*(?:[-*+\u2022]|\d+[.)])[^\n]*)+/gm, ' ')
520
+ .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
521
+ .replace(new RegExp(`\\b(?:(?:tuyet\\s+doi\\s+)?khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
522
+ .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
523
+ .replace(/\b(?:uu tien|prefer(?:red)?)\s+(?:a\s+|an\s+)?(?:configuration|config|cau hinh)\s+(?:change|changes|update|thay doi)\b/g, ' ')
524
+ .replace(/\b(?:neu\s+(?:chi\s+)?(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
525
+ .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
526
+ .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, ' ')
527
+ .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
528
+ .replace(/\bwrite\s+permission\b/g, 'permission')
529
+ .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
530
+ .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
531
+ .replace(/(?:^|[\n.;])[ \t]*(?:[-*+•]|\d+[.)])?[ \t]*(?:outputs?|reports?|deliverables?|each|lists?|includes?|fields?|format|sections?|columns?)(?:\s+\w+){0,3}?\s*:(?:[^\n.;]|\.(?=\w))*/g,
532
+ (span, offset, whole) => {
533
+ const fixNouns = /\bfix(?:es)?(?:\s+applied\b|\s+(?:directions?|lists?|plans?|suggestions?|proposals?|items?|entries?|notes?|findings?|acceptance|recommendations?)\b)(?=\s*(?:,|$))|\bfixes\b(?=\s*(?:,|$))/g;
534
+ const residual = span.replace(fixNouns, ' ');
535
+ if (residual === span) return span;
536
+ if (new RegExp(`\\b(?:then|${NON_ORDER_VERBS}|${ORDER_VERBS})\\b`).test(residual)) return span;
537
+ const tail = whole.slice(offset + span.length);
538
+ if (tail.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/).some((clause) => CLAUSE_ORDER_LEAD.test(clause))) return span;
539
+ return residual;
540
+ });
541
+ }
542
+
543
+ const READ_ONLY_DECLARATION = /\breviewer\s*[,—:-]?\s+read[\s_-]*only\b|\bread[\s_-]*only(?:\s+\w+){0,3}?\s+(?:audit|investigation|review|analysis|inspection|inventory|task|request|session|pass|check|diagnos\w+|verification|forensics?|reports?)\b|(?:^|\n)[\s*#>_-]*read[\s_-]*only\s*[.:!—-]|\b(?:audit|investigation|review|analysis|inventory)[\s-]*only\b|[(,]\s*read[\s_-]*only\s*[).]|\bchi\s+(?:doc|audit|kiem tra|kiem ke|phan tich|dieu tra)\b|\bkhong\s+sua\b|\b(?:make|with)\s+no\s+(?:source\s+|code\s+|file\s+)?(?:changes|edits|modifications)\b|\b(?:do\s+not|don't|dont|never)\s+(?:\w+\s+){0,2}edits?\b/;
544
+ 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|bo sung|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)';
545
+ const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
546
+ 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`);
547
+ function hasExplicitOrder(clauses) {
548
+ return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
549
+ }
550
+ const CLAUSE_ORDER_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:commit|push(?!\s+back\b)|publish|release(?!\s+(?:notes?|process|works?|plan|candidate|checklist)\b)|apply|(?:run|execute)\s+(?:the\s+)?(?:implementation|build|migration|deploy(?:ment)?|publish|release|script|command))\b/;
551
+ const ADDITIVE_ORDER_LEAD = /^(?:[-*+\u2022>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:add|create|build|implement|them|bo sung|tao)\s+(?=[^.;\n]{0,80}?(?:\b(?:tests?|checks?|e2e|specs?|fixtures?|files?|modules?|functions?|features?|endpoints?|handlers?|resolvers?|parsers?|helpers?|scripts?|hooks?|options?|flags?|source|code)\b|\.[cm]?[jt]sx?\b))/;
552
+ function ordersChangeAcrossClauses(clauses) {
553
+ return clauses.some((clause) => CLAUSE_ORDER_LEAD.test(clause));
554
+ }
555
+ const GIT_ORDER_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|chi|now|also|first|then)\s+)*(commit|push(?!\s+back\b))\b/;
556
+ const PRIVATE_ORDER = /\b(?:make|set|keep|switch|change|lam|giu|chuyen|dat)\b[^.;\n]{0,40}\b(?:private|rieng tu|che do private)\b|\bvisibility\s+(?:to\s+|sang\s+)?private\b|\b(?:create|tao|push|publish|day)\b[^.;\n]{0,40}\b(?:private\s+(?:github\s+|git\s+)?(?:repo|repos|repository|remote)|(?:repo|repos|repository|remote)\s+(?:github\s+)?private)\b|\b(?:to|vao|len|tren)\s+(?:the\s+)?private\s+(?:repo|repository|remote)\b/;
557
+ function declaredReadOnly(promptText, releasePolicy) {
558
+ if (releasePolicy) return false;
559
+ const plain = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
560
+ .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
561
+ if (!READ_ONLY_DECLARATION.test(plain)) return false;
562
+ const text = neutralizeNonOrders(plain);
563
+ const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
564
+ .map((clause) => clause.split(/\bwithout\b/)[0])
565
+ .filter((clause) => !/\b(?:do not|don't|never|khong)\b|^[\s*+•>\d.)-]*(?:(?:vui long|xin|hay|please)\s+)*dung\b/.test(clause));
566
+ return /\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|investigation|inventory|forensic|root cause|kiem tra|kiem ke|phan tich|dieu tra|doc log|tim hieu)\b/.test(text.replace(/\bread[\s_-]*only\b/g, 'task mode'))
567
+ && !hasExplicitOrder(clauses)
568
+ && !ordersChangeAcrossClauses(clauses);
569
+ }
570
+
571
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:(?:keep|continue|tiep tuc)\s+)?(?:verify|re-?run|confirm|xac nhan|validate|double-?check|wait(?:ing)?|monitor|watch|cho(?=\s+(?:dung\s+)?(?:job|verifier|build|test|suite|gate|ci|run|task|ket qua|xong|output)\b))\b/;
572
+ 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/;
573
+ function isVerificationOnlyRequest(promptText) {
574
+ const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
575
+ .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
576
+ const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b|\broi\b)\s*/)
577
+ .map((clause) => clause.split(/\bwithout\b/)[0]);
578
+ return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
579
+ && !clauses.some((clause) => CHANGE_LEAD.test(clause));
580
+ }
581
+
582
+ function neutralizeBannedLists(text) {
583
+ const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin|commit|push)';
584
+ const LIST = `${OP}(?:\\s*(?:,|/|\\band\\b|\\bor\\b|\\bva\\b|\\bhoac\\b|\\balso\\b)\\s*${OP})*`;
585
+ const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
586
+ const LIFT = /\b(?:was|were|previously|before|formerly|no\s+longer|even\s+though|although|though|now\s+allows?|now\s+permits?|owner\s+allows?|khong\s+bi|da\s+duoc\s+phep|except)\b/;
587
+ const span = new RegExp(`\\b(?:(?:forbidden|prohibited|banned)\\s*:\\s*${LIST}|${LIST}\\s*:?\\s*(?:(?:is|are|remains?|van|con)\\s+)?(?:both\\s+)?${BAN})\\b`, 'g');
588
+ return text.replace(span, (m, offset, whole) => {
589
+ const start = Math.max(whole.lastIndexOf('.', offset), whole.lastIndexOf(';', offset), whole.lastIndexOf('\n', offset)) + 1;
590
+ const stops = [whole.indexOf('.', offset + m.length), whole.indexOf(';', offset + m.length), whole.indexOf('\n', offset + m.length)].filter((i) => i >= 0);
591
+ const end = stops.length ? Math.min(...stops) : whole.length;
592
+ return LIFT.test(whole.slice(start, end)) ? m : ' ';
593
+ });
594
+ }
595
+
596
+ function classifyTaskSemantics(input = {}) {
597
+ return analyzeTaskIntent(input).semantic;
598
+ }
599
+
600
+ function actionableClauses(promptText) {
601
+ const text = neutralizeBannedLists(neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()));
602
+ const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
603
+ const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong)\b|^[\s*+•>\d.)-]*(?:(?:vui long|xin|hay|please)\s+)*dung\b/.test(clause)
604
+ && !/^[\s*+•>\d.)-]*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause)
605
+ // A fragment led by a subordinator continues the previous clause's predicate ("…, including source
606
+ // fixes F-3" is prose about the past, never a new imperative).
607
+ && !/^[\s*+•>\d.)-]*(?:including|such as|e\.g\.|i\.e\.|via|per)\b/.test(clause));
608
+ return { text, clauses, actionable };
609
+ }
610
+
611
+ // Positive additive-order signal (add/create/bo sung ... <code or test artifact>) for callers that must not treat the
612
+ // classifier's CODE_IMPLEMENTATION default as an order (a bare "ok" is that default, not a change order).
613
+ function ordersAdditiveChange(promptText = '') {
614
+ return actionableClauses(publicationScope(promptText).text).actionable.some((clause) => ADDITIVE_ORDER_LEAD.test(clause));
615
+ }
616
+
617
+ function classifySemanticText({ promptText = '', targetFile = '' } = {}) {
618
+ promptText = publicationScope(promptText).text;
619
+ const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
620
+ const preflight = releaseText.match(/\b(?:yarn\s+release:verify|npm\s+run\s+release:verify)\b/)?.[0];
621
+ const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
622
+ && /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
623
+ ? { kind: 'conditional-release', preflight } : null;
624
+ const { text, clauses, actionable } = actionableClauses(promptText);
625
+ if (ordersPublicationText(promptText)) {
626
+ const implementation = hasExplicitOrder(clauses.filter((clause) => !/\b(?:release|publish)\b/.test(clause)));
627
+ const remaining = clauses.filter((clause) => !ordersPublicationText(clause) && !/\b(?:release|publish)\b/.test(clause)).join('; ');
628
+ const base = remaining.trim() ? classifySemanticText({ promptText: remaining, targetFile }) : {};
629
+ delete base.verificationOnly;
630
+ delete base.statusReport;
631
+ return { ...base, taskKind: implementation ? 'MIXED' : 'OPERATIONAL', publication: true, ...(releasePolicy ? { releasePolicy } : {}) };
632
+ }
633
+ if (!/[a-z0-9]/.test(text)) return { taskKind: 'READ_ONLY' };
634
+ if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY', ...(isVerificationOnlyRequest(promptText) ? { verificationOnly: true } : {}) };
635
+ const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
636
+ && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
637
+ const codeChangeVerb = actionable.some((clause) => !/^(?:run\s+|execute\s+)?git\s+/.test(clause) && /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
638
+ const reportArtifact = String(promptText).match(/(?:^|\s)((?:docs\/|reports\/)[A-Za-z0-9_./-]+\.(?:md|txt))(?=\s|[.,;:]|$)/)?.[1];
639
+ const documentation = !sourceOrder && !/\b(?:implement|fix|refactor|modify|edit|update|sua|chinh)\b/.test(actionable.join(' ')) && reportArtifact && actionable.some((clause) => /\b(?:write|create|save|viet|tao|luu)\b/.test(clause))
640
+ && /\b(?:report|bao cao|analysis|phan tich|document)\b/.test(text);
641
+ const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
642
+ const cronOperation = actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy|ensure|dam bao)\b/.test(clause)
643
+ && /\bcron\b|scheduled\s+(?:cron\s+)?(?:task|job)|tac vu\s+(?:cron|dinh ky)/.test(clause))
644
+ && (!codeChangeVerb || hasOperationId);
645
+ const operationNotOrdered = (clause) => {
646
+ const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin|commit|push)';
647
+ const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
648
+ if (/\b(?:was|were|previously|before|formerly|no\s+longer|even\s+though|although|though|now\s+allows?|now\s+permits?|owner\s+allows?|khong\s+bi|da\s+duoc\s+phep|except)\b/.test(clause)) return false;
649
+ if (/^[\s*+•>\d.)-]*(?:production|prod|staging)\s+(?:restart|redeploy|deploy(?:ment)?)\s*$/.test(clause)) return true;
650
+ const gap = `(?:(?!\\b(?:which|that|who|where|when|if|and|but|then)\\b|${OP})[^.;,()\\-:]){0,40}?`;
651
+ const banAfter = new RegExp(`^${gap}\\b(?:(?:is|are|remains?|van|con)\\s+)?${BAN}\\b(?!\\s+(?:to|from)\\b)`);
652
+ const joined = new RegExp(`^\\s*(?:/|,|and|or|va|hoac)\\s*${OP}\\b`);
653
+ const bannedAfter = (rest) => {
654
+ if (banAfter.test(rest)) return true;
655
+ const j = joined.exec(rest);
656
+ return j ? bannedAfter(rest.slice(j[0].length)) : false;
657
+ };
658
+ const banBefore = /\b(?:forbidden|prohibited|banned)\s+(?:(?:production|prod)\s+)?$/;
659
+ let total = 0;
660
+ let banned = 0;
661
+ for (const match of clause.matchAll(new RegExp(`\\b${OP}\\b`, 'g'))) {
662
+ total += 1;
663
+ if (bannedAfter(clause.slice(match.index + match[0].length)) || banBefore.test(clause.slice(0, match.index))) banned += 1;
664
+ }
665
+ return total > 0 && banned === total;
666
+ };
667
+ const gitOperation = actionable.some((clause) => !operationNotOrdered(clause)
668
+ && (GIT_ORDER_LEAD.test(clause) || /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause)));
669
+ const runtimeOperation = actionable.some((clause) => !operationNotOrdered(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)
670
+ && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
671
+ const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
672
+ const operation = Boolean(nativeDomain);
673
+ if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
674
+ const CONFIG_CHANGE_REPORT_CLAUSE = /\b(?:da|has|have|had|already)\s+(?:doi|thay doi|changed?|updated?)\b|\b(?:nao|gi|which|what|report|bao cao)\b[^.;]*?\b(?:doi|thay doi|changed?|updated?)\b/;
675
+ const CONFIG_MUTATION_VERB = /\b(?:set|change|toggle|enable|disable|switch|turn|configure|modify|update|edit|doi(?!\s+chieu\b)|bat(?!\s+dau\b)|tat(?!\s+ca\b)|chinh)\b/;
676
+ const CONFIG_NOUN = /\b(?:configuration|config|toggle|cau hinh)\b/;
677
+ const CONFIG_REPORT_FRAME = /^\s*(?:report|show|tell|list|describe|explain|summari[sz]e|bao cao|cho biet|liet ke|mo ta)\b/;
678
+ const CONFIG_QUESTION_FRAME_PREFIX = /\b(?:what|which|how|report|bao cao|nao|gi)\b/;
679
+ const config = !sourceOrder && actionable.some((clause) => {
680
+ if (!CONFIG_NOUN.test(clause) || !CONFIG_MUTATION_VERB.test(clause)) return false;
681
+ if (CONFIG_REPORT_FRAME.test(clause)) return false;
682
+ const verb = CONFIG_MUTATION_VERB.exec(clause);
683
+ if (CONFIG_QUESTION_FRAME_PREFIX.test(clause.slice(0, verb.index))) return false;
684
+ const noun = CONFIG_NOUN.exec(clause);
685
+ return !CONFIG_CHANGE_REPORT_CLAUSE.test(clause) || Boolean(noun && noun.index > verb.index);
686
+ });
687
+ const repository = Boolean(targetFile) || /[\w.-]+\.(?:json|ya?ml|toml|ini)\b/.test(text);
688
+ 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);
689
+ const ACTIONABLE_MUTATION_ORDER = /(?<!\b(?:what|which|how|nao|gi)\s)(?<!thay )\b(?:implement|fix(?:es)?|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/;
690
+ const configReportQuestion = !actionable.some((clause) => ACTIONABLE_MUTATION_ORDER.test(clause))
691
+ && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh|tham so)\b/.test(clause)
692
+ && CONFIG_CHANGE_REPORT_CLAUSE.test(clause));
693
+ const statusReport = (configReportQuestion || (!mutationWord
694
+ && /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text)))
695
+ && !operation && !config;
696
+ 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)
697
+ // An additive imperative (add/create/build/them/bo sung/tao) leading its own clause is an implementation order even
698
+ // when a sibling clause only reviews or checks, provided its object is a code/test/check artifact (a report such as
699
+ // "create a timeline" stays read-only); descriptive mentions never lead a clause.
700
+ && !sourceOrder && !codeChangeVerb && !operation && !config && !ordersChangeAcrossClauses(actionable)
701
+ && !actionable.some((clause) => ADDITIVE_ORDER_LEAD.test(clause));
702
+ const taskKind = operation ? (sourceOrder || codeChangeVerb ? 'MIXED' : 'OPERATIONAL')
703
+ : config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
704
+ const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
705
+ return { taskKind, ...(releasePolicy ? { releasePolicy } : {}), ...(documentation ? { reportArtifact } : {}), ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
706
+ ...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
707
+ ...(operation && nativeDomain === 'scheduled-job' ? { operationGoal: { domain: 'scheduled-job', target: id, desiredState: /\b(?:delete|remove|cancel|xoa|huy|absent|removed|deleted|no longer|khong con)\b/.test(actionable.join(' ')) ? 'absent' : 'present',
708
+ completionSemantics: /\b(?:ensure|dam bao)\b/.test(text)
709
+ || actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy)\b/.test(clause)
710
+ && /\b(?:if\s+(?:it\s+)?(?:still\s+)?exists|if\s+present|neu\s+(?:van\s+|con\s+)?ton tai)\b/.test(clause))
711
+ ? 'ensure-state' : 'execute-and-verify' } } : {}) };
712
+ }
713
+
714
+ function applyTaskSemantics(contract, semantic) {
715
+ const evidence = contract?.completionEvidence || [];
716
+ if (semantic.taskKind === 'READ_ONLY' && semantic.conditionalObligations?.length) {
717
+ return { ...(contract || {}), ...semantic, statusReport: false, completionEvidence: ['verification-evidence'] };
718
+ }
719
+ if (semantic.publication) return { ...(contract || {}), ...semantic, completionEvidence: semantic.taskKind === 'MIXED'
720
+ ? [...new Set([...evidence.filter((item) => item !== 'impact-evidence'), 'write-evidence', ...(semantic.operationGoal ? ['operation-verification'] : ['verification-evidence']), 'release-publication'])] : ['release-publication'] };
721
+ const operationOnly = semantic.taskKind === 'OPERATIONAL'
722
+ || (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
723
+ return { ...(contract || {}), ...semantic,
724
+ completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
725
+ : semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
726
+ : semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
727
+ : semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly ? ['verification-evidence'] : ['impact-evidence'])
728
+ // A routed contract owns the debt; an unrouted (informational/null) turn carries none, because
729
+ // CODE_IMPLEMENTATION is the classifier's default kind, not proof that a change was ordered.
730
+ : semantic.taskKind === 'CODE_IMPLEMENTATION' && contract ? [...new Set([...evidence, 'write-evidence', 'verification-evidence'])] : evidence };
731
+ }
732
+ // END GENERATED SEMANTIC ENGINE
212
733
  const STOPWORDS = new Set([
213
734
  'the', 'a', 'an', 'and', 'or', 'to', 'for', 'of', 'with', 'in', 'on', 'is', 'are',
214
735
  'this', 'that', 'it', 'as', 'by', 'be', 'use', 'using', 'implement', 'fix', 'task',
215
736
  'cần', 'và', 'là', 'cho', 'một', 'những', 'dùng',
216
737
  ]);
217
738
 
218
- // BUG-C21-04: every fs call in this deadline-guarded block is async — a sync
219
- // read/stat/scan on a stalled mount parks the event loop and the unref'd
220
- // deadline above can never fire (orphan node grandchild).
221
739
  const fsp = fs.promises;
222
740
 
223
741
  async function pathExists(filePath) {
@@ -237,9 +755,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
237
755
  }
238
756
 
239
757
  async function writeJsonFileAtomic(filePath, text) {
240
- // tmp+rename: readers see old-or-new, never a torn/half file. A truncated
241
- // skill-router-state.json parses as `{}` downstream and completion-gate.sh then
242
- // releases a premature Stop with no message — the mid-run silent-stop class.
243
758
  await ensureDir(filePath);
244
759
  const tempPath = `${filePath}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
245
760
  try {
@@ -363,12 +878,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
363
878
  };
364
879
  }
365
880
 
366
- // TASK-004 (BL-006): shared route resolver — the same module route-task.mjs
367
- // uses, so hook state and helper state compute identical rigor/fastPath/
368
- // riskFloor/resumable/decision-shadow fields. Loaded lazily via pathToFileURL
369
- // (cache-utils pattern); any load/derive failure degrades to null so routing
370
- // never breaks over advisory fields. Path is resolved lazily because
371
- // `projectRoot` is declared below (TDZ).
372
881
  let routeResolverModule = null;
373
882
  let routeResolverLoadAttempted = false;
374
883
  async function loadRouteResolver() {
@@ -386,9 +895,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
386
895
  return routeResolverModule;
387
896
  }
388
897
 
389
- // TASK-007 (BL-009): playbook registry — same lazy pathToFileURL pattern as
390
- // the shared resolver. Loaded once per hook run; failure degrades to null so
391
- // routing never blocks on playbook resolution.
392
898
  let playbookRegistryModule = null;
393
899
  let playbookRegistryLoadAttempted = false;
394
900
  async function loadPlaybookRegistry() {
@@ -406,9 +912,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
406
912
  return playbookRegistryModule;
407
913
  }
408
914
 
409
- // BL-019 (SPEC FR-001): measured tier module — same lazy pathToFileURL
410
- // pattern. A missing module/failure degrades to null so the static map
411
- // stays authoritative and routing never blocks on the artifact read.
412
915
  let tierSelectionModule = null;
413
916
  let tierSelectionLoadAttempted = false;
414
917
  async function loadTierSelection() {
@@ -426,8 +929,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
426
929
  return tierSelectionModule;
427
930
  }
428
931
 
429
- // Tier lane: who performs the edit for the routed task (worker hand-off vs inline) for the MAIN
430
- // session's tier. Same lazy pattern; a missing module degrades to no tier lane (legacy behavior).
431
932
  let tierLaneModule = null;
432
933
  let tierLaneLoadAttempted = false;
433
934
  async function loadTierLane() {
@@ -444,8 +945,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
444
945
  }
445
946
  return tierLaneModule;
446
947
  }
447
- // The persisted `tierLane` is what the Stop-side playbook acceptance reads (delegationRequired);
448
- // the printed lines are what tell the model to dispatch the worker and tag its prompt.
449
948
  function writeTierLaneLines(summary, taskId) {
450
949
  const lane = summary?.tierLane;
451
950
  if (!lane?.instruction) {
@@ -456,8 +955,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
456
955
  process.stdout.write(`[ukit-skill-router] Handback: start each worker prompt with "[ukit-task ${taskId}]" so its result binds to this task\n`);
457
956
  const hostHarness = process.env.UKIT_HARNESS || 'claude-code';
458
957
  if (summary.playbookId === 'feature-implementation' && hostHarness !== 'claude-code') {
459
- // omp/codex expose no agent identity to hooks: the gate cannot refuse orchestrator writes there,
460
- // so say what is actually enforced instead of promising a refusal.
461
958
  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');
462
959
  } else if (summary.playbookId === 'feature-implementation') {
463
960
  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');
@@ -1872,188 +2369,6 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
1872
2369
 
1873
2370
  // What exactly was ordered, so a receipt for a DIFFERENT verb/service/probe cannot discharge it.
1874
2371
  // Unnamed specifics stay empty (any proven receipt of the domain counts); named ones are required.
1875
- const NOT_A_SERVICE = new Set(['it', 'this', 'that', 'them', 'the', 'all', 'everything', 'service', 'services', 'again', 'now', 'to', 'and', 'then']);
1876
- function nativeOperationGoal(domain, actionable, text) {
1877
- const goal = { domain, target: null, desiredState: 'verified', completionSemantics: 'execute-and-verify', requiredTargets: [] };
1878
- const clauses = actionable.join(' . ');
1879
- if (domain === 'git-integrity') {
1880
- const verbs = [...clauses.matchAll(/\bgit\s+(push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b/g)].map((m) => m[1]);
1881
- if (/\bpush\s+(?:to\s+)?origin\b/.test(clauses)) verbs.push('push');
1882
- goal.requiredTargets = [...new Set(verbs)];
1883
- goal.check = /\bfsck\b/.test(text) ? 'fsck' : /\bdiff\s+--check\b/.test(text) ? 'diff-check' : null;
1884
- return goal;
1885
- }
1886
- const services = [...clauses.matchAll(/\b(?:restart|redeploy|deploy|khoi dong lai|trien khai)\s+(?:the\s+|lai\s+)?([a-z0-9][\w.-]*)/g)]
1887
- .map((m) => m[1]).filter((name) => !NOT_A_SERVICE.has(name));
1888
- goal.requiredTargets = [...new Set(services)];
1889
- const probePath = text.match(/(?:^|\s)(\/[a-z0-9_.-]+(?:\/[a-z0-9_.-]+)*)/)?.[1] || null;
1890
- const probeStatus = text.match(/\b(?:returns?|returning|responds?(?:\s+with)?|http|status(?:\s+code)?)\s+([1-5]\d\d)\b/)?.[1] || null;
1891
- if (probePath || probeStatus) goal.probe = { path: probePath, status: probeStatus };
1892
- return goal;
1893
- }
1894
-
1895
- // Task semantics are independent of source execution strategy and observed progress.
1896
- // P1-A (forensic false block): the clause splitter below cuts at commas, so a negation only reached the
1897
- // FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
1898
- // the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
1899
- // sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
1900
- // Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
1901
- // fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
1902
- const NON_ORDER_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|restart|sua|viet|chinh|them|tao|xoa';
1903
- // Declared verification purpose ("Mục tiêu: xác nhận ...", "Goal: verify ..."). Under that declaration a
1904
- // conditional repair/release branch ("Nếu phát hiện lỗi ... sửa ...", "Neu co source fix ... publish") is a
1905
- // branch the check MAY take, not an unconditional order, and disposable test-project setup ("Tạo project thử
1906
- // nghiệm độc lập") is scaffolding of the check. Unconditional orders in the same request keep blocking.
1907
- const VERIFICATION_GOAL = /(?:^|\n)[ \t*#_-]*(?:muc tieu|goal|purpose|objective)\s*:[^\n]*\b(?:xac nhan|verify|confirm|kiem tra|validate|double-?check|re-?run)\b/;
1908
- const CONDITIONAL_BRANCH = /(?:^|\n|\.\s+)[ \t]*(?:\d+[.)]\s*)?(?:neu\s+(?:phat hien\s+(?:false-block|loi|bug|defect)|co\s+(?:source fix|sua doi))|if\s+(?:(?:a\s+)?(?:bug|defect|failure)\s+is\s+found|(?:you\s+)?find\s+(?:a\s+)?(?:bug|defect)))\b[^\n]*?(?=\.\s+|$|\n)/g;
1909
- const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
1910
- const DISPOSABLE_SETUP = /\b(?:tao|create)\s+(?:mot\s+)?(?:project|du an)\s+(?:thu nghiem|test|disposable|doc lap)\b(?:\s+(?:doc lap|independent))?/g;
1911
- function neutralizeNonOrders(text) {
1912
- let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1913
- if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
1914
- return source
1915
- // A list item that DESCRIBES test cases ("Negative controls: yêu cầu thực sự chỉnh config/restart/deploy ...") is
1916
- // specification of the work, not an order; it runs to the next bullet or blank line.
1917
- .replace(/^[ \t]*(?:[-*+\u2022]|\d+[.)])[ \t]*(?:negative|positive)\s+controls?\b[^\n]*(?:\n(?![ \t]*(?:[-*+\u2022]|\d+[.)])|[ \t]*\n)[^\n]*)*/gm, ' ')
1918
- // A header that negates the list under it ("Authorization ... không cho phép:", "Prohibitions:", "not allowed:")
1919
- // makes every bullet an item of the prohibition, not an order.
1920
- .replace(/(?:^|(?<=[.;][ \t]))[^.;\n]*\b(?:khong\s+(?:cho\s+phep|duoc|lam)|(?:does|do|did|is|are)\s+not\s+(?:allow|permit|authori[sz]e)\w*|not\s+(?:allowed|permitted|authori[sz]ed)|prohibitions?|prohibited(?:\s+actions?)?|forbidden(?:\s+actions?)?|banned|must\s+not\s+do)\s*:[ \t]*(?:\n[ \t]*(?:[-*+\u2022]|\d+[.)])[^\n]*)+/gm, ' ')
1921
- .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
1922
- .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
1923
- .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
1924
- .replace(/\b(?:uu tien|prefer(?:red)?)\s+(?:a\s+|an\s+)?(?:configuration|config|cau hinh)\s+(?:change|changes|update|thay doi)\b/g, ' ')
1925
- .replace(/\b(?:neu\s+(?:chi\s+)?(?:phai|can|thieu|khong du|bang chung chua du)(?![^.;\n]*\b(?:restart|deploy|xoa|delete|remove|cancel)\b)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1926
- .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
1927
- .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, ' ')
1928
- .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
1929
- .replace(/\bwrite\s+permission\b/g, 'permission')
1930
- .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
1931
- .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
1932
- }
1933
-
1934
- 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)';
1935
- const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
1936
- 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`);
1937
- function hasExplicitOrder(clauses) {
1938
- return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
1939
- }
1940
- // Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
1941
- // confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
1942
- // "template/install", "source change") as mutation orders. Order is structural: a request whose
1943
- // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
1944
- // change-leading clause, orders no change - it owes verification evidence, not a write.
1945
- const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
1946
- 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/;
1947
- function isVerificationOnlyRequest(promptText) {
1948
- const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
1949
- .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1950
- const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
1951
- .map((clause) => clause.split(/\bwithout\b/)[0]);
1952
- return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
1953
- && !clauses.some((clause) => CHANGE_LEAD.test(clause));
1954
- }
1955
-
1956
- function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
1957
- // A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
1958
- const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
1959
- const preflight = releaseText.match(/\b(?:yarn\s+release:verify|npm\s+run\s+release:verify)\b/)?.[0];
1960
- const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
1961
- && /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
1962
- ? { kind: 'conditional-release', preflight } : null;
1963
- const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
1964
- const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
1965
- // Folded đúng (correctly) is also "dung"; only command-position đừng negates an order.
1966
- const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong)\b|^[\s*+•>\d.)-]*(?:(?:vui long|xin|hay|please)\s+)*dung\b/.test(clause)
1967
- && !/^[\s*+•>\d.)-]*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
1968
- const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
1969
- && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
1970
- const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
1971
- const reportArtifact = String(promptText).match(/(?:^|\s)((?:docs\/|reports\/)[A-Za-z0-9_./-]+\.(?:md|txt))(?=\s|[.,;:]|$)/)?.[1];
1972
- const documentation = !sourceOrder && !/\b(?:implement|fix|refactor|modify|edit|update|sua|chinh)\b/.test(actionable.join(' ')) && reportArtifact && actionable.some((clause) => /\b(?:write|create|save|viet|tao|luu)\b/.test(clause))
1973
- && /\b(?:report|bao cao|analysis|phan tich|document)\b/.test(text);
1974
- const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
1975
- // A cron/job MENTION is not an operation: with a code-change verb in the request, an
1976
- // operation needs a concrete target id; otherwise it is a coding task about scheduling.
1977
- const cronOperation = actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy|ensure|dam bao)\b/.test(clause)
1978
- && /\bcron\b|scheduled\s+(?:cron\s+)?(?:task|job)|tac vu\s+(?:cron|dinh ky)/.test(clause))
1979
- && (!codeChangeVerb || hasOperationId);
1980
- // Non-source operations (git history/push, service restart/deploy): ordered only by an
1981
- // actionable clause (negated/deferred/audit clauses are already filtered out). Git and
1982
- // runtime are separate domains with separate proof; a prompt naming both stays a coding
1983
- // task rather than being half-verified. A code-change verb anywhere keeps write evidence.
1984
- 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));
1985
- // 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
1986
- // when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
1987
- // "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
1988
- 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)
1989
- && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
1990
- const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
1991
- const operation = Boolean(nativeDomain);
1992
- // A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
1993
- if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
1994
- // A config MENTION is not a config change: the same clause needs a mutation verb, and a
1995
- // request that also changes source/code is a coding task (config is part of its tests/fixtures).
1996
- // A DIAGNOSTIC report of past changes ("which parameters/configuration has changed, if any" /
1997
- // "tham so/cau hinh nao da thay doi, neu co") is a question about history, never an authorization
1998
- // to mutate: only a forward order ("change X to Y") stays a config mutation.
1999
- const CONFIG_CHANGE_REPORT_CLAUSE = /\b(?:da|has|have|had|already)\s+(?:doi|thay doi|changed?|updated?)\b|\b(?:nao|gi|which|what|report|bao cao)\b[^.;]*?\b(?:doi|thay doi|changed?|updated?)\b/;
2000
- // A relative past-change clause inside a FORWARD imperative ("Set the config that
2001
- // has changed to dark mode") names WHICH config — it does not make the sentence a
2002
- // report. The override holds only when the clause's FIRST mutation word precedes
2003
- // the config noun AND no reporting/question frame opens the clause or precedes
2004
- // that word: in "Report what change was made to the config" the mutation word is
2005
- // a question's noun, never an order.
2006
- const CONFIG_MUTATION_VERB = /\b(?:set|change|toggle|enable|disable|switch|turn|configure|modify|update|edit|doi(?!\s+chieu\b)|bat(?!\s+dau\b)|tat(?!\s+ca\b)|chinh)\b/;
2007
- const CONFIG_NOUN = /\b(?:configuration|config|toggle|cau hinh)\b/;
2008
- const CONFIG_REPORT_FRAME = /^\s*(?:report|show|tell|list|describe|explain|summari[sz]e|bao cao|cho biet|liet ke|mo ta)\b/;
2009
- const CONFIG_QUESTION_FRAME_PREFIX = /\b(?:what|which|how|report|bao cao|nao|gi)\b/;
2010
- const config = !sourceOrder && actionable.some((clause) => {
2011
- if (!CONFIG_NOUN.test(clause) || !CONFIG_MUTATION_VERB.test(clause)) return false;
2012
- if (CONFIG_REPORT_FRAME.test(clause)) return false;
2013
- const verb = CONFIG_MUTATION_VERB.exec(clause);
2014
- if (CONFIG_QUESTION_FRAME_PREFIX.test(clause.slice(0, verb.index))) return false;
2015
- const noun = CONFIG_NOUN.exec(clause);
2016
- return !CONFIG_CHANGE_REPORT_CLAUSE.test(clause) || Boolean(noun && noun.index > verb.index);
2017
- });
2018
- const repository = Boolean(targetFile) || /[\w.-]+\.(?:json|ya?ml|toml|ini)\b/.test(text);
2019
- // A status/result lookup ("check the suite output and report") orders no change: it needs a
2020
- // lookup verb AND no mutation vocabulary anywhere in the request (fail-closed otherwise).
2021
- 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);
2022
- // A bounded, recognizable reporting QUESTION about what changed ("Tham số/cấu hình
2023
- // nào đã thay đổi, nếu có." / "which configuration has changed, if any") is a
2024
- // status report even without a lookup verb — but ONLY when no actionable clause
2025
- // actually orders a mutation: a mixed request ("Add a dark-mode button; report
2026
- // which configuration changed") keeps its implementation contract and evidence.
2027
- // The two lookbehinds keep a question's noun ("what change was made") and the
2028
- // Vietnamese past-tense verb "thay đổi" from counting as mutation orders.
2029
- const ACTIONABLE_MUTATION_ORDER = /(?<!\b(?:what|which|how|nao|gi)\s)(?<!thay )\b(?:implement|fix(?:es)?|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/;
2030
- const configReportQuestion = !actionable.some((clause) => ACTIONABLE_MUTATION_ORDER.test(clause))
2031
- && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh|tham so)\b/.test(clause)
2032
- && CONFIG_CHANGE_REPORT_CLAUSE.test(clause));
2033
- const statusReport = (configReportQuestion || (!mutationWord
2034
- && /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text)))
2035
- && !operation && !config;
2036
- 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)
2037
- && !sourceOrder && !codeChangeVerb && !operation && !config;
2038
- const taskKind = operation ? (sourceOrder || (nativeDomain !== 'scheduled-job' && codeChangeVerb) ? 'MIXED' : 'OPERATIONAL')
2039
- : config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
2040
- const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
2041
- return { taskKind, ...(releasePolicy ? { releasePolicy } : {}), ...(documentation ? { reportArtifact } : {}), ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
2042
- ...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
2043
- ...(operation && nativeDomain === 'scheduled-job' ? { operationGoal: { domain: 'scheduled-job', target: id, desiredState: /\b(?:delete|remove|cancel|xoa|huy|absent|removed|deleted|no longer|khong con)\b/.test(actionable.join(' ')) ? 'absent' : 'present',
2044
- completionSemantics: /\b(?:ensure|dam bao)\b/.test(text) ? 'ensure-state' : 'execute-and-verify' } } : {}) };
2045
- }
2046
-
2047
- function applyTaskSemantics(contract, semantic) {
2048
- const evidence = contract?.completionEvidence || [];
2049
- const operationOnly = semantic.taskKind === 'OPERATIONAL'
2050
- || (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
2051
- return { ...(contract || {}), ...semantic,
2052
- completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
2053
- : semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
2054
- : semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
2055
- : semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
2056
- }
2057
2372
 
2058
2373
  function buildRouteSummary({
2059
2374
  activeSkills = [],
@@ -2139,7 +2454,6 @@ function applyTaskSemantics(contract, semantic) {
2139
2454
  nextActionType: nextAction?.type || null,
2140
2455
  nextActionCommand,
2141
2456
  helperHint,
2142
- // FR-004 additive field — present only for advisor/reference/replan prompts.
2143
2457
  ...(advisorIntent ? { advisorIntent } : {}),
2144
2458
  line: line || 'task=unknown',
2145
2459
  };
@@ -2236,16 +2550,9 @@ function applyTaskSemantics(contract, semantic) {
2236
2550
  const questionSignal = raw.includes('?')
2237
2551
  || /\b(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
2238
2552
  || /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu|nghĩa là|được không|không\b)/.test(raw);
2239
- // '?' or a leading/Vietnamese interrogative — required to overrule error wording.
2240
2553
  const strongQuestion = raw.includes('?')
2241
2554
  || /^(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
2242
2555
  || /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu)/.test(raw);
2243
- // C85-019: consult-shaped questions overrule subject-matter signal scores.
2244
- // "should I fix X?", "X chưa nhỉ?", "có nên/có cần sửa không?" mention
2245
- // mutation vocabulary as SUBJECT — the ask is a decision/answer, not an
2246
- // order. Tight on purpose: modal-subject inversion restricted to
2247
- // should/shall (decision asks) — "how do I fix…?" and "bạn có thể sửa…
2248
- // giúp tôi không?" are work-shaped questions, not consults.
2249
2556
  const folded = raw.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\u0111/g, 'd');
2250
2557
  const politeAsk = /\b(?:giup|giuong|ho|dum)\b[^\n?,;]{0,60}\b(?:khong|không)\s*[?!.]?\s*$/.test(folded)
2251
2558
  || /\bcho\b[^\n?,;]{0,60}\b(?:khong|không)\s*[?!.]?\s*$/.test(folded);
@@ -2254,8 +2561,6 @@ function applyTaskSemantics(contract, semantic) {
2254
2561
  || /\b(?:chua|duoc khong|dung khong|khong nhi|nhi|roi)\s*[.!?]*\s*$/.test(folded)
2255
2562
  || (!politeAsk && /\b(?:khong|không)\s*[?.!]?\s*$/.test(folded))
2256
2563
  || /\bchi\s+(?:hoi|tu\s+van|giai\s+thich|xem)\b/.test(folded);
2257
- // P1: a consult-shaped clause ("Should I also bump…?", a Vietnamese tail) must never demote a prompt that
2258
- // still carries a real imperative order — otherwise the order loses its completion contract.
2259
2564
  if (consultShape && hasMutationOrder({ promptText, commandText })) return false;
2260
2565
  if (scores) {
2261
2566
  if (!consultShape && (
@@ -2272,26 +2577,16 @@ function applyTaskSemantics(contract, semantic) {
2272
2577
  return false;
2273
2578
  }
2274
2579
  }
2275
- // C85-019: consult-shape also overrides the vocabulary vetoes — the
2276
- // mutation/debug/lỗi words are the question's SUBJECT, not the ask.
2277
2580
  const implementWords = /(?<![A-Za-z0-9_])(implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|sửa|thêm|tạo|xóa|đổi|thay thế|cập nhật|viết|build|chạy|cài)(?![A-Za-z0-9_])/.test(raw);
2278
2581
  if (implementWords && !consultShape) return false;
2279
2582
  const investigationWords = /\b(why|debug|triage|root cause|investigate|tại sao)\b/.test(raw);
2280
2583
  if (investigationWords && !consultShape) return false;
2281
- // \b never matches around 'lỗi' (diacritics are not \w), so test it as a plain
2282
- // substring: an error report is informational only when phrased as a question.
2283
2584
  if (raw.includes('lỗi') && !strongQuestion && !consultShape) return false;
2284
2585
  const reviewWords = /\b(review|audit|verify)\b/.test(raw);
2285
2586
  if (reviewWords && !consultShape) return false;
2286
- // C85-019: consult shape is itself a question signal (Vietnamese tails like
2287
- // "chưa"/"rồi" never match the Latin interrogative regex above).
2288
2587
  return questionSignal || consultShape;
2289
2588
  }
2290
2589
 
2291
- // Greeting fast-path (parity with route-task.mjs isPureGreetingPrompt): a
2292
- // bare "hi" / "xin chào" / "thanks" (≤6 tokens, no command, no target) is an
2293
- // informational turn — no mutating lane, no task debt. Any non-greeting
2294
- // word ("hi, fix the login bug") disqualifies the prompt.
2295
2590
  function isPureGreetingPrompt({ promptText = '', commandText = '', targetFile = null } = {}) {
2296
2591
  const raw = String(promptText ?? '').trim();
2297
2592
  if (!raw || String(commandText ?? '').trim() || targetFile) return false;
@@ -2320,21 +2615,6 @@ function applyTaskSemantics(contract, semantic) {
2320
2615
  return coreHit;
2321
2616
  }
2322
2617
 
2323
- // TASK-C85-019: an explicit no-change/advisory assertion overrides the signal
2324
- // scores. A report or advisory question carrying fix vocabulary as SUBJECT
2325
- // MATTER ("không tạo thay đổi", "giữ nguyên mã", "do not change anything",
2326
- // "chỉ tư vấn") still fires implementSignal/smallFixSignal/failureSignal,
2327
- // which used to route the turn mutating so the completion gate demanded an
2328
- // Edit the user explicitly declined. The override fires only when an
2329
- // assertion phrase is present AND no unambiguous implement order survives
2330
- // outside the negated/advisory/quoted/report clauses: "sửa file X nhưng giữ
2331
- // nguyên Y" keeps its mutation lane because 'sửa file X' is outside the
2332
- // keep-clause, while "hook cần được cấu hình để chỉ yêu cầu bằng chứng sửa
2333
- // file" is a description of the hook's behavior, not an order. Folded
2334
- // (diacritic-free) text is matched so both Vietnamese spellings work.
2335
- // Interrogative-deontic forms ("should I…?", "có cần sửa…?") count as
2336
- // advisory requests, not orders; a bare "không biết…chưa" uncertainty is NOT
2337
- // an assertion and stays on the plain question path.
2338
2618
  function hasAdvisoryOnlyAssertion({ promptText = '', commandText = '' } = {}) {
2339
2619
  const folded = `${promptText ?? ''}\n${commandText ?? ''}`
2340
2620
  .toLowerCase()
@@ -2358,13 +2638,6 @@ function applyTaskSemantics(contract, semantic) {
2358
2638
  || /\bdung\s+(?:sua|thay\s+doi|edit|chinh)\b/.test(trimmed)
2359
2639
  || /\bchi\s+(?:tu\s+van|hoi|giai\s+thich|xem|la\s+cau\s+hoi)\b/.test(trimmed)
2360
2640
  || /\bco\s+(?:can|nen)\s+sua\b/.test(trimmed)
2361
- // C89-008 (user-reported 9-stop loop): a waiting/no-change turn asserts
2362
- // the answer is advisory — "đang chờ kết quả test QAS, không có gì cần
2363
- // sửa" — but matches none of the shapes above and is not question-shaped,
2364
- // so it escaped every informational gate and the stop hook demanded an
2365
- // Edit the honest turn could never produce. 'không có gì' needs its own
2366
- // trigger — the generic 'không (cần) sửa' pattern cannot skip the 'có
2367
- // gì' gap.
2368
2641
  || /\bkhong\s+co\s+gi\s+(?:can|phai|de|dang|con)\s+(?:sua|thay\s+doi|lam|chinh|edit|fix)\b/.test(trimmed)
2369
2642
  || /\bnothing\s+(?:to\s+(?:fix|edit|change|update|modify|do)|needs?\s+(?:fixing|editing|changing|updating))\b/.test(trimmed)
2370
2643
  || /\bdang\s+(?:cho|doi)\b/.test(trimmed)
@@ -2372,55 +2645,27 @@ function applyTaskSemantics(contract, semantic) {
2372
2645
  || /\bchua\s+(?:can|nen|phai)\s+(?:sua|thay\s+doi|lam|chinh|edit|fix)\b/.test(trimmed)
2373
2646
  || /\b(?:cho|doi)\s+(?:ket\s+qua|bao\s+cao|feedback|phe\s+duyet|duyet|phien\s+ban|ci|test|qas|review|build|deploy|approval|xong)\b/.test(trimmed);
2374
2647
  if (!advisoryAssertion) return false;
2375
- // Strip quoted spans and every clause class that can carry mutation
2376
- // vocabulary without being an order: the advisory/negated assertions
2377
- // themselves, conditional or purpose clauses (nếu/để/cho/if), reported
2378
- // demands of another agent (yêu cầu/ép/bắt/requires/forced), passive
2379
- // necessity descriptions (cần được cấu hình/must be configured), and the
2380
- // ledger's own evidence-class names. Whatever mutation verb remains after
2381
- // the strip is an actual implement order and vetoes the advisory route.
2382
- // C85-019c: clause strips must stop BEFORE a conjunction-joined imperative
2383
- // — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
2384
- // order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
2385
- // TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
2386
2648
  const MUT_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh';
2387
2649
  const TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
2388
- // C89-008: WAIT_TAIL ends a waiting-clause strip before any coordinating
2389
- // conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything
2390
- // up to a mutation verb, or at punctuation/EOS — "đang chờ kết quả nhưng
2391
- // sửa file này" keeps 'sửa file này' as a surviving order.
2392
2650
  const WAIT_TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and|nhung|roi|then|later|sau\\s+do)\\s+[^.,;\\n]*?\\b(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
2393
2651
  const residue = trimmed
2394
2652
  .replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
2395
- // "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
2396
2653
  .replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
2397
2654
  .replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
2398
- // P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
2399
- // advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
2400
- // answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
2401
- // commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
2402
2655
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
2403
2656
  .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
2404
2657
  .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
2405
2658
  .replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
2406
2659
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
2407
- // A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
2408
- // verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
2409
2660
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
2410
2661
  .replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
2411
2662
  .replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
2412
2663
  .replace(new RegExp(`\\b(?:khong|chua)\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
2413
2664
  .replace(new RegExp(`\\bdung\\s+${TAIL}`, 'g'), ' ')
2414
- // C89-008: a waiting clause narrates what the TURN waits on, not what
2415
- // the request orders — verbs inside it ("chờ sửa xong", "waiting for
2416
- // the fix") belong to the wait, not to the implement order.
2417
2665
  .replace(new RegExp(`\\bdang\\s+(?:cho|doi)\\b${WAIT_TAIL}`, 'g'), ' ')
2418
2666
  .replace(new RegExp(`\\b(?:waiting|awaiting)\\s+(?:for|on)\\b${WAIT_TAIL}`, 'g'), ' ')
2419
2667
  .replace(new RegExp(`\\b(?:cho|doi)\\s+(?:ket\\s+qua|bao\\s+cao|feedback|phe\\s+duyet|duyet|phien\\s+ban|ci|test|qas|review|build|deploy|approval|xong)\\b${WAIT_TAIL}`, 'g'), ' ')
2420
2668
  .replace(new RegExp(`\\bnothing\\s+(?:to|needs?)\\b${TAIL}`, 'g'), ' ')
2421
- // 'chưa cần/nên/phải sửa' defers the change — same class as 'không cần
2422
- // sửa' but 'chưa' is not covered by the 'không' strip above. WAIT_TAIL
2423
- // bounds it so '…nhưng sửa phần này' keeps the surviving order.
2424
2669
  .replace(new RegExp(`\\bchua\\s+(?:can|nen|phai)\\s+${WAIT_TAIL}`, 'g'), ' ')
2425
2670
  .replace(new RegExp(`\\b(?:neu|if)\\s+${TAIL}`, 'g'), ' ')
2426
2671
  .replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
@@ -2437,77 +2682,49 @@ function applyTaskSemantics(contract, semantic) {
2437
2682
  return !mutationOrder;
2438
2683
  }
2439
2684
 
2440
- // C85-019: mutation vocabulary ≠ mutation order. The same residue logic the
2441
- // advisory gate uses — quotes, negated clauses, conditionals (nếu/để/cho/if),
2442
- // reported demands (yêu cầu/ép/requires), necessity phrases (cần được/must
2443
- // be), evidence-class names — decides whether ANY surviving verb is an
2444
- // imperative. Used by hasAdvisoryOnlyAssertion and by the question-shape
2445
- // gate below (question + no order → consult).
2446
2685
  function hasMutationOrder({ promptText = '', commandText = '' } = {}) {
2447
- const folded = `${promptText ?? ''}\n${commandText ?? ''}`
2686
+ const folded = `${publicationScope(promptText).text}\n${commandText ?? ''}`
2448
2687
  .toLowerCase()
2449
2688
  .normalize('NFD')
2450
2689
  .replace(/[\u0300-\u036f]/g, '')
2451
2690
  .replace(/\u0111/g, 'd');
2452
2691
  const trimmed = folded.trim();
2453
2692
  if (!trimmed) return false;
2454
- if (!String(commandText ?? '').trim() && classifyTaskSemantics({ promptText }).verificationOnly === true) return false;
2455
- // C85-019c: clause strips must stop BEFORE a conjunction-joined imperative
2456
- // — "giữ nguyên header và sửa lỗi login" keeps 'và sửa lỗi' as a surviving
2457
- // order; an unbounded [^.,;\n]* swallows it and the order escapes the gate.
2458
- // TAIL = lazy strip ending before 'và'/'and' (folded va|and) or EOS.
2693
+ if (ordersPublication(promptText)) return true;
2694
+ if (!String(commandText ?? '').trim() && (declaredReadOnly(promptText, classifyTaskSemantics({ promptText }).releasePolicy)
2695
+ || classifyTaskSemantics({ promptText }).verificationOnly === true)) return false;
2696
+ if (/^(?:we\s+need|i\s+want|can)\s+(?:a\s+|an\s+|mot\s+)?[^.;\n]*\b(?:feature|flag|option|tinh nang)\b/.test(trimmed)) return true;
2459
2697
  const MUT_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh';
2460
2698
  const TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
2461
- // C89-008: WAIT_TAIL ends a waiting-clause strip before a coordinating
2462
- // conjunction ('và/and/nhưng/rồi/then/later/sau đó') followed by anything
2463
- // up to a mutation verb, or at punctuation/EOS.
2464
2699
  const WAIT_TAIL = '[^.,;\\n]*?(?=\\s+(?:va|and|nhung|roi|then|later|sau\\s+do)\\s+[^.,;\\n]*?\\b(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])|[.,;]|\\s*$)';
2465
- const residue = trimmed
2700
+ const residue = neutralizeNonOrders(trimmed)
2466
2701
  .replace(/"[^"\n]*"|«[^»\n]*»/g, ' ')
2467
- // "tạo plan" / "create a plan": the plan is the deliverable, not a mutation order.
2468
2702
  .replace(/\b(?:tao|lap|viet|create|make|write|draft|produce)\s+(?:cho\s+toi\s+)?(?:a\s+|an\s+|the\s+|1\s+|mot\s+)?(?:one\s+)?(?:structured\s+|forensic\s+|short\s+|brief\s+)?(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|bao\s+cao|report|danh\s+gia|assessment)\b/g, ' ')
2469
- // Descriptive state, not an order: "đã/vừa [được] + verb" ("hooks vừa được cài đặt") reports
2470
- // something already done. Only the verb is stripped, so a later imperative still survives.
2471
2703
  .replace(/(?<![a-z0-9_])(?:da|vua)\s+(?:duoc\s+)?(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![a-z0-9_])/g, ' ')
2472
2704
  .replace(/\bwrite-evidence\b|\bverification-evidence\b|\bimpact-evidence\b|\broot-cause\b/g, ' ')
2473
- // P1-A (forensic false block): nouns and investigation questions are not orders. 'write/verification
2474
- // advisory', 'state change', 'each proposed fix' name things; 'whether X may create Y' asks a question to
2475
- // answer. A negated ENUMERATION with objects ('do not implement fixes, modify source code, ... or create
2476
- // commits') negates every item to its sentence end; the older verb-only chain broke at 'fixes'.
2477
2705
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
2478
2706
  .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
2479
2707
  .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
2480
2708
  .replace(new RegExp(`\\bwhether\\b[^.,;\\n]*?(?=\\s+(?:va|and)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])|[.,;\\n]|$)`, 'g'), ' ')
2481
2709
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh)(?![A-Za-z0-9_])(?:[^.,;\\n\u2014]*?(?:\\s*,\\s*(?:(?:or|and|nor|hay|hoac|va)\\s+)?|\\s+(?:or|nor|hay|hoac)\\s+)(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\\s+the|cap\\s+nhat|viet|chay|cai|chinh|trien\\s+khai|cau\\s+hinh|restart|manipulate|resume|push|merge|publish|touch|alter|reset|rollback|revert|commit|kill|release|tag)(?![A-Za-z0-9_]))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
2482
- // A negated LIST ("do not implement, fix, or refactor"; "khong implement, sua, refactor hay chinh") negates every
2483
- // verb in the list: the TAIL strip below stops at the first comma, which would leave the later verbs as orders.
2484
2710
  .replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
2485
2711
  .replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
2486
2712
  .replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
2487
2713
  .replace(new RegExp(`\\b(?:khong|chua)\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
2488
2714
  .replace(new RegExp(`\\bdung\\s+${TAIL}`, 'g'), ' ')
2489
- // C89-008: a waiting clause narrates what the TURN waits on, not what
2490
- // the request orders — verbs inside it belong to the wait.
2491
2715
  .replace(new RegExp(`\\bdang\\s+(?:cho|doi)\\b${WAIT_TAIL}`, 'g'), ' ')
2492
2716
  .replace(new RegExp(`\\b(?:waiting|awaiting)\\s+(?:for|on)\\b${WAIT_TAIL}`, 'g'), ' ')
2493
2717
  .replace(new RegExp(`\\b(?:cho|doi)\\s+(?:ket\\s+qua|bao\\s+cao|feedback|phe\\s+duyet|duyet|phien\\s+ban|ci|test|qas|review|build|deploy|approval|xong)\\b${WAIT_TAIL}`, 'g'), ' ')
2494
2718
  .replace(new RegExp(`\\bnothing\\s+(?:to|needs?)\\b${TAIL}`, 'g'), ' ')
2495
- // 'chưa cần/nên/phải sửa' defers the change — WAIT_TAIL bounds it so a
2496
- // '…nhưng sửa phần này' order survives.
2497
2719
  .replace(new RegExp(`\\bchua\\s+(?:can|nen|phai)\\s+${WAIT_TAIL}`, 'g'), ' ')
2498
2720
  .replace(new RegExp(`\\b(?:neu|if)\\s+${TAIL}`, 'g'), ' ')
2499
- // 'đối chiếu' (cross-check) folds to 'doi chieu' and is not the verb 'đổi' (change).
2500
2721
  .replace(/\bdoi\s+chieu\b/g, ' ')
2501
- // R2: a deferred/future clause ("chua sua", "how we would later implement", "R2 onward: ...",
2502
- // "fix strategy ...") describes work for a LATER round, not an order for this turn.
2503
2722
  .replace(new RegExp(`\\bchua\\s+(?:sua|fix|implement|trien\\s+khai|viet|thay\\s+doi)\\b${WAIT_TAIL}`, 'g'), ' ')
2504
2723
  .replace(/\bhow\s+(?:we|i|you|they)\s+(?:would|will|could|should|might)\b[^.,;\n?]*/g, ' ')
2505
2724
  .replace(/\b(?:would|will|could|might)\s+later\b[^.,;\n?]*/g, ' ')
2506
2725
  .replace(/\b(?:r\d+|round\s*\d+)\s+onward\b[^\n]*/g, ' ')
2507
2726
  .replace(/\bfix\s+strategy\b[^\n]*/g, ' ')
2508
2727
  // C85-019b: a "khi <subject> <verb>" when-clause is temporal context —
2509
- // "khi tôi cài hệ thống này" narrates WHEN something happens; the verb
2510
- // inside it is never the order.
2511
2728
  .replace(new RegExp(`\\bkhi\\s+(?:toi|ta|ban|no|anh|chi|he|she|we)\\s+[^.,;\\n?]*?(?=\\s+(?:va|and)\\b|\\s*$)`, 'g'), ' ')
2512
2729
  .replace(new RegExp(`\\b(?:de|cho)\\s+${TAIL}`, 'g'), ' ')
2513
2730
  .replace(new RegExp(`\\b(?:yeu\\s+cau|doi\\s+hoi|ep\\s+phai|bat\\s+buoc|bat\\s+phai)\\b${TAIL}`, 'g'), ' ')
@@ -2519,19 +2736,16 @@ function applyTaskSemantics(contract, semantic) {
2519
2736
  .replace(/\b(?:just|only|simply)\s+(?:advis\w*|asking?|a\s+question|answering?|explaining?|consulting|reporting)\b[^.,;\n]*/g, ' ')
2520
2737
  .replace(/\badvisory\s+only\b[^.,;\n]*/g, ' ')
2521
2738
  .replace(new RegExp(`\\bkeep\\s+[^\\n.,;]{0,60}\\b(?:unchanged|as\\s+is|the\\s+same|intact)\\b${TAIL}`, 'g'), ' ')
2522
- // C85-019b: question-tagged mutation verbs are asks, not orders.
2523
2739
  // English: "<modal> I/we <verb>" (should I fix). Vietnamese: S-V-order
2524
2740
  // status questions — "<verb> … (chưa|được không|rồi|nhỉ)?" means "has
2525
- // it been done", never "do it". EXCLUDED: "… giúp/cho/hộ/dùm tôi
2526
- // không?" — the 'không' is a polite request marker, not a status tag;
2527
- // also EXCLUDED are wh-led modals ("which files should I update") —
2528
- // a selection ask is a plan, not a yes/no consult. Strips run LAST
2529
- // so a real imperative mid-sentence ("sửa file X cho tôi") survives.
2530
2741
  .replace(/\b(?<!\b(?:which|what|how)\s[^.,;?]{0,30})(?:should|shall)\s+(?:i|we)\s+[^\n?]*[?]?/g, ' ')
2531
2742
  .replace(/[^\n.,;?]{0,80}\b(?:chua|duoc khong|dung khong|khong nhi|nhi|roi|a)\s*[?]?\s*$/g, ' ')
2532
2743
  .replace(/^(?!.*\b(?:giup|giuong|cho|ho|dum)\b)[^\n.,;?]{0,80}\b(?:khong|không)\s*[?]?\s*$/g, ' ')
2533
2744
  .replace(/\b(?:co|có)\s+(?:nen|can|phai)\s+[^\n?]*[?]?/g, ' ');
2534
- return /(?<![A-Za-z0-9_])(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![A-Za-z0-9_])/.test(residue);
2745
+ return /(?<![A-Za-z0-9_])(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![A-Za-z0-9_])/.test(residue)
2746
+ // The canonical engine is the authority on additive/imperative change orders (bo sung, bat buoc them ...)
2747
+ // that the residue stripping above treats as obligation prose; the copies must not disagree.
2748
+ || ordersAdditiveChange(promptText);
2535
2749
  }
2536
2750
 
2537
2751
  // <advisor-intent:begin>
@@ -2666,11 +2880,6 @@ function applyTaskSemantics(contract, semantic) {
2666
2880
  return 'informational';
2667
2881
  }
2668
2882
 
2669
- // TASK-C85-019: an explicit no-change/advisory assertion beats every signal
2670
- // score — the prompt mentions mutation vocabulary as subject matter, not as
2671
- // an order. Checked before the review/delivery/consultation gates so an
2672
- // advisory turn never mints write or verification debt; a surviving
2673
- // imperative outside the negated clause still routes mutating.
2674
2883
  if (hasAdvisoryOnlyAssertion({ promptText, commandText })) {
2675
2884
  return 'informational';
2676
2885
  }
@@ -2693,18 +2902,7 @@ function applyTaskSemantics(contract, semantic) {
2693
2902
  const questionShapedPrompt = /\?\s*$/.test(trimmedPromptText)
2694
2903
  || leadingInterrogative
2695
2904
  || /\b(?:la gi|duoc khong)\s*$/.test(foldedPromptText)
2696
- // C85-019: Vietnamese status/consult tails without a '?' — "… chưa",
2697
- // "… chưa nhỉ", "… không nhỉ", "… đúng không". A question about a defect
2698
- // must not mint the bug-fix floor's write/repro debt.
2699
2905
  || /\b(?:chua|duoc khong|dung khong|khong nhi|khong|nhi)\s*[.!?]*\s*$/.test(foldedPromptText);
2700
- // Implement verbs (Vietnamese included — the English-only scores above cannot see
2701
- // them) mark an action order even when phrased as a question ("ban sua giup toi…?" is
2702
- // an edit order, not a consultation). A leading interrogative overrides them:
2703
- // "Lam sao ma do duoc khi toi cai…?" asks HOW something is done, it does not order
2704
- // the action performed.
2705
- // C85-019: question shape + no surviving imperative = consult. The
2706
- // pre-C85-019 all-zero-scores clause consulted only when NOTHING in the
2707
- // prompt resembled work — impossible for a question about code. Replacing
2708
2906
  // it with hasMutationOrder keeps "sửa file cho tôi?" mutating while
2709
2907
  // "lỗi này chưa nhỉ" / "should I fix the hook?" consult. A targetFile
2710
2908
  // still vetoes.
@@ -3952,10 +4150,17 @@ function applyTaskSemantics(contract, semantic) {
3952
4150
  && !storedContract.statusReport && previous.routingContext?.lastExplicitUserPromptText) {
3953
4151
  try {
3954
4152
  const fresh = classifyTaskSemantics({ promptText: previous.routingContext.lastExplicitUserPromptText, targetFile: persisted.routingContext?.targetFile });
3955
- // Compare every classifier-owned obligation field, not just the kind: a stale operationGoal /
3956
- // configurationScope / releasePolicy under an unchanged kind is the same false-obligation bug.
3957
- const sig = (c) => JSON.stringify([c?.taskKind, c?.operationGoal ?? null, c?.configurationScope ?? null, c?.releasePolicy?.kind ?? null, c?.reportArtifact ?? null]);
3958
- if (fresh?.taskKind && sig(fresh) !== sig(storedContract)) semanticFromPrompt = fresh;
4153
+ // Always compare the complete semantic contract, including evidence and flags.
4154
+ // Partial signatures hid contradictions under an unchanged task kind.
4155
+ if (fresh?.taskKind) {
4156
+ const identity = semanticContractIdentity({ promptText: previous.routingContext.lastExplicitUserPromptText,
4157
+ targetFile: persisted.routingContext?.targetFile || '', taskInstanceId });
4158
+ const comparison = compareSemanticContracts({ stored: storedContract,
4159
+ fresh: applyTaskSemantics(buildExecutionContract(persisted.routeSummary?.executionMode || 'local-build'), fresh),
4160
+ previousIdentity: previous.semanticIdentity, identity });
4161
+ if (comparison.status === 'invariant-violation') persisted.semanticDiagnostic = comparison;
4162
+ else if (comparison.status !== 'equal') semanticFromPrompt = fresh;
4163
+ }
3959
4164
  } catch {}
3960
4165
  }
3961
4166
  const semantic = semanticFromPrompt
@@ -3963,15 +4168,26 @@ function applyTaskSemantics(contract, semantic) {
3963
4168
  : continuingTask && previous.routeSummary?.executionContract?.taskKind
3964
4169
  ? previous.routeSummary.executionContract
3965
4170
  : classifyTaskSemantics({ promptText: extractPrompt(payload) || persisted.routingContext?.lastExplicitUserPromptText, targetFile: persisted.routingContext?.targetFile });
3966
- const informationalTurn = !continuingTask && persisted.routingContext?.executionMode === 'informational'
3967
- && semantic.taskKind === 'CODE_IMPLEMENTATION';
3968
- if (informationalTurn) {
4171
+ // Only an authoritative request (or an owned continuation) creates debt.
4172
+ // Answer turns suspend it regardless of the classifier's fallback kind;
4173
+ // a tool-only event without an owned contract cannot synthesize a new task.
4174
+ const turnPrompt = extractPrompt(payload);
4175
+ const answerQuestion = /\?\s*$/.test(turnPrompt)
4176
+ && !/\b(?:review|audit|investigate|forensic)\b/i.test(turnPrompt);
4177
+ const informationalTurn = !continuingTask && !semantic.statusReport
4178
+ && (semantic.taskKind === 'CODE_IMPLEMENTATION' || answerQuestion)
4179
+ && (isInformationalPrompt({ promptText: turnPrompt })
4180
+ || isPureGreetingPrompt({ promptText: turnPrompt }));
4181
+ const unownedToolTurn = !extractPrompt(payload) && payload.tool_name && !storedContract;
4182
+ if (informationalTurn || unownedToolTurn) {
3969
4183
  // An answer-only turn must not enforce carried write debt, but it must not ERASE the
3970
4184
  // contract either: park it (session- and task-bound) so a continuation or the next
3971
4185
  // tool event of the same session restores it instead of finding "no contract routed".
3972
4186
  const parked = previous.routeSummary?.executionContract?.completionEvidence?.length
3973
4187
  ? { sessionId, taskInstanceId: previous.taskInstanceId, requestKey: previous.requestKey,
3974
- routeSummary: previous.routeSummary, routingContext: previous.routingContext, ts: Date.now() }
4188
+ routeSummary: previous.routeSummary, routingContext: previous.routingContext,
4189
+ semanticIdentity: previous.semanticIdentity, semanticDiagnostic: previous.semanticDiagnostic,
4190
+ contractOrigin: previous.contractOrigin, ts: Date.now() }
3975
4191
  : previous.suspendedContract;
3976
4192
  if (parked) persisted.suspendedContract = parked;
3977
4193
  delete persisted.routeSummary;
@@ -3982,7 +4198,9 @@ function applyTaskSemantics(contract, semantic) {
3982
4198
  const prior = previous.routeSummary?.executionContract;
3983
4199
  const parked = prior && !prior.statusReport && prior.completionEvidence?.length
3984
4200
  ? { sessionId, taskInstanceId: previous.taskInstanceId, requestKey: previous.requestKey,
3985
- routeSummary: previous.routeSummary, routingContext: previous.routingContext, ts: Date.now() }
4201
+ routeSummary: previous.routeSummary, routingContext: previous.routingContext,
4202
+ semanticIdentity: previous.semanticIdentity, semanticDiagnostic: previous.semanticDiagnostic,
4203
+ contractOrigin: previous.contractOrigin, ts: Date.now() }
3986
4204
  : previous.suspendedContract;
3987
4205
  if (parked) persisted.suspendedContract = parked;
3988
4206
  persisted.routeSummary ||= { executionMode: 'local-build' };
@@ -3991,7 +4209,7 @@ function applyTaskSemantics(contract, semantic) {
3991
4209
  if (persisted.contractOrigin !== 'carried-system') persisted.contractOrigin = 'classified';
3992
4210
  delete persisted.routeSummary.completionState;
3993
4211
  delete persisted.routeSummary.continuationState;
3994
- } else if (semantic.taskKind !== 'CODE_IMPLEMENTATION' || persisted.routeSummary) {
4212
+ } else if (semantic.taskKind !== 'CODE_IMPLEMENTATION' || persisted.routeSummary || persisted.routingContext?.mutationOrder === true) {
3995
4213
  persisted.routeSummary ||= { executionMode: 'local-build' };
3996
4214
  if ((semanticFromPrompt || !continuingTask) && persisted.routeSummary.executionContract) {
3997
4215
  // A NEW task never inherits the previous task's classifier-owned obligations either (e.g. a stale
@@ -4000,8 +4218,8 @@ function applyTaskSemantics(contract, semantic) {
4000
4218
  const kept = { ...persisted.routeSummary.executionContract };
4001
4219
  // statusReport / verificationOnly are only stripped on re-derivation: for a NEW task they drive the
4002
4220
  // carried-status rebuild below, so they must survive to be recognised there.
4003
- const staleKeys = semanticFromPrompt ? ['operationGoal', 'configurationScope', 'releasePolicy', 'reportArtifact', 'verificationOnly', 'statusReport']
4004
- : ['operationGoal', 'configurationScope', 'releasePolicy', 'reportArtifact'];
4221
+ const staleKeys = semanticFromPrompt ? ['publication', 'operationGoal', 'configurationScope', 'releasePolicy', 'conditionalObligations', 'reportArtifact', 'verificationOnly', 'statusReport']
4222
+ : ['publication', 'operationGoal', 'configurationScope', 'releasePolicy', 'conditionalObligations', 'reportArtifact'];
4005
4223
  for (const key of staleKeys) delete kept[key];
4006
4224
  // MIXED/OPERATIONAL evidence tags belong to the previous task's classification: a new task starts from
4007
4225
  // the mode's base evidence so it is not asked for operation proof its own prompt never ordered.
@@ -4053,6 +4271,17 @@ function applyTaskSemantics(contract, semantic) {
4053
4271
  && (!persisted.routeSummary || persisted.routeSummary.executionContract?.statusReport)) {
4054
4272
  persisted.suspendedContract = previous.suspendedContract;
4055
4273
  }
4274
+ if (persisted.semanticDiagnostic?.status === 'invariant-violation' && previous.routeSummary?.executionContract) {
4275
+ persisted.routeSummary.executionContract = previous.routeSummary.executionContract;
4276
+ }
4277
+ if (persisted.contractOrigin === 'classified' && persisted.routingContext?.lastExplicitUserPromptText) {
4278
+ const intentInput = { promptText: persisted.routingContext.lastExplicitUserPromptText,
4279
+ targetFile: persisted.routingContext.targetFile || '', taskInstanceId: persisted.taskInstanceId, sessionId };
4280
+ persisted.semanticIdentity = semanticContractIdentity(intentInput);
4281
+ const analysis = analyzeTaskIntent(intentInput);
4282
+ persisted.semanticProvenance = { engineVersion: analysis.engineVersion, actions: analysis.actions,
4283
+ spans: analysis.spans.map(({ raw, ...span }) => span) };
4284
+ }
4056
4285
  text = JSON.stringify(persisted);
4057
4286
  await writeJsonFileAtomic(statePath, text);
4058
4287
  if (!sessionId) return persisted;
@@ -4088,7 +4317,9 @@ function applyTaskSemantics(contract, semantic) {
4088
4317
  && sessionId && parkedContract.sessionId === sessionId
4089
4318
  && (continuationPrompt || /^(?:Edit|Write|MultiEdit|NotebookEdit)$/.test(String(payload.tool_name || '')))) {
4090
4319
  previous = { ...previous, taskInstanceId: parkedContract.taskInstanceId, routeSummary: parkedContract.routeSummary,
4091
- requestKey: parkedContract.requestKey, routingContext: parkedContract.routingContext || previous.routingContext };
4320
+ requestKey: parkedContract.requestKey, routingContext: parkedContract.routingContext || previous.routingContext,
4321
+ semanticIdentity: parkedContract.semanticIdentity, semanticDiagnostic: parkedContract.semanticDiagnostic,
4322
+ contractOrigin: parkedContract.contractOrigin };
4092
4323
  delete previous.suspendedContract;
4093
4324
  }
4094
4325
  // A turn the SYSTEM injected (a harness /loop wakeup, or the post-compaction "session is being
@@ -4348,8 +4579,6 @@ function applyTaskSemantics(contract, semantic) {
4348
4579
  const keptActive = active.filter((entry) => shouldKeepRouteEntryForIntent(entry, intentMode));
4349
4580
  active.length = 0;
4350
4581
  active.push(...keptActive);
4351
- // FR-004: a classified advisor/reference/replan hand-off always loads
4352
- // advisor-plan, even when no catalog signal fired. It takes the FIRST of the
4353
4582
  // two active-skill slots; the best-scoring other route fills the second.
4354
4583
  const advisorIntent = classifyAdvisorRequest({ promptText, commandText });
4355
4584
  const advisorCatalogEntry = advisorIntent
@@ -4764,7 +4993,6 @@ function applyTaskSemantics(contract, semantic) {
4764
4993
  });
4765
4994
  mergeResolverFields(routeSummary, finalSharedFields);
4766
4995
  inheritTaskPlaybook(routeSummary, previous, taskInstanceId);
4767
- // BL-019 (SPEC FR-001): fold the persisted measured map into the route —
4768
4996
  // identical semantics to route-task.mjs finalizeTaskRoute / taskRouting.js
4769
4997
  // deriveTaskRoute: measuredTier emitted additively; modelTier follows the
4770
4998
  // map only when tier-map.json says 'promote'. Advisory — every failure