@ngockhoale/ukit 3.4.36 → 3.4.38

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
@@ -9,6 +9,20 @@ All notable changes to UKit are documented here.
9
9
  - New `/ukit:ci-check` command + `ci-check.mjs` helper (core pack): prechecks, fail-closed budget guard, local preflight, at most one dispatch per invocation, write-ahead run-id state, `status` (never dispatches), `--ref`, `--dry-run`, per-OS report. Never pushes, merges or publishes.
10
10
  - Default-branch bootstrap stub documented in `docs/design/CI-CROSS-PLATFORM-MATRIX.md` (requires separate approval).
11
11
 
12
+ ## 3.4.38 - 2026-10-12
13
+
14
+ **Completion Gate safety closure: mutation orders always get a contract; git sync needs authoritative remote proof.**
15
+ - Issue 1 root cause: `isInformationalPrompt`'s consult-shape (a stray "Should I …?" / "…hay không" / "có phải") overrode a surviving imperative, so a real mutation order routed `informational` with no completion contract and Stop released with a non-blocking "nothing was verified" notice. Fix (router + `route-task.mjs` parity): a consult shape never demotes a prompt that still has a mutation order. Pure consults ("Should I fix X?") stay informational and never demand write evidence.
16
+ - Issue 2: the 3.4.37 `git status -sb` `synced-state` proof read a local tracking ref (stale-able) and is removed. A masked commit/push is now discharged only by a standalone, unmasked `git ls-remote <remote> refs/heads/<branch>` observed after every mutation attempt whose sha equals live HEAD, on the branch's own tracking remote, with a clean worktree (gate-time `gitAuthority`). Remote behind, stale tracking ref, uncommitted changes, wrong remote/branch, forged/piped/failed output, other task/session: BLOCK.
17
+ - Regressions through the real hook chain: `tests/hooks/completionClosure.test.js`; unit: `gitSyncPostcondition.test.js`. No real Windows run.
18
+
19
+ ## 3.4.37 - 2026-10-12
20
+
21
+ **Completion Gate: a clean, in-sync branch observation discharges a masked commit+push.**
22
+ - Root cause: commit/push run inside a heredoc/`;`/pipe chain correctly mint an exit-unproven (failed) mutation receipt, and no later independent observation could ever discharge the `git-integrity` goal, so a finished, published release kept blocking Stop with `operation-verification`.
23
+ - Fix: a standalone whole-tree `git status -sb` that prints exactly one `## branch...upstream` line (clean, no ahead/behind/gone) after every recorded git mutation attempt is a trusted `synced-state` proof for commit/push goals. Piped/failed/dirty/ahead/behind/no-upstream/earlier/other-task observations never count.
24
+ - Regression: `tests/hooks/gitSyncPostcondition.test.js` (RED on 3.4.36). No real Windows run.
25
+
12
26
  ## 3.4.36 - 2026-10-12
13
27
 
14
28
  **Completion Gate: a finished task (incl. a release) stays finished across wakeups, compaction and repeated Stops.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.36",
