@ngockhoale/ukit 3.4.51 → 3.4.53

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,23 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.53 - 2026-10-22
6
+
7
+ **Stop-level closure of the UNPROVEN verdict (follow-up to 3.4.52).**
8
+ - `recoveryInstruction`: a non-current `unproven` verdict no longer yields "verdict is stale, re-run at current head"; it names the real gap (masked pipe, exit status unproven) and never "fix the failure".
9
+ - `stop-coordinator`: the `verify:pass` predicate and `goalVerificationVerdict` treat `unproven` as unmet but report "unproven (exit status not proven)", not a verified failure; `unproven` never counts as a pass.
10
+ - New Stop-level regressions (`tests/hooks/executionLedgerStopLifecycle.test.js`, GC-03e): UNPROVEN neither failure nor PASS; exhausted retry budget ends visibly without a recovery loop or write demand; same-task same-command PASS clears its own blocker, a different command / UNPROVEN rerun / another task does not; a superseded failure stops producing recovery text.
11
+ - Note: `tests/handoff/**` is excluded from `vitest`; run `tests/handoff/goalCompletion.test.js` with `node --test` (vitest exits 1 with "No test files found").
12
+ - Verified offline on macOS/Node 22; no live host proof.
13
+
14
+ ## 3.4.52 - 2026-10-22
15
+
16
+ **Verification-loop ledger integrity (post-3.4.51 follow-up).**
17
+ - Fix: the fd number of a redirection (`2>&1`, `2>/dev/null`, `12>&1`) leaked into the verification-loop fingerprint as an argument, so `... .test.js 2>&1 | tail` was reported as `... .test.js 2`. Genuine numeric arguments and quoted text are untouched.
18
+ - A masked-pipe exit (`exitStatusUnproven`) now carries its own non-satisfying `unproven` verdict kind and is no longer counted as a definite failure toward the verification-loop streak.
19
+ - A later valid PASS of the same command on the same task/ledger clears its own sticky `verification-loop` blocker; evidence never crosses tasks or ledgers. Wrapper exit 0 still never becomes PASS.
20
+ - Regressions: `tests/hooks/executionLedger*.test.js`, `verificationExitProof`. Verified offline on macOS/Node 22; live host (Claude Code/OMP) in-memory state unverified, reload/`ukit update` + restart needed to load the fix.
21
+
5
22
  ## 3.4.51 - 2026-10-21
6
23
 
7
24
  **Handoff orchestration integrity (CKit/OMP 18.12.5 incident audit).**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.51",
