@ngockhoale/ukit 3.4.48 → 3.4.50

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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.50 - 2026-10-21
6
+
7
+ **Verification exit-proof integrity (long-run OMP field audit).**
8
+ - Fix execution-ledger false PASS receipts: a bare `pipefail`/`PIPESTATUS` word (e.g. `echo pipefail`) no longer exempts a masked pipe; only real `set -o pipefail` before the pipe or an immediate `exit ${PIPESTATUS[n]}` with the correct index proves the runner status.
9
+ - A runner followed by `|| …`, backgrounded with `&`, inside a `for`/`while`/`case` flow, behind `bash -c` hiding a masked pipe, or followed by `exit 0`/`true`/`:`/leading `!` is recorded as `exitStatusUnproven` (not passing). Claude `tool_response.interrupted:true` now counts as failure.
10
+ - Bare runners, `cd repo; runner`, `&&` chains, real pipefail and terminal subshell/group runners stay accepted. Sequential consumers (including `; echo "exit=$?"`) and `if runner; then echo ok; fi` no longer receive PASS credit: recognizing a runner attempt does not prove its exit. Loop keywords as runner arguments (`vitest -t case`) are not loops.
11
+ - Audit report: `UKIT_LONG_RUN_ORCHESTRATION_AUDIT.md`. Verified locally on macOS/Node 22 only; OMP live host-ordering attribution remains UNVERIFIED. No CI dispatch.
12
+
13
+ ## 3.4.49 - 2026-10-19
14
+
15
+ **Conditional consumer verification intent closure.**
16
+ - Fix a live Completion Gate false write obligation: a verification request ("Mục tiêu: xác nhận …") whose source repair/release steps apply only if a defect is found, plus a disposable test-project setup, was classified CODE_IMPLEMENTATION and demanded write-evidence even after a real `npm test` exit 0. The classifier now recognizes a declared verification goal with a narrowly structured conditional repair branch and `xác nhận` as a verify lead; unconditional trailing orders, quoted goals and non-repair conditionals still block.
17
+ - Safety preserved: when the conditional repair branch is actually taken (an owned in-project substantive source write), the evaluator re-activates the release-publication demand; explicit publish prohibitions dominate. Classifier copies (src, resolver, hook) are pinned by a mirror parity test.
18
+ - Reproduced through the npm 3.4.48 consumer router/record/Stop lifecycle with real native exits; no receipt fabrication, task reassignment or ledger reset. Direct `node --test` recognition is unchanged. Verified on macOS/Node 22 only; OMP live attribution UNVERIFIED; no paid OMP smoke or CI dispatch.
19
+
5
20
  ## 3.4.48 - 2026-10-19
6
21
 
7
22
  **Completion evidence closure and reproducible multi-host installation.**
@@ -71,6 +71,17 @@ entries:
71
71
  validation: [manual]
72
72
  archive_policy: never
73
73
  notes: release record
74
+ - id: root-long-run-orchestration-audit
75
+ path: UKIT_LONG_RUN_ORCHESTRATION_AUDIT.md
76
+ class: canonical
77
+ audience: [owner, maintainer]
78
+ owner: runtime
79
+ source_of_truth: UKIT_LONG_RUN_ORCHESTRATION_AUDIT.md
80
+ merge_strategy: none
81
+ load_policy: on-demand
82
+ validation: [manual, test:tests/hooks/executionLedger.test.js, test:tests/hooks/verificationExitProof.test.js]
83
+ archive_policy: replace-with-snapshot
84
+ notes: evidence-based long-run field audit; current reproductions are distinct from incident attribution
74
85
  - id: registry-self
75
86
  path: manifests/documentation.yaml