3
+ "version": "3.4.38",
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",
@@ -2068,6 +2068,9 @@ function applyTaskSemantics(contract, semantic) {
2068
2068
  || /\b(?:chua|duoc khong|dung khong|khong nhi|nhi|roi)\s*[.!?]*\s*$/.test(folded)
2069
2069
  || (!politeAsk && /\b(?:khong|không)\s*[?.!]?\s*$/.test(folded))
2070
2070
  || /\bchi\s+(?:hoi|tu\s+van|giai\s+thich|xem)\b/.test(folded);
2071
+ // P1: a consult-shaped clause ("Should I also bump…?", a Vietnamese tail) must never demote a prompt that
2072
+ // still carries a real imperative order — otherwise the order loses its completion contract.
2073
+ if (consultShape && hasMutationOrder({ promptText, commandText })) return false;
2071
2074
  if (scores) {
2072
2075
  if (!consultShape && (
2073
2076
  scores.implementSignal
@@ -4325,6 +4325,9 @@ function isInformationalPrompt({
4325
4325
  || /\b(?:chua|duoc khong|dung khong|khong nhi|nhi|roi)\s*[.!?]*\s*$/.test(folded)
4326
4326
  || (!politeAsk && /\b(?:khong|không)\s*[?.!]?\s*$/.test(folded))
4327
4327
  || /\bchi\s+(?:hoi|tu\s+van|giai\s+thich|xem)\b/.test(folded);
4328
+ // P1: a consult-shaped clause ("Should I also bump…?", a Vietnamese tail) must never demote a prompt that
4329
+ // still carries a real imperative order — otherwise the order loses its completion contract.
4330
+ if (consultShape && hasMutationOrder({ promptText, commandText })) return false;
4328
4331
  if (scores) {
4329
4332
  if (!consultShape && (
4330
4333
  scores.implementSignal
@@ -51,6 +51,10 @@ const GIT_MUTATION = /^git\s+(?:push|commit|clean|reset|merge|rebase|stash|tag|c
51
51
  const GIT_VERB = /^git\s+(push|commit|clean|reset|merge|rebase|stash|tag|checkout|restore|pull|cherry-pick|branch)\b/;
52
52
  const GIT_STATE = /^git\s+(?:fsck\b|diff\s+(?:--check|--exit-code|--quiet)\b|status\s+(?:--porcelain|-s|--short)\b)/;
53
53
  const SERVICE_MUTATION = /^(?:docker(?:\s+compose)?|docker-compose)\s+(?:restart|up|down|stop|start|build)\b|^(?:sudo\s+)?systemctl\s+(?:restart|start|stop|reload)\b|^pm2\s+(?:restart|reload|start)\b|^kubectl\s+(?:rollout|apply)\b|^launchctl\s+kickstart\b|^(?:\.\/)?(?:scripts\/)?deploy[\w.-]*(?:\s|$)|^(?:npm|yarn|pnpm)\s+(?:run\s+)?deploy\b/;
54
+ // Authoritative remote observation: `git ls-remote <remote> refs/heads/<branch>` asks the remote itself (a local
55
+ // tracking ref / `git status -sb` can be stale). The gate later requires that sha to equal live HEAD on the
56
+ // branch's own tracking remote with a clean tree, so a masked commit/push is recovered without re-running it.
57
+ const GIT_LS_REMOTE = /^git\s+ls-remote\s+(\S+)\s+(refs\/heads\/\S+)\s*$/;
54
58
  const DRY_RUN = /(?:^|\s)(?:--dry-run|--check|--no-act|-n)(?:\s|=|$)/;
55
59
  const CURL = /^curl\s/;
56
60
  function requiredTargetOf(segment, required) {
@@ -68,11 +72,20 @@ export function bashOperationReceipt(command, response, transportSuccess, goal,
68
72
  const real = command.split('&&').map((seg) => seg.trim());
69
73
  const text = responseText(response);
70
74
  const hasOutputField = typeof response === 'string' || typeof response?.stdout === 'string' || typeof response?.output === 'string';
71
- let role = null; let proven = false; let target = null;
75
+ let role = null; let proven = false; let target = null; let remoteSync = null;
72
76
  for (const seg of real) {
73
77
  if (!seg || /^cd\s+\S+$/.test(seg) || DRY_RUN.test(seg.replace(/'[^']*'|"[^"]*"/g, '""'))) continue;
74
78
  if (domain === 'git-integrity') {
75
- if (GIT_STATE.test(seg)) {
79
+ const lsRemote = seg.match(GIT_LS_REMOTE);
80
+ if (lsRemote) {
81
+ const lines = text.split('\n').map((l) => l.trimEnd()).filter(Boolean);
82
+ const hit = hasOutputField && lines.length === 1 ? lines[0].match(/^([0-9a-f]{40}|[0-9a-f]{64})\t(refs\/heads\/\S+)$/) : null;
83
+ role = 'state';
84
+ if (hit && hit[2] === lsRemote[2] && lsRemote[1].length <= 256 && lsRemote[2].length <= 256) {
85
+ remoteSync = { remote: lsRemote[1], ref: hit[2], sha: hit[1] };
86
+ proven = true;
87
+ }
88
+ } else if (GIT_STATE.test(seg)) {
76
89
  const wanted = goal.check;
77
90
  const kind = /\bfsck\b/.test(seg) ? 'fsck' : /\bdiff\b/.test(seg) ? 'diff-check' : 'status';
78
91
  // `-- <pathspec>` / extra paths narrow the check; only a whole-tree status proves a clean tree.
@@ -106,7 +119,7 @@ export function bashOperationReceipt(command, response, transportSuccess, goal,
106
119
  return { kind: 'operation', success: ok, transportSuccess: transportSuccess === true, harness,
107
120
  ...(masked ? { exitStatusUnproven: true } : {}),
108
121
  operation: { domain, role, scope, target: role === 'state' ? null : target,
109
- ...(role === 'state' ? { state: 'verified', complete: ok } : {}) } };
122
+ ...(role === 'state' ? { state: 'verified', complete: ok, ...(remoteSync && ok ? { remoteSync } : {}) } : {}) } };
110
123
  }
111
124
 
112
125
  // Release evidence: `npm publish` and `npm view <pkg> version` as REAL Bash tool results. The command must
@@ -3004,7 +3004,7 @@ function requiredEvidence(state = {}) {
3004
3004
  function evidenceSatisfied(evidence, ledger = {}, state = {}, { cwd } = {}) {
3005
3005
  const routeSummary = state?.routeSummary || {};
3006
3006
  if (evidence === 'operation-verification') {
3007
- return operationCompletionPolicy(scopedContract(state, ledger), null, ledger)?.complete === true;
3007
+ return operationCompletionPolicy(scopedContract(state, ledger), null, withGitAuthority(ledger, cwd))?.complete === true;
3008
3008
  }
3009
3009
  if (evidence === 'write-evidence') {
3010
3010
  const expected = routeSummary.expectedSourceFiles || [];
@@ -3272,6 +3272,21 @@ function scopedContract(state, ledger) {
3272
3272
  scope: contract.operationGoal.scope || (session ? `session:${session}` : null) } };
3273
3273
  }
3274
3274
 
3275
+ // Gate-time local git truth for the remote-parity proof (cwd-bound, so it lives here, not in the pure evidence module).
3276
+ function gitAuthority(cwd) {
3277
+ if (!cwd) return null;
3278
+ const head = gitOutput(['rev-parse', 'HEAD'], cwd);
3279
+ const branch = gitOutput(['symbolic-ref', '--short', 'HEAD'], cwd);
3280
+ if (!head || !branch) return null;
3281
+ const remote = gitOutput(['config', `branch.${branch}.remote`], cwd);
3282
+ const dirty = gitOutput(['status', '--porcelain'], cwd);
3283
+ return { head, branch, remote, clean: dirty === null && gitOutput(['rev-parse', '--git-dir'], cwd) !== null };
3284
+ }
3285
+ function withGitAuthority(ledger, cwd) {
3286
+ const gitAuth = ledger && Object.keys(ledger).length ? gitAuthority(cwd) : null;
3287
+ return gitAuth ? { ...ledger, gitAuthority: gitAuth } : ledger;
3288
+ }
3289
+
3275
3290
  function effectiveRequestLedger(state = {}, ledger = {}) {
3276
3291
  if (state?.sessionId && ledger?.sessionId && state.sessionId !== ledger.sessionId) return {};
3277
3292
  if (state?.taskInstanceId || ledger?.taskInstanceId) {
@@ -3394,7 +3409,7 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
3394
3409
  // continuation budget — the request already accumulated. A different prompt keeps a clean slate.
3395
3410
  const effectiveLedger = effectiveRequestLedger(state, ledger);
3396
3411
  const operationStatus = operationCompletionPolicy(scopedContract(state, effectiveLedger),
3397
- null, effectiveLedger);
3412
+ null, withGitAuthority(effectiveLedger, cwd));
3398
3413
  const operationalTask = operationStatus !== null;
3399
3414
  const missingEvidence = evidence.filter((item) => !evidenceSatisfied(item, effectiveLedger, state, { cwd }));
3400
3415
  if (operationalTask && !operationStatus.complete && !missingEvidence.includes('operation-verification')) missingEvidence.push('operation-verification');
@@ -1,5 +1,6 @@
1
1
  // Adapter-normalized proof only; neither native output nor agent prose is evidence here.
2
2
  const MAX_OPERATIONS = 16;
3
+ const validRemoteSync = (r) => Boolean(r) && typeof r === 'object' && ['remote', 'ref', 'sha'].every((k) => typeof r[k] === 'string' && r[k].length > 0 && r[k].length <= 256);
3
4
  export function compactOperation(operation) {
4
5
  if (!operation || typeof operation !== 'object' || Array.isArray(operation)) return { invalid: true };
5
6
  const bounded = (value) => typeof value === 'string' && value.length > 0 && value.length <= 256;
@@ -12,7 +13,7 @@ export function compactOperation(operation) {
12
13
  return { domain: operation.domain, scope: operation.scope, role: operation.role, target: operation.target,
13
14
  ...(operation.state !== undefined ? { state: operation.state } : {}),
14
15
  ...(operation.presentTargets !== undefined ? { presentTargets: operation.presentTargets === null ? null : [...operation.presentTargets] } : {}),
15
- complete: operation.complete === true };
16
+ complete: operation.complete === true, ...(validRemoteSync(operation.remoteSync) ? { remoteSync: { ...operation.remoteSync } } : {}) };
16
17
  }
17
18
  export function foldOperationEvidence(bank = {}, receipt, ledger) {
18
19
  const raw = receipt?.operation;
@@ -46,6 +47,7 @@ export function foldOperationEvidence(bank = {}, receipt, ledger) {
46
47
  ...(op.state !== undefined ? { state: op.state } : {}),
47
48
  ...(op.presentTargets !== undefined ? { presentTargets: [...op.presentTargets] } : {}),
48
49
  complete: op.complete === true,
50
+ ...(validRemoteSync(op.remoteSync) ? { remoteSync: { ...op.remoteSync } } : {}),
49
51
  } };
50
52
  return { ...bank, seenToolUseIds: receipt.toolUseId ? [...seen, receipt.toolUseId] : seen,
51
53
  [key]: { ...prior, [op.role]: proof } };
@@ -75,6 +77,15 @@ export function operationEvidenceStatus(goal, ledger = {}) {
75
77
  && (absent || (!Object.hasOwn(op || {}, 'presentTargets') && op?.target === goal.target));
76
78
  if (!observation?.success || !observation.toolUseId || !Number.isSafeInteger(observation.sequence)
77
79
  || op.complete !== true || !stateMatches) return incomplete;
80
+ // Independent postcondition (masked/failed commit+push recovery): the REMOTE itself (git ls-remote, observed
81
+ // after every mutation attempt) must report live HEAD on this branch's own tracking remote, with a clean tree.
82
+ // `ledger.gitAuthority` is the gate-time local truth; without it (or on any mismatch) nothing is proven.
83
+ const auth = ledger.gitAuthority;
84
+ if (goal.domain === 'git-integrity' && validRemoteSync(op.remoteSync) && auth && auth.clean === true
85
+ && requiredTargets.every((t) => t === 'commit' || t === 'push')
86
+ && attempts.every((r) => observation.sequence > r.sequence)
87
+ && op.remoteSync.sha === auth.head && op.remoteSync.ref === `refs/heads/${auth.branch}`
88
+ && op.remoteSync.remote === auth.remote) return { complete: true, operationProof: 'remote-parity' };
78
89
  if (goal.completionSemantics === 'ensure-state' && attempts.length === 0) {
79
90
  return { complete: true, operationProof: 'desired-state-only' };
80
91
  }