@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.
@@ -1,16 +1,4 @@
1
- // Single source of truth for UKit execution contracts (WS-D contract table unification).
2
- //
3
- // Every routing decision that is keyed on an execution contract derives from the tables in
4
- // this file:
5
- // - src/index/taskRouting.js (in-process routing: contracts + risk profiles + mode order)
6
- // - src/core/runtimeConfig.js (config defaults: contracts minus runtime-only keys)
7
- // - template_project/ukit/storage/config.json (literal mirror of the config defaults)
8
- // - template_project/.claude/ukit/index/route-task.mjs (shipped standalone CLI: contracts + tiers)
9
- //
10
- // route-task.mjs runs inside target projects and cannot import from src/, so it keeps a
11
- // literal copy — tests/consistency/executionContractSync.test.js locks that copy to these
12
- // tables so the four can never drift again (the local-build completionEvidence drift that
13
- // motivated this module is the failure mode it exists to prevent).
1
+ export { classifyTaskSemantics, applyTaskSemantics, isVerificationOnlyRequest } from '../../template_project/.claude/ukit/index/publication-intent.mjs';
14
2
 
15
3
  export const EXECUTION_MODE_ORDER = [
16
4
  'tiny-fix',
@@ -90,13 +78,8 @@ export const MODEL_TIER_BY_CONTRACT = {
90
78
  'review-release': 'smart',
91
79
  };
92
80
 
93
- // Decision table v2 (V3_RESHAPE §5): MODEL_TIER_BY_CONTRACT stays the base row;
94
- // resolveModelTier adds the (riskFloor, hostCapabilities) columns. Deterministic —
95
- // no registry, no leases, no outbound capability calls.
96
81
  export const ROUTE_EFFORTS = new Set(['low', 'medium', 'high']);
97
82
 
98
- // Tier, effort and risk are independent decisions. Effort is Medium for every tier (Low for chores);
99
- // High needs complexity evidence (`complexityEvidence` below), never risk or tier alone.
100
83
  const MODEL_TIER_EFFORT = {
101
84
  lite: 'low',
102
85
  code: 'medium',
@@ -168,294 +151,3 @@ export function buildConfigContracts() {
168
151
  Object.entries(EXECUTION_CONTRACTS).map(([mode, contract]) => [mode, stripContractForConfig(contract)])
169
152
  );
170
153
  }
171
-
172
- // What exactly was ordered, so a receipt for a DIFFERENT verb/service/probe cannot discharge it.
173
- // Unnamed specifics stay empty (any proven receipt of the domain counts); named ones are required.
174
- const NOT_A_SERVICE = new Set(['it', 'this', 'that', 'them', 'the', 'all', 'everything', 'service', 'services', 'again', 'now', 'to', 'and', 'then']);
175
- function nativeOperationGoal(domain, actionable, text) {
176
- const goal = { domain, target: null, desiredState: 'verified', completionSemantics: 'execute-and-verify', requiredTargets: [] };
177
- const clauses = actionable.join(' . ');
178
- if (domain === 'git-integrity') {
179
- const verbs = [...clauses.matchAll(/\bgit\s+(push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b/g)].map((m) => m[1]);
180
- if (/\bpush\s+(?:to\s+)?origin\b/.test(clauses)) verbs.push('push');
181
- goal.requiredTargets = [...new Set(verbs)];
182
- goal.check = /\bfsck\b/.test(text) ? 'fsck' : /\bdiff\s+--check\b/.test(text) ? 'diff-check' : null;
183
- return goal;
184
- }
185
- const services = [...clauses.matchAll(/\b(?:restart|redeploy|deploy|khoi dong lai|trien khai)\s+(?:the\s+|lai\s+)?([a-z0-9][\w.-]*)/g)]
186
- .map((m) => m[1]).filter((name) => !NOT_A_SERVICE.has(name));
187
- goal.requiredTargets = [...new Set(services)];
188
- const probePath = text.match(/(?:^|\s)(\/[a-z0-9_.-]+(?:\/[a-z0-9_.-]+)*)/)?.[1] || null;
189
- const probeStatus = text.match(/\b(?:returns?|returning|responds?(?:\s+with)?|http|status(?:\s+code)?)\s+([1-5]\d\d)\b/)?.[1] || null;
190
- if (probePath || probeStatus) goal.probe = { path: probePath, status: probeStatus };
191
- return goal;
192
- }
193
-
194
- // Task semantics are independent of source execution strategy and observed progress.
195
- // P1-A (forensic false block): the clause splitter below cuts at commas, so a negation only reached the
196
- // FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
197
- // the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
198
- // sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
199
- // Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
200
- // fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
201
- 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';
202
- // Declared verification purpose ("Mục tiêu: xác nhận ...", "Goal: verify ..."). Under that declaration a
203
- // conditional repair/release branch ("Nếu phát hiện lỗi ... sửa ...", "Neu co source fix ... publish") is a
204
- // branch the check MAY take, not an unconditional order, and disposable test-project setup ("Tạo project thử
205
- // nghiệm độc lập") is scaffolding of the check. Unconditional orders in the same request keep blocking.
206
- 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/;
207
- 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;
208
- const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
209
- 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;
210
- // Round-2b (FS-01 B5): a bare "no <verb>, <verb>, ..." enumeration names operations that are NOT
211
- // ordered ("no commit, release, publish"). The clause splitter cuts at commas, so without this the
212
- // tails ("release", "publish") would look like separate imperative clauses to the request-wide order
213
- // guard. Only a "no"-headed verb list is consumed; a real order never starts with "no". Built lazily
214
- // because ORDER_VERBS is declared below this function.
215
- const noVerbList = () => new RegExp(`\\bno\\s+(?:${ORDER_VERBS})\\b(?:\\s*,\\s*(?:${ORDER_VERBS})\\b)*`, 'g');
216
- function neutralizeNonOrders(text) {
217
- let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
218
- if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
219
- return source.replace(noVerbList(), ' ')
220
- // A list item that DESCRIBES test cases ("Negative controls: yêu cầu thực sự chỉnh config/restart/deploy ...") is
221
- // specification of the work, not an order; it runs to the next bullet or blank line.
222
- .replace(/^[ \t]*(?:[-*+\u2022]|\d+[.)])[ \t]*(?:negative|positive)\s+controls?\b[^\n]*(?:\n(?![ \t]*(?:[-*+\u2022]|\d+[.)])|[ \t]*\n)[^\n]*)*/gm, ' ')
223
- // A header that negates the list under it ("Authorization ... không cho phép:", "Prohibitions:", "not allowed:")
224
- // makes every bullet an item of the prohibition, not an order.
225
- .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, ' ')
226
- .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'), ' ')
227
- .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
228
- .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
229
- .replace(/\b(?:uu tien|prefer(?:red)?)\s+(?:a\s+|an\s+)?(?:configuration|config|cau hinh)\s+(?:change|changes|update|thay doi)\b/g, ' ')
230
- .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, ' ')
231
- .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
232
- .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, ' ')
233
- .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
234
- .replace(/\bwrite\s+permission\b/g, 'permission')
235
- .replace(/\b(?:state|status)\s+changes?\b/g, ' ')
236
- .replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ')
237
- // An output/deliverable spec ("Output: fix direction", "each: title, fix direction", "Output:
238
- // fixes / fix list / acceptance to close") NAMES the artifacts the answer must contain; inside
239
- // such a clause "fix" is a deliverable noun, not a mutation order, so a read-only audit with a
240
- // fix-shaped deliverable list must not owe write evidence (FS-07, src twin of the FS-01 fix in
241
- // route-resolver.mjs). A determiner object inside the span ("Output: fix the bug in parser.ts")
242
- // stays a real order - fail closed.
243
- // Round-2 (FS-01 B1-B4, src twin): a span runs through a file-extension dot ("fix items.ts"), a bare
244
- // singular "fix" is an order (never a noun), and a span keeps its order unless EVERY fix token in it is a
245
- // deliverable noun and no order verb / "then" remains in the clause ("fix list, commit and push").
246
- .replace(/(?:^|[\n.;])[ \t]*(?:[-*+•]|\d+[.)])?[ \t]*(?:outputs?|reports?|deliverables?|each|lists?|includes?|fields?|format|sections?|columns?)(?:\s+\w+){0,3}?\s*:(?:[^\n.;]|\.(?=\w))*/g,
247
- (span) => {
248
- 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;
249
- const residual = span.replace(fixNouns, ' ');
250
- if (residual === span) return span;
251
- if (new RegExp(`\\b(?:then|${NON_ORDER_VERBS}|${ORDER_VERBS})\\b`).test(residual)) return span;
252
- return residual;
253
- });
254
- }
255
-
256
- // Explicit scope declaration (intent ownership): when the request itself declares it is read-only
257
- // ("READ-ONLY audit", "chi doc", "make no source changes") and carries no explicit order to change
258
- // anything, verbs and operation names inside it are descriptions (a "git stash" that already ran, "a
259
- // worklog edit", "confirmed fixes"), not orders - so they must not demand write or operation proof.
260
- // An explicit order (imperative-initial or modal request) keeps the request MIXED/IMPLEMENTATION:
261
- // the declaration never overrides a real instruction, and fenced/quoted/blockquoted text is never one.
262
- // A bare verb with no object is the tail of a negated list ("no commit, release, publish"), not an order.
263
- const READ_ONLY_DECLARATION = /\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/;
264
- 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)';
265
- const IMPERATIVE_ORDER = new RegExp(`^(?:[-*+\\u2022>\\d.)\\s]*)(?:(?:please|hay|vui long|now|also|first|then)\\s+)*(?:${ORDER_VERBS})\\b\\s+\\S`);
266
- 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`);
267
- function hasExplicitOrder(clauses) {
268
- return clauses.some((clause) => IMPERATIVE_ORDER.test(clause) || MODAL_ORDER.test(clause));
269
- }
270
- // Round-2b (FS-01 B5): a clause AFTER the output span still orders a change ("Output: fix lists\ncommit
271
- // the changes", "Output: fix lists; commit"). Order detection must be request-wide, not span-local: any
272
- // non-negated clause whose command position is an ORDER verb - with or without an object, so a bare
273
- // trailing "commit" counts - is an explicit order. The vocabulary is the order class the read-only
274
- // declaration must never absorb (git/publish/release/apply/run-implementation); noun-ish scaffolding
275
- // ("tao timeline ...", "create a list") is NOT an order and must not defeat a read-only audit.
276
- // Negated lists ("no commit, release, publish", "Khong npm publish, git push, release") are blanked
277
- // first by neutralizeNonOrders, so their bare tails never read as orders.
278
- 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/;
279
- function ordersChangeAcrossClauses(clauses) {
280
- return clauses.some((clause) => CLAUSE_ORDER_LEAD.test(clause));
281
- }
282
- function declaredReadOnly(promptText, releasePolicy) {
283
- if (releasePolicy) return false;
284
- const plain = String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
285
- .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
286
- if (!READ_ONLY_DECLARATION.test(plain)) return false;
287
- const text = neutralizeNonOrders(plain);
288
- const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/)
289
- .map((clause) => clause.split(/\bwithout\b/)[0])
290
- .filter((clause) => !/\b(?:do not|don't|never|khong)\b|^[\s*+•>\d.)-]*(?:(?:vui long|xin|hay|please)\s+)*dung\b/.test(clause));
291
- 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)
292
- && !hasExplicitOrder(clauses)
293
- && !ordersChangeAcrossClauses(clauses);
294
- }
295
-
296
- // Verification-only request. Root cause of a false `write-evidence` block on a "verify / re-run /
297
- // confirm" prompt: the vocabulary scans counted nouns and compounds ("coordinator fix", "re-run",
298
- // "template/install", "source change") as mutation orders. Order is structural: a request whose
299
- // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
300
- // change-leading clause, orders no change - it owes verification evidence, not a write.
301
- 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/;
302
- 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/;
303
- export function isVerificationOnlyRequest(promptText) {
304
- const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
305
- .replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
306
- const clauses = neutralizeNonOrders(plain).split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b|\broi\b)\s*/)
307
- .map((clause) => clause.split(/\bwithout\b/)[0]);
308
- return clauses.some((clause) => VERIFY_LEAD.test(clause)) && !hasExplicitOrder(clauses)
309
- && !clauses.some((clause) => CHANGE_LEAD.test(clause));
310
- }
311
-
312
- // "Deploy and restart are banned", "Forbidden: deploy", "deploy: banned" name operations to prohibit them. The clause split on
313
- // "and"/commas would separate the ban from the first operation, so a banned operation list is removed before splitting.
314
- function neutralizeBannedLists(text) {
315
- const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
316
- const LIST = `${OP}(?:\\s*(?:,|/|\\band\\b|\\bor\\b|\\bva\\b|\\bhoac\\b|\\balso\\b)\\s*${OP})*`;
317
- const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
318
- 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/;
319
- 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');
320
- return text.replace(span, (m, offset, whole) => {
321
- const start = Math.max(whole.lastIndexOf('.', offset), whole.lastIndexOf(';', offset), whole.lastIndexOf('\n', offset)) + 1;
322
- const stops = [whole.indexOf('.', offset + m.length), whole.indexOf(';', offset + m.length), whole.indexOf('\n', offset + m.length)].filter((i) => i >= 0);
323
- const end = stops.length ? Math.min(...stops) : whole.length;
324
- return LIFT.test(whole.slice(start, end)) ? m : ' ';
325
- });
326
- }
327
-
328
- export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
329
- // A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
330
- const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
331
- const preflight = releaseText.match(/\b(?:yarn\s+release:verify|npm\s+run\s+release:verify)\b/)?.[0];
332
- const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
333
- && /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
334
- ? { kind: 'conditional-release', preflight } : null;
335
- const text = neutralizeBannedLists(neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()));
336
- const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
337
- // Folded đúng (correctly) is also "dung"; only command-position đừng negates an order.
338
- 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)
339
- && !/^[\s*+•>\d.)-]*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
340
- if (declaredReadOnly(promptText, releasePolicy)) return { taskKind: 'READ_ONLY' };
341
- const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
342
- && /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
343
- const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
344
- const reportArtifact = String(promptText).match(/(?:^|\s)((?:docs\/|reports\/)[A-Za-z0-9_./-]+\.(?:md|txt))(?=\s|[.,;:]|$)/)?.[1];
345
- 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))
346
- && /\b(?:report|bao cao|analysis|phan tich|document)\b/.test(text);
347
- const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
348
- // A cron/job MENTION is not an operation: with a code-change verb in the request, an
349
- // operation needs a concrete target id; otherwise it is a coding task about scheduling.
350
- const cronOperation = actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy|ensure|dam bao)\b/.test(clause)
351
- && /\bcron\b|scheduled\s+(?:cron\s+)?(?:task|job)|tac vu\s+(?:cron|dinh ky)/.test(clause))
352
- && (!codeChangeVerb || hasOperationId);
353
- // Non-source operations (git history/push, service restart/deploy): ordered only by an
354
- // actionable clause (negated/deferred/audit clauses are already filtered out). Git and
355
- // runtime are separate domains with separate proof; a prompt naming both stays a coding
356
- // task rather than being half-verified. A code-change verb anywhere keeps write evidence.
357
- // A clause that STATES an operation is prohibited ("production deploy vẫn bị cấm", "forbidden deploy") or merely lists a
358
- // production operation as a bare noun item ("* Production restart.") names it; it does not order it.
359
- const operationNotOrdered = (clause) => {
360
- const OP = '(?:restart|redeploy|deploy(?:ment)?|trien khai|khoi dong lai|git\\s+push|push\\s+(?:to\\s+)?origin)';
361
- const BAN = '(?:bi\\s+(?:cam|chan)|forbidden|prohibited|banned|not\\s+allowed|not\\s+permitted|disallowed)';
362
- // A lift or past marker ("was prohibited, owner now allows", "even though it is not allowed", "khong bi chan")
363
- // means the prohibition is history or lifted: the clause still orders the operation.
364
- 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;
365
- if (/^[\s*+•>\d.)-]*(?:production|prod|staging)\s+(?:restart|redeploy|deploy(?:ment)?)\s*$/.test(clause)) return true;
366
- // Evaluated per operation occurrence: the clause orders nothing only when EVERY operation in it is prohibited.
367
- // The ban must attach to its own operation: no parenthesis, dash, colon, conjunction, relative pronoun or second operation in between.
368
- const gap = `(?:(?!\\b(?:which|that|who|where|when|if|and|but|then)\\b|${OP})[^.;,()\\-:]){0,40}?`;
369
- const banAfter = new RegExp(`^${gap}\\b(?:(?:is|are|remains?|van|con)\\s+)?${BAN}\\b(?!\\s+(?:to|from)\\b)`);
370
- // "deploy/restart is forbidden", "deploy and restart are banned": operations joined as a list share one ban.
371
- const joined = new RegExp(`^\\s*(?:/|,|and|or|va|hoac)\\s*${OP}\\b`);
372
- const bannedAfter = (rest) => {
373
- if (banAfter.test(rest)) return true;
374
- const j = joined.exec(rest);
375
- return j ? bannedAfter(rest.slice(j[0].length)) : false;
376
- };
377
- const banBefore = /\b(?:forbidden|prohibited|banned)\s+(?:(?:production|prod)\s+)?$/;
378
- let total = 0;
379
- let banned = 0;
380
- for (const match of clause.matchAll(new RegExp(`\\b${OP}\\b`, 'g'))) {
381
- total += 1;
382
- if (bannedAfter(clause.slice(match.index + match[0].length)) || banBefore.test(clause.slice(0, match.index))) banned += 1;
383
- }
384
- return total > 0 && banned === total;
385
- };
386
- const gitOperation = actionable.some((clause) => !operationNotOrdered(clause) && /\bgit\s+(?:push|clean|commit|reset|rebase|merge|tag|stash|checkout|restore|cherry-pick)\b|\bpush\s+(?:to\s+)?origin\b/.test(clause));
387
- // 'trien khai' is Vietnamese for both "deploy" and "implement": it orders a runtime operation only
388
- // when it names a runtime target; bare it describes work ("Tier 3 truc tiep trien khai thay Tier 2").
389
- // "restart"/"deploy" after a noun modifier ("process restart", "after redeploy") is a scenario, not an order.
390
- 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)
391
- && !/\.[cm]?[jt]sx?\b/.test(clause) && !/\bgit\b/.test(clause));
392
- const nativeDomain = cronOperation ? 'scheduled-job' : gitOperation !== runtimeOperation ? (gitOperation ? 'git-integrity' : 'service-runtime') : null;
393
- const operation = Boolean(nativeDomain);
394
- // A verification-only request orders no change; a concrete operation order (cancel/restart/push) stays operational.
395
- if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
396
- // A config MENTION is not a config change: the same clause needs a mutation verb, and a
397
- // request that also changes source/code is a coding task (config is part of its tests/fixtures).
398
- // A DIAGNOSTIC report of past changes ("which parameters/configuration has changed, if any" /
399
- // "tham so/cau hinh nao da thay doi, neu co") is a question about history, never an authorization
400
- // to mutate: only a forward order ("change X to Y") stays a config mutation.
401
- 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/;
402
- // A relative past-change clause inside a FORWARD imperative ("Set the config that
403
- // has changed to dark mode") names WHICH config — it does not make the sentence a
404
- // report. The override holds only when the clause's FIRST mutation word precedes
405
- // the config noun AND no reporting/question frame opens the clause or precedes
406
- // that word: in "Report what change was made to the config" the mutation word is
407
- // a question's noun, never an order.
408
- 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/;
409
- const CONFIG_NOUN = /\b(?:configuration|config|toggle|cau hinh)\b/;
410
- const CONFIG_REPORT_FRAME = /^\s*(?:report|show|tell|list|describe|explain|summari[sz]e|bao cao|cho biet|liet ke|mo ta)\b/;
411
- const CONFIG_QUESTION_FRAME_PREFIX = /\b(?:what|which|how|report|bao cao|nao|gi)\b/;
412
- const config = !sourceOrder && actionable.some((clause) => {
413
- if (!CONFIG_NOUN.test(clause) || !CONFIG_MUTATION_VERB.test(clause)) return false;
414
- if (CONFIG_REPORT_FRAME.test(clause)) return false;
415
- const verb = CONFIG_MUTATION_VERB.exec(clause);
416
- if (CONFIG_QUESTION_FRAME_PREFIX.test(clause.slice(0, verb.index))) return false;
417
- const noun = CONFIG_NOUN.exec(clause);
418
- return !CONFIG_CHANGE_REPORT_CLAUSE.test(clause) || Boolean(noun && noun.index > verb.index);
419
- });
420
- const repository = Boolean(targetFile) || /[\w.-]+\.(?:json|ya?ml|toml|ini)\b/.test(text);
421
- // A status/result lookup ("check the suite output and report") orders no change: it needs a
422
- // lookup verb AND no mutation vocabulary anywhere in the request (fail-closed otherwise).
423
- 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);
424
- // A bounded, recognizable reporting QUESTION about what changed ("Tham số/cấu hình
425
- // nào đã thay đổi, nếu có." / "which configuration has changed, if any") is a
426
- // status report even without a lookup verb — but ONLY when no actionable clause
427
- // actually orders a mutation: a mixed request ("Add a dark-mode button; report
428
- // which configuration changed") keeps its implementation contract and evidence.
429
- // The two lookbehinds keep a question's noun ("what change was made") and the
430
- // Vietnamese past-tense verb "thay đổi" from counting as mutation orders.
431
- 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/;
432
- const configReportQuestion = !actionable.some((clause) => ACTIONABLE_MUTATION_ORDER.test(clause))
433
- && actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh|tham so)\b/.test(clause)
434
- && CONFIG_CHANGE_REPORT_CLAUSE.test(clause));
435
- const statusReport = (configReportQuestion || (!mutationWord
436
- && /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text)))
437
- && !operation && !config;
438
- // Round-2b (FS-01 B5): order detection is request-wide. A later clause that commands an order verb
439
- // ("Output: fix lists\ncommit the changes", "...; publish") keeps the mutation even when the output
440
- // span itself was neutralised to deliverable nouns; a true noun-only audit output stays read-only.
441
- 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)
442
- && !sourceOrder && !codeChangeVerb && !operation && !config && !ordersChangeAcrossClauses(actionable);
443
- const taskKind = operation ? (sourceOrder || (nativeDomain !== 'scheduled-job' && codeChangeVerb) ? 'MIXED' : 'OPERATIONAL')
444
- : config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
445
- const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
446
- return { taskKind, ...(releasePolicy ? { releasePolicy } : {}), ...(documentation ? { reportArtifact } : {}), ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
447
- ...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
448
- ...(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',
449
- completionSemantics: /\b(?:ensure|dam bao)\b/.test(text) ? 'ensure-state' : 'execute-and-verify' } } : {}) };
450
- }
451
-
452
- export function applyTaskSemantics(contract, semantic) {
453
- const evidence = contract?.completionEvidence || [];
454
- const operationOnly = semantic.taskKind === 'OPERATIONAL'
455
- || (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
456
- return { ...(contract || {}), ...semantic,
457
- completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
458
- : semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
459
- : semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
460
- : semantic.taskKind === 'READ_ONLY' ? (semantic.verificationOnly || (evidence.length === 1 && evidence[0] === 'verification-evidence') ? ['verification-evidence'] : ['impact-evidence']) : evidence };
461
- }
@@ -12,7 +12,10 @@
12
12
  $ARGUMENTS
13
13
 
14
14
  > **Can be run multiple times.** If the plan or tasks aren't satisfactory after the first run, re-run `/ukit:handoff-create` to refine until the plan is good — as long as no task has been implemented yet.
15
- > **NO git commit. NO git push.** This phase only writes docs files. No code changes.
15
+ > **NO git commit. NO git push by default.** This phase only writes docs files. No code changes. The
16
+ > one exception is the owner-authorized docs publication below: an explicit owner message in this
17
+ > session may authorize a **docs-only** `git commit` / `git push`, which the guard still verifies —
18
+ > source, mixed or out-of-scope file sets are denied, and publication never activates the backlog.
16
19
 
17
20
  ## Planning-only contract — this command never implements
18
21
 
@@ -31,6 +34,35 @@ handoff-fullstack cursor. Running this command **makes the session's handoff int
31
34
  planning**, so the Stop gate and the SessionStart compact/resume hook both keep quiet
32
35
  until the human explicitly resumes.
33
36
 
37
+ ### Owner-authorized docs publication — the only git exception
38
+
39
+ `git commit` / `git push` in a create session require BOTH of:
40
+
41
+ 1. **An affirmative owner ORDER, per operation** — an explicit message from the human owner
42
+ in THIS session's transcript naming BOTH the exact operation and the docs scope (e.g.
43
+ "commit the docs" authorizes commit only; "push the docs to origin" authorizes push
44
+ only). The latest decisive owner text per op wins: a later "don't push yet" revokes.
45
+ Questions ("why did push fail?"), history ("the push failed yesterday"), wholly quoted
46
+ text, foreign-session/sidechain turns, assistant prose, tool output, and this command's
47
+ own "NO git commit" text never authorize (and never revoke).
48
+ 2. **A strictly verified docs-only operation** — one pure `git commit`/`git push` (no
49
+ `;`/`&&` chains, no `-C`/`-c`/`--git-dir`/`--work-tree` redirects, no
50
+ `--force`/`--mirror`/`--all`/`--tags`/`--delete`/`-a`/`--amend`/`--no-verify`, no
51
+ `src:dst` refspecs) where every staged file (commit) or every outgoing commit's files
52
+ (push — commits are inspected with rename detection off, not the net diff) is a docs
53
+ path holding a regular file (symlinks and submodules are rejected). Denied even with
54
+ authorization: source or mixed sets, tests-only sets, an empty index, pushes whose
55
+ destination cannot be pinned to the verified upstream, and widening/redirecting config
56
+ (`push.default` beyond simple/current/upstream, `push.followTags`, `remote.*.mirror`,
57
+ `remote.*.push`, `remote.pushDefault`, `branch.*.pushRemote`). A first push of a NEW
58
+ remote branch is possible only as `git push -u <remote> <branch>` with an owner order
59
+ that explicitly says create/new; every commit between the remote default head and the
60
+ branch must be docs-only. Any failed verification step denies — nothing is allowed on
61
+ an unreadable repository state.
62
+
63
+ Publication moves bytes, not the backlog: it never runs the ready gate, never flips task
64
+ statuses, and never folds a queued blueprint into a cycle.
65
+
34
66
  ### A live fullstack run is never overwritten — the new plan is QUEUED
35
67
 
36
68
  Before writing anything, read `docs/AI_HANDOFF/RUN.md`. If it exists and its `Phase:`
@@ -205,6 +237,14 @@ Do not repeat or respawn a refused dispatch, change its writer, or manufacture a
205
237
  fallback plan to satisfy Stop. Preserve the exact refusal and incomplete planning
206
238
  state. A hook refusal is not a transient network error.
207
239
 
240
+ A **deterministic dispatch/init failure** (a field the host validator rejects, a refused
241
+ agent, an isolation refusal) is never retried with an unchanged payload: either apply the
242
+ refusal's own correction once — a genuinely different payload — and retry, or record the
243
+ refused state and move on; retry is legitimate only after the condition actually changed.
244
+ If the independent reviewer cannot be dispatched at all, do not review from the main
245
+ session and do not fabricate a verdict: keep the affected tasks `needs_breakdown` with the
246
+ missing gate named, unless the owner/policy explicitly authorizes a fallback.
247
+
208
248
  ## Step 1 — Read context (lite model)
209
249
 
210
250
  Before this first context-agent call, omit the `isolation` field entirely.