@ngockhoale/ukit 3.4.36 → 3.4.37

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,13 @@ 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.37 - 2026-10-12
13
+
14
+ **Completion Gate: a clean, in-sync branch observation discharges a masked commit+push.**
15
+ - 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`.
16
+ - 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.
17
+ - Regression: `tests/hooks/gitSyncPostcondition.test.js` (RED on 3.4.36). No real Windows run.
18
+
12
19
  ## 3.4.36 - 2026-10-12
13
20
 
14
21
  **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.37",
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",
@@ -51,6 +51,9 @@ 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
+ // Whole-tree branch status: a clean tree whose branch line carries no [ahead]/[behind]/gone marker proves
55
+ // commit + push landed regardless of how they were run (a masked heredoc chain mints no mutation receipt).
56
+ const GIT_SYNC = /^git\s+status\s+(?:-sb|-bs|--short\s+--branch|--branch\s+--short)\s*$/;
54
57
  const DRY_RUN = /(?:^|\s)(?:--dry-run|--check|--no-act|-n)(?:\s|=|$)/;
55
58
  const CURL = /^curl\s/;
56
59
  function requiredTargetOf(segment, required) {
@@ -68,11 +71,15 @@ export function bashOperationReceipt(command, response, transportSuccess, goal,
68
71
  const real = command.split('&&').map((seg) => seg.trim());
69
72
  const text = responseText(response);
70
73
  const hasOutputField = typeof response === 'string' || typeof response?.stdout === 'string' || typeof response?.output === 'string';
71
- let role = null; let proven = false; let target = null;
74
+ let role = null; let proven = false; let target = null; let synced = false;
72
75
  for (const seg of real) {
73
76
  if (!seg || /^cd\s+\S+$/.test(seg) || DRY_RUN.test(seg.replace(/'[^']*'|"[^"]*"/g, '""'))) continue;
74
77
  if (domain === 'git-integrity') {
75
- if (GIT_STATE.test(seg)) {
78
+ if (GIT_SYNC.test(seg)) {
79
+ const lines = text.split('\n').map((l) => l.trimEnd()).filter(Boolean);
80
+ role = 'state'; synced = hasOutputField && lines.length === 1 && /^## [^\s.]\S*\.\.\.\S+$/.test(lines[0]);
81
+ proven = synced;
82
+ } else if (GIT_STATE.test(seg)) {
76
83
  const wanted = goal.check;
77
84
  const kind = /\bfsck\b/.test(seg) ? 'fsck' : /\bdiff\b/.test(seg) ? 'diff-check' : 'status';
78
85
  // `-- <pathspec>` / extra paths narrow the check; only a whole-tree status proves a clean tree.
@@ -106,7 +113,7 @@ export function bashOperationReceipt(command, response, transportSuccess, goal,
106
113
  return { kind: 'operation', success: ok, transportSuccess: transportSuccess === true, harness,
107
114
  ...(masked ? { exitStatusUnproven: true } : {}),
108
115
  operation: { domain, role, scope, target: role === 'state' ? null : target,
109
- ...(role === 'state' ? { state: 'verified', complete: ok } : {}) } };
116
+ ...(role === 'state' ? { state: 'verified', complete: ok, ...(synced && ok ? { synced: true } : {}) } : {}) } };
110
117
  }
111
118
 
112
119
  // Release evidence: `npm publish` and `npm view <pkg> version` as REAL Bash tool results. The command must
@@ -12,7 +12,7 @@ export function compactOperation(operation) {
12
12
  return { domain: operation.domain, scope: operation.scope, role: operation.role, target: operation.target,
13
13
  ...(operation.state !== undefined ? { state: operation.state } : {}),
14
14
  ...(operation.presentTargets !== undefined ? { presentTargets: operation.presentTargets === null ? null : [...operation.presentTargets] } : {}),
15
- complete: operation.complete === true };
15
+ complete: operation.complete === true, ...(operation.synced === true ? { synced: true } : {}) };
16
16
  }
17
17
  export function foldOperationEvidence(bank = {}, receipt, ledger) {
18
18
  const raw = receipt?.operation;
@@ -46,6 +46,7 @@ export function foldOperationEvidence(bank = {}, receipt, ledger) {
46
46
  ...(op.state !== undefined ? { state: op.state } : {}),
47
47
  ...(op.presentTargets !== undefined ? { presentTargets: [...op.presentTargets] } : {}),
48
48
  complete: op.complete === true,
49
+ ...(op.synced === true ? { synced: true } : {}),
49
50
  } };
50
51
  return { ...bank, seenToolUseIds: receipt.toolUseId ? [...seen, receipt.toolUseId] : seen,
51
52
  [key]: { ...prior, [op.role]: proof } };
@@ -75,6 +76,10 @@ export function operationEvidenceStatus(goal, ledger = {}) {
75
76
  && (absent || (!Object.hasOwn(op || {}, 'presentTargets') && op?.target === goal.target));
76
77
  if (!observation?.success || !observation.toolUseId || !Number.isSafeInteger(observation.sequence)
77
78
  || op.complete !== true || !stateMatches) return incomplete;
79
+ // Independent postcondition: a trusted clean+in-sync branch observation made AFTER every recorded git
80
+ // mutation attempt (failed or masked ones included) proves commit/push landed. Never for other targets.
81
+ if (goal.domain === 'git-integrity' && op.synced === true && requiredTargets.every((t) => t === 'commit' || t === 'push')
82
+ && attempts.every((r) => observation.sequence > r.sequence)) return { complete: true, operationProof: 'synced-state' };
78
83
  if (goal.completionSemantics === 'ensure-state' && attempts.length === 0) {
79
84
  return { complete: true, operationProof: 'desired-state-only' };
80
85
  }