76
87
  class: canonical
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.48",
3
+ "version": "3.4.50",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -199,12 +199,22 @@ function nativeOperationGoal(domain, actionable, text) {
199
199
  // Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
200
200
  // fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
201
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;
202
210
  function neutralizeNonOrders(text) {
203
- return String(text)
211
+ let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
212
+ if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
213
+ return source
204
214
  .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'), ' ')
205
215
  .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
206
216
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
207
- .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
217
+ .replace(/\b(?:neu\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, ' ')
208
218
  .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
209
219
  .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, ' ')
210
220
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
@@ -244,7 +254,7 @@ function declaredReadOnly(promptText, releasePolicy) {
244
254
  // "template/install", "source change") as mutation orders. Order is structural: a request whose
245
255
  // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
246
256
  // change-leading clause, orders no change - it owes verification evidence, not a write.
247
- const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|validate|double-?check)\b/;
257
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
248
258
  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/;
249
259
  export function isVerificationOnlyRequest(promptText) {
250
260
  const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
@@ -1881,12 +1881,22 @@ function nativeOperationGoal(domain, actionable, text) {
1881
1881
  // Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
1882
1882
  // fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
1883
1883
  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';
1884
+ // Declared verification purpose ("Mục tiêu: xác nhận ...", "Goal: verify ..."). Under that declaration a
1885
+ // conditional repair/release branch ("Nếu phát hiện lỗi ... sửa ...", "Neu co source fix ... publish") is a
1886
+ // branch the check MAY take, not an unconditional order, and disposable test-project setup ("Tạo project thử
1887
+ // nghiệm độc lập") is scaffolding of the check. Unconditional orders in the same request keep blocking.
1888
+ 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/;
1889
+ 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;
1890
+ const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
1891
+ 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;
1884
1892
  function neutralizeNonOrders(text) {
1885
- return String(text)
1893
+ let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1894
+ if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
1895
+ return source
1886
1896
  .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'), ' ')
1887
1897
  .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
1888
1898
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
1889
- .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1899
+ .replace(/\b(?:neu\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, ' ')
1890
1900
  .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
1891
1901
  .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, ' ')
1892
1902
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
@@ -1905,7 +1915,7 @@ function hasExplicitOrder(clauses) {
1905
1915
  // "template/install", "source change") as mutation orders. Order is structural: a request whose
1906
1916
  // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
1907
1917
  // change-leading clause, orders no change - it owes verification evidence, not a write.
1908
- const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|validate|double-?check)\b/;
1918
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
1909
1919
  const CHANGE_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:make|rebuild|redo|rewrite|restore|revert|correct|clean|patch|resolve|ensure|run(?!(?=[^.]*\b(?:tests?|suite|gate|checks?|lint|typecheck|pilot|smoke|e2e|verif\w*|vitest|jest|audit)\b)))\b/;
1910
1920
  function isVerificationOnlyRequest(promptText) {
1911
1921
  const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
@@ -1096,12 +1096,22 @@ function nativeOperationGoal(domain, actionable, text) {
1096
1096
  // Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
1097
1097
  // fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
1098
1098
  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';
1099
+ // Declared verification purpose ("Mục tiêu: xác nhận ...", "Goal: verify ..."). Under that declaration a
1100
+ // conditional repair/release branch ("Nếu phát hiện lỗi ... sửa ...", "Neu co source fix ... publish") is a
1101
+ // branch the check MAY take, not an unconditional order, and disposable test-project setup ("Tạo project thử
1102
+ // nghiệm độc lập") is scaffolding of the check. Unconditional orders in the same request keep blocking.
1103
+ 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/;
1104
+ 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;
1105
+ const GOAL_LABEL = /\b(?:muc tieu|goal|purpose|objective)\s*:/g;
1106
+ 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;
1099
1107
  function neutralizeNonOrders(text) {
1100
- return String(text)
1108
+ let source = String(text).replace(/```[\s\S]*?```/g, ' ').replace(/^[ \t]*>.*$/gm, ' ');
1109
+ if (VERIFICATION_GOAL.test(source)) source = source.replace(GOAL_LABEL, ' ').replace(CONDITIONAL_BRANCH, '\n').replace(DISPOSABLE_SETUP, ' ');
1110
+ return source
1101
1111
  .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'), ' ')
1102
1112
  .replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
1103
1113
  .replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
1104
- .replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
1114
+ .replace(/\b(?:neu\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, ' ')
1105
1115
  .replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
1106
1116
  .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, ' ')
1107
1117
  .replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
@@ -1141,7 +1151,7 @@ function declaredReadOnly(promptText, releasePolicy) {
1141
1151
  // "template/install", "source change") as mutation orders. Order is structural: a request whose
1142
1152
  // command-position verbs are verification verbs, with no explicit order (hasExplicitOrder) and no
1143
1153
  // change-leading clause, orders no change - it owes verification evidence, not a write.
1144
- const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|validate|double-?check)\b/;
1154
+ const VERIFY_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:verify|re-?run|confirm|xac nhan|validate|double-?check)\b/;
1145
1155
  const CHANGE_LEAD = /^(?:[-*+•>\d.)\s]*)(?:(?:please|hay|vui long|now|also|first|then)\s+)*(?:make|rebuild|redo|rewrite|restore|revert|correct|clean|patch|resolve|ensure|run(?!(?=[^.]*\b(?:tests?|suite|gate|checks?|lint|typecheck|pilot|smoke|e2e|verif\w*|vitest|jest|audit)\b)))\b/;
1146
1156
  export function isVerificationOnlyRequest(promptText) {
1147
1157
  const plain = String(promptText ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
@@ -460,6 +460,10 @@ function explicitError(payload = {}) {
460
460
  return [
461
461
  payload.isError,
462
462
  payload.is_error,
463
+ payload.interrupted,
464
+ payload.tool_output?.interrupted,
465
+ payload.tool_result?.interrupted,
466
+ payload.tool_response?.interrupted,
463
467
  payload.tool_output?.isError,
464
468
  payload.tool_output?.is_error,
465
469
  payload.tool_result?.isError,
@@ -530,8 +534,8 @@ function extractExitCode(payload = {}) {
530
534
  return null;
531
535
  }
532
536
 
533
- // The trailing boundary accepts shell terminators (`;`, `&`, `|`, `)`) so
534
- // `yarn release:verify; echo "exit=$?"` — the real-exit-code idiom — still mints a receipt.
537
+ // Recognize runner presence separately from exit proof. A trailing echo of `$?`
538
+ // can report a status in text but cannot make the wrapper's exit authoritative.
535
539
  // Blank out quoted literals so `echo '(jest passed)'` / `git commit -m "run yarn test"` do not
536
540
  // look like a runner invocation. A `bash|sh|zsh -c '<script>'` wrapper really executes its
537
541
  // quoted script, so that script is tested on its own (bounded recursion).
@@ -584,71 +588,94 @@ function isVerificationCommand(command, depth = 0) {
584
588
  ].some((pattern) => pattern.test(text));
585
589
  }
586
590
 
587
- // Split a shell command into pipelines of stages (quote/escape aware). `&&`, `||`, `;` and
588
- // newlines separate pipelines; a lone `|` (or `|&`) separates stages. Redirections such as
589
- // `2>&1` / `&>` are not separators.
590
- function shellPipelineStages(command) {
591
- const text = String(command || '');
592
- const pipelines = [];
593
- let stages = [];
591
+ // Bounded shell exit-proof recognition, not a shell interpreter. Keep separators so a
592
+ // later echo/OR/background job cannot turn a child runner failure into verification PASS.
593
+ function verificationShellUnits(command) {
594
+ const units = [];
594
595
  let buffer = '';
595
596
  let quote = null;
596
597
  let escaped = false;
597
- const closeStage = () => {
598
- if (buffer.trim()) stages.push(buffer.trim());
599
- buffer = '';
600
- };
601
- const closePipeline = () => {
602
- closeStage();
603
- if (stages.length) pipelines.push(stages);
604
- stages = [];
605
- };
606
- for (let i = 0; i < text.length; i += 1) {
607
- const char = text[i];
598
+ const close = (separator) => { units.push({ text: buffer.trim(), separator }); buffer = ''; };
599
+ for (let i = 0; i < command.length; i += 1) {
600
+ const char = command[i];
608
601
  if (escaped) { escaped = false; buffer += char; continue; }
609
- if (char === '\\') { escaped = true; buffer += char; continue; }
602
+ if (char === '\\' && quote !== "'") { escaped = true; buffer += char; continue; }
610
603
  if (quote) { if (char === quote) quote = null; buffer += char; continue; }
611
604
  if (char === "'" || char === '"') { quote = char; buffer += char; continue; }
612
- if (char === '|') {
613
- if (text[i + 1] === '|') { closePipeline(); i += 1; continue; }
614
- closeStage();
615
- if (text[i + 1] === '&') i += 1;
605
+ if (char === '#' && (!buffer || /\s$/.test(buffer))) {
606
+ while (i < command.length && command[i] !== '\n') i += 1;
607
+ if (i < command.length) close(';');
616
608
  continue;
617
609
  }
618
- if (char === '&') {
619
- if (text[i + 1] === '&') { closePipeline(); i += 1; continue; }
620
- buffer += char; // `2>&1`, `&>file`, trailing background `&`
610
+ if (char === '&' && (/[<>]$/.test(buffer) || command[i + 1] === '>')) { buffer += char; continue; }
611
+ if (char === '|' || char === '&') {
612
+ const next = command[i + 1];
613
+ const separator = next === char ? char + next : char === '|' && next === '&' ? '|&' : char;
614
+ close(separator);
615
+ if (separator.length === 2) i += 1;
621
616
  continue;
622
617
  }
623
- if (char === ';' || char === '\n') { closePipeline(); continue; }
618
+ if (char === ';' || char === '\n' || char === '\r') { close(';'); continue; }
624
619
  buffer += char;
625
620
  }
626
- closePipeline();
627
- return pipelines;
621
+ if (quote || escaped) return null;
622
+ if (buffer.trim()) close('');
623
+ return units.filter((unit) => unit.text || unit.separator !== ';');
628
624
  }
629
625
 
630
- // A shell pipeline exits with its LAST stage's status, so `npx vitest run | tail` is exit 0
631
- // even when vitest failed. A verification command in a non-final stage therefore has an
632
- // unproven exit status — unless the command enables `pipefail` or reads `PIPESTATUS`.
633
- // Returns the masked verification stage (string) or null.
634
- function maskedVerificationStage(command) {
635
- const text = String(command || '');
636
- if (/\bpipefail\b|\bPIPESTATUS\b|\bpipestatus\b/.test(text)) return null;
637
- for (const stages of shellPipelineStages(text)) {
638
- for (const stage of stages.slice(0, -1)) {
639
- if (isVerificationCommand(stage)) return stage;
626
+ function maskedVerificationStage(command, depth = 0) {
627
+ const text = String(command || '').trim();
628
+ const units = verificationShellUnits(text);
629
+ if (!units?.length || depth > 2) return text;
630
+ let pipefail = false;
631
+ let sawRunner = false;
632
+ for (let i = 0; i < units.length; i += 1) {
633
+ const unit = units[i];
634
+ const plain = maskQuotedLiterals(unit.text);
635
+ // Unsupported flow may skip a runner or discard an earlier iteration's failure.
636
+ // Parens, braces and `if` are not interpreted here. A sequential consumer must not
637
+ // turn the status of an earlier runner into PASS, even when it prints that status.
638
+ if (sawRunner && units[i - 1]?.separator === ';' && !/^[)}]+$/.test(plain)) return unit.text;
639
+ // Command position only: `vitest -t case` is an argument, not a loop.
640
+ if (/^(?:for|while|until|case|function|done)\b/.test(plain)) return unit.text;
641
+ // A later `exit 0`/`true`/`:` replaces the runner's status; a leading `!` inverts it.
642
+ if (sawRunner && /^(?:exit\s+0|true|:)$/.test(plain)) return unit.text;
643
+ if (/^!\s/.test(plain) && isVerificationCommand(unit.text)) return unit.text;
644
+ if (unit.separator === '||' || unit.separator === '&') return unit.text;
645
+ const enable = /^set\s+-(?:[a-z]*o)\s+pipefail$/.test(plain);
646
+ const disable = /^set\s+\+(?:[a-z]*o)\s+pipefail$/.test(plain);
647
+ if (enable || disable) pipefail = enable;
648
+ let runner = isVerificationCommand(unit.text);
649
+ // Only an entire simple shell wrapper can inherit the inner script's proof.
650
+ const wrapper = unit.text.match(/^(?:bash|sh|zsh|dash|ksh)\s+(?:-o\s+\w+\s+|-[a-z]+\s+)*(-[a-z]*c)\s+(?:'([^']*)'|"((?:[^"\\]|\\.)*)")$/);
651
+ if (runner && wrapper) {
652
+ if (maskedVerificationStage(wrapper[2] ?? wrapper[3] ?? '', depth + 1)) return unit.text;
653
+ } else if (runner && /(?:^|\s)(?:bash|sh|zsh|dash|ksh)\s/.test(plain)) return unit.text;
654
+ if (runner) sawRunner = true;
655
+ if (unit.separator === '|' || unit.separator === '|&') {
656
+ if (runner && !pipefail) {
657
+ // A single runner's immediate Bash PIPESTATUS read can propagate its status.
658
+ let end = i;
659
+ while (units[end]?.separator === '|' || units[end]?.separator === '|&') end += 1;
660
+ const proof = units[end + 1];
661
+ const expected = `exit ${'${'}PIPESTATUS[${i - (units.slice(0, i).findLastIndex((u) => u.separator !== '|' && u.separator !== '|&') + 1)}]}`;
662
+ if (units[end]?.separator !== ';' || proof?.text !== expected || proof.separator
663
+ || units.slice(i + 1, end + 1).some((u) => isVerificationCommand(u.text))) return unit.text;
664
+ return null;
665
+ }
666
+ continue;
640
667
  }
641
668
  }
642
- return null;
669
+ return sawRunner ? null : text;
643
670
  }
644
671
 
645
672
  // Announce through the in-proc hook emitter only (never fd 1 from library callers).
646
673
  function noteUnprovenExitStatus(stage) {
647
674
  if (!ledgerMessageEmitter) return;
648
675
  const message =
649
- `UKit execution-ledger: \`${stage}\` ran in a pipeline, so the exit status belongs to the last pipe stage `
650
- + '(e.g. tail/grep), not the test runner — it is NOT counted as passing verification. '
651
- + 'Re-run with `set -o pipefail; <cmd> | tail` (or without the pipe) to prove the result.';
676
+ `UKit execution-ledger: runner exit status is unproven for \`${stage}\` — `
677
+ + 'the shell wrapper may mask or skip the runner; it is NOT counted as passing verification. '
678
+ + 'Re-run the bare verification command in the same workspace, without loops, backgrounding or status-swallowing consumers.';
652
679
  try {
653
680
  ledgerMessageEmitter(`${JSON.stringify({ systemMessage: message })}\n`);
654
681
  } catch { /* an emitter failure never loses the ledger event */ }
@@ -3532,8 +3559,32 @@ export function promptOrdersNpmPublish(state = {}) {
3532
3559
  return ordered && !prohibited;
3533
3560
  }
3534
3561
 
3562
+ // Conditional repair is not a publication order until owned project source actually changes.
3563
+ // Reuse the affirmative publication parser on the taken branch, including its negation rules.
3564
+ function conditionalRepairOrdersPublish(state, ledger, cwd) {
3565
+ if (ledger?.substantiveWrite !== true || !cwd) return false;
3566
+ const text = String(state?.routingContext?.lastExplicitUserPromptText || '')
3567
+ .normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase()
3568
+ .replace(/```[^]*?```|~~~[^]*?~~~/g, '').replace(/^[ \t]*>.*$/gm, '');
3569
+ if (!/(?:^|\n)[ \t*#_-]*(?:muc tieu|goal|purpose|objective)\s*:[^\n]*\b(?:xac nhan|verify|confirm|validate)\b/.test(text)) return false;
3570
+ const branch = text.match(/(?:^|\n|\.\s+)[ \t]*(?:\d+[.)]\s*)?(?:neu\s+co\s+source fix|if\s+(?:there is a\s+)?source fix)\s*:[^\n]*/)?.[0];
3571
+ if (!branch) return false;
3572
+ const writes = [...(ledger.receipts || []), ...Object.keys(ledger.bankedWrites || {}).map((file) => ({ kind: 'write', success: true, file }))];
3573
+ const projectWrite = writes.some((receipt) => {
3574
+ if (receipt.kind !== 'write' || receipt.success !== true || !receipt.file || isBookkeepingWrite(receipt.file)) return false;
3575
+ if (receipt.detail === 'bash-workspace-delta' && /^bash-workspace:/.test(receipt.file)) return true;
3576
+ const relative = path.relative(path.resolve(cwd), path.resolve(cwd, receipt.file));
3577
+ return relative && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)
3578
+ && !/^(?:\.ukit|\.cache|node_modules|tests?|fixtures?|tmp)(?:[\\/]|$)/.test(relative);
3579
+ });
3580
+ if (!projectWrite) return false;
3581
+ // Explicit task-wide publication prohibitions still dominate the conditional branch.
3582
+ if (/(?:do not|never|khong|chua)\s+(?:npm\s+)?publish\b|\bno\s+(?:release|publication)\b/.test(text)) return false;
3583
+ return promptOrdersNpmPublish({ routingContext: { lastExplicitUserPromptText: branch.replace(/^.*?:/, 'Run ') } });
3584
+ }
3585
+
3535
3586
  function releasePublicationMissing(state, ledger, cwd) {
3536
- if (!promptOrdersNpmPublish(state)) return null;
3587
+ if (!promptOrdersNpmPublish(state) && !conditionalRepairOrdersPublish(state, ledger, cwd)) return null;
3537
3588
  let pkg = null;
3538
3589
  try { pkg = JSON.parse(fsSync.readFileSync(path.join(cwd || process.cwd(), 'package.json'), 'utf8')); } catch { return null; }
3539
3590
  if (!pkg?.version) return null;