3
+ "version": "3.4.53",
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",
@@ -801,10 +801,13 @@ function compactReceipt(receipt) {
801
801
 
802
802
  // --- Typed verdicts (SPEC-typed-verdicts §2) -----------------------------------
803
803
  // One optional verdict record rides the ledger entry — additive, no new store.
804
- // kind: 'live-verified' | 'test-verified' | 'check-only' | 'blocked' | 'failed'.
805
- // Currency is proven by headSha + baseSha + patch-id, not by commit messages or a
806
- // green check from an older SHA.
807
- const VERDICT_KINDS = new Set(['live-verified', 'test-verified', 'check-only', 'blocked', 'failed']);
804
+ // kind: 'live-verified' | 'test-verified' | 'check-only' | 'blocked' | 'failed'
805
+ // | 'unproven'.
806
+ // 'unproven' is a masked wrapper exit 0 (pipe/`sh -c` consumer): the runner may have
807
+ // passed or failed — the receipt proves NEITHER, so it is never a satisfying kind
808
+ // (fail-closed) and never a definite 'failed'. Currency is proven by headSha + baseSha
809
+ // + patch-id, not by commit messages or a green check from an older SHA.
810
+ const VERDICT_KINDS = new Set(['live-verified', 'test-verified', 'check-only', 'blocked', 'failed', 'unproven']);
808
811
  const VERDICT_SATISFYING_KINDS = new Set(['test-verified', 'live-verified']);
809
812
  const MAX_VERDICT_HISTORY = 8;
810
813
 
@@ -1133,7 +1136,10 @@ function mintVerificationVerdict(receipt, payload, projectRoot, sessionRoot = pr
1133
1136
  }
1134
1137
  let kind = supplied.kind;
1135
1138
  if (!VERDICT_KINDS.has(kind)) {
1136
- if (receipt.success !== true) kind = 'failed';
1139
+ // A masked wrapper exit 0 proves the runner's status neither way: its own kind,
1140
+ // never 'failed' (the runner may have passed) and never a satisfying kind.
1141
+ if (receipt.exitStatusUnproven === true) kind = 'unproven';
1142
+ else if (receipt.success !== true) kind = 'failed';
1137
1143
  else kind = isTestGateCommand(receipt.command) ? 'test-verified' : 'check-only';
1138
1144
  }
1139
1145
  return {
@@ -1220,6 +1226,18 @@ function terminalShellCommandUnit(command) {
1220
1226
  ) next += 1;
1221
1227
  return next;
1222
1228
  };
1229
+ // A redirection's leading IO number (`2>&1`, `2> err.log`, `3< in`) belongs to the stream
1230
+ // operator, not to the command: drop a trailing digit run ONLY when it is the whole
1231
+ // current shell word (preceded by whitespace or the start of the buffer). The scanner
1232
+ // only reaches redirections outside quotes, so a quoted literal (`-t "2" > out`) ends in
1233
+ // a quote character and is never stripped; an ordinary numeric argument separated from
1234
+ // the operator by whitespace (`--testTimeout 2 > out`, `-n 2 > out`) keeps its digits.
1235
+ const stripIoNumber = () => {
1236
+ let end = pipeBuffer.length;
1237
+ while (end > 0 && /\d/.test(pipeBuffer[end - 1])) end -= 1;
1238
+ if (end === pipeBuffer.length || (end > 0 && !/\s/.test(pipeBuffer[end - 1]))) return;
1239
+ pipeBuffer = pipeBuffer.slice(0, end);
1240
+ };
1223
1241
 
1224
1242
  for (let index = 0; index < text.length; index += 1) {
1225
1243
  const char = text[index];
@@ -1252,6 +1270,7 @@ function terminalShellCommandUnit(command) {
1252
1270
  }
1253
1271
  if (char === '$' && text[index + 1] === '(') return null;
1254
1272
  if (char === '>' || char === '<') {
1273
+ stripIoNumber();
1255
1274
  let next = index + 1;
1256
1275
  if (text[next] === '>' && char === '>') next += 1;
1257
1276
  if (text[next] === '&') next += 1;
@@ -2479,7 +2498,13 @@ function applyReceiptCore(ledger, receipt, { vibecode = false } = {}) {
2479
2498
  }
2480
2499
  next.verificationAttempted = true;
2481
2500
  next.verificationSucceeded = next.verificationSucceeded || receipt.success;
2482
- next.verificationFailed = next.verificationFailed || !receipt.success;
2501
+ // SPEC-typed-verdicts §2: an UNPROVEN run (masked wrapper exit 0) is neither a pass
2502
+ // nor a definite failure — it must not grow failure counts, the loop streak or a
2503
+ // blocker, and it never moves `verificationFailed`. Only a PROVEN non-zero exit is a
2504
+ // definite failure; fail-closed acceptance is unchanged (only a current
2505
+ // test-verified/live-verified verdict ever satisfies the gate).
2506
+ const unproven = receipt.exitStatusUnproven === true;
2507
+ next.verificationFailed = next.verificationFailed || (!receipt.success && !unproven);
2483
2508
  // TASK-026 fix round 1: bank the FIRST successful run of this command (same
2484
2509
  // terminal-shell identity the loop tracker uses), so rerunning a banked check is a
2485
2510
  // replay — never progress — regardless of receipt-window eviction.
@@ -2510,7 +2535,8 @@ function applyReceiptCore(ledger, receipt, { vibecode = false } = {}) {
2510
2535
 
2511
2536
  // Verification-loop tracking. `terminalShellCommandUnit` gives the loop identity:
2512
2537
  // the same failing check rerun — `setup && yarn test` and `yarn test 2>&1 | tail`
2513
- // both count as the same command, while a different check resets the streak.
2538
+ // both count as the same command (redirection IO numbers are stripped), while a
2539
+ // different check resets the streak.
2514
2540
  const fingerprint = terminalShellCommandUnit(receipt.command) || receipt.command;
2515
2541
  const counts = { ...(next.verificationFailureCounts || {}) };
2516
2542
  if (receipt.success) {
@@ -2519,7 +2545,18 @@ function applyReceiptCore(ledger, receipt, { vibecode = false } = {}) {
2519
2545
  if (next.failedVerificationStreak?.fingerprint === fingerprint) {
2520
2546
  next.failedVerificationStreak = null;
2521
2547
  }
2522
- } else {
2548
+ // A valid, newer pass of the SAME check clears its OWN verification-loop blocker:
2549
+ // same terminal-shell identity the tracker minted it with, applied to this task's
2550
+ // ledger in receive order (a replayed older event is deduped by journalSeen, and a
2551
+ // foreign task never reaches this ledger — recordLedgerEvent rejects
2552
+ // receipt-task-mismatch and a new task re-keys to a fresh ledger). Unrelated
2553
+ // command evidence never clears another check's blocker; an unproven or failed
2554
+ // run (success:false) never clears anything, so the exhausted-retry blocker still
2555
+ // ends the run instead of inviting an automatic recovery loop.
2556
+ if (next.blocker?.kind === 'verification-loop' && next.blocker.command === fingerprint) {
2557
+ next.blocker = null;
2558
+ }
2559
+ } else if (!unproven) {
2523
2560
  const streakFingerprint = next.failedVerificationStreak?.fingerprint;
2524
2561
  const streakCount = streakFingerprint === fingerprint
2525
2562
  ? Number(next.failedVerificationStreak.count || 0) + 1
@@ -3446,7 +3483,9 @@ function recoveryInstruction(missingEvidence, ledger = {}, routeSummary = {}, {
3446
3483
  if (!instruction && ledger.verdict && typeof ledger.verdict === 'object') {
3447
3484
  if (ledger.verdict.kind === 'check-only') {
3448
3485
  instruction = 'check-only does not satisfy this lane; verify on the real surface (run the feature / inspect the artifact).';
3449
- } else {
3486
+ } else if (ledger.verdict.kind !== 'unproven') {
3487
+ // An UNPROVEN verdict never satisfied the gate, so its currency is irrelevant: the
3488
+ // gap is the missing exit status (named below), not a stale head.
3450
3489
  const currency = verdictCurrent(ledger.verdict, { cwd });
3451
3490
  if (currency.current !== true) {
3452
3491
  const sha = ledger.verdict.headSha ? String(ledger.verdict.headSha).slice(0, 12) : 'unknown';
@@ -3459,8 +3498,9 @@ function recoveryInstruction(missingEvidence, ledger = {}, routeSummary = {}, {
3459
3498
  }
3460
3499
  // A pipe-masked run is UNPROVEN, not failed: name the real gap (no provable exit status)
3461
3500
  // instead of telling the model to "fix the failure" of a run that may have passed.
3501
+ // Unproven never sets `verificationFailed`, so this must not key off that flag.
3462
3502
  const lastVerification = [...(ledger.receipts || [])].reverse().find((r) => r?.kind === 'verification');
3463
- if (!instruction && ledger.verificationFailed && lastVerification?.exitStatusUnproven === true) {
3503
+ if (!instruction && lastVerification?.exitStatusUnproven === true) {
3464
3504
  instruction = `The last verification (\`${String(lastVerification.command || '').slice(0, 80)}\`) ran in a pipeline, so its exit status is unproven. Re-run it without the pipe, or with \`set -o pipefail\`, and inspect the exit code.`;
3465
3505
  }
3466
3506
  if (!instruction && ledger.verificationFailed) {
@@ -1065,9 +1065,14 @@ async function evalVerifyPass(projectRoot) {
1065
1065
  for (let i = receipts.length - 1; i >= 0; i -= 1) {
1066
1066
  const r = receipts[i];
1067
1067
  if (!r || typeof r !== 'object' || r.kind !== 'verification') continue;
1068
- if (r.success === true && r.verdict?.kind !== 'failed') {
1068
+ if (r.success === true && r.verdict?.kind !== 'failed' && r.verdict?.kind !== 'unproven') {
1069
1069
  return { pass: true, detail: 'last verification passed' };
1070
1070
  }
1071
+ // A masked-pipe run (exit status unproven) is not a verified failure: same fail-closed
1072
+ // result, but the detail must say what is actually missing.
1073
+ if (r.exitStatusUnproven === true || r.verdict?.kind === 'unproven') {
1074
+ return { pass: false, detail: 'last verification unproven (exit status not proven)' };
1075
+ }
1071
1076
  return { pass: false, detail: 'last verification failed' };
1072
1077
  }
1073
1078
  return { pass: false, detail: 'no verification receipt recorded' };
@@ -1136,6 +1141,7 @@ function goalVerificationVerdict(ledger) {
1136
1141
  const verdict = typeof ledger.verdict === 'string' ? ledger.verdict.trim().toLowerCase() : null;
1137
1142
  if (verdict === 'pass' || verdict === 'passed' || verdict === 'verified') return { pass: true, detail: 'verification verdict: pass' };
1138
1143
  if (verdict === 'fail' || verdict === 'failed') return { pass: false, detail: 'verification verdict: fail' };
1144
+ if (verdict === 'unproven') return { pass: false, detail: 'verification verdict: unproven (exit status not proven)' };
1139
1145
  if (ledger.verified === true) return { pass: true, detail: 'verification: verified' };
1140
1146
  if (ledger.verified === false) return { pass: false, detail: 'verification: not verified' };
1141
1147
  if (typeof ledger.verify === 'string' && ledger.verify.trim().toLowerCase() === 'pass') return { pass: true, detail: 'verify: pass' };