@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.
- package/CHANGELOG.md +10 -0
- package/manifests/documentation.yaml +88 -0
- package/manifests/platform.full.yaml +13 -1
- package/package.json +5 -1
- package/scripts/install/module-dependency-closure.mjs +53 -0
- package/scripts/install/sync-installed-mirror.mjs +17 -4
- package/scripts/release/candidate-state.mjs +31 -0
- package/scripts/release/verify-release.mjs +60 -6
- package/src/core/buildPlan.js +2 -0
- package/src/core/executionContracts.js +1 -309
- package/template_project/.claude/commands/ukit/handoff-create.md +41 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +459 -1
- package/template_project/.claude/hooks/skill-router.sh +585 -357
- package/template_project/.claude/ukit/index/publication-intent.mjs +528 -0
- package/template_project/.claude/ukit/index/route-resolver.mjs +1 -290
- package/template_project/.claude/ukit/index/route-task.mjs +13 -1
- package/template_project/.claude/ukit/runtime/block-breaker.mjs +232 -1
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +46 -8
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +161 -56
- package/template_project/.claude/ukit/runtime/hook-chain-runner.mjs +51 -1
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +34 -10
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +357 -44
- package/template_project/.omp/hooks/pre/ukit-bridge.js +50 -26
- package/scripts/dev/capture-omp-stall.mjs +0 -246
|
@@ -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
|
|
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 (
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
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
|
-
//
|
|
3956
|
-
//
|
|
3957
|
-
|
|
3958
|
-
|
|
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
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
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,
|
|
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,
|
|
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
|