@ngockhoale/ukit 3.4.40 → 3.4.42
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,22 @@ 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.42 - 2026-10-13
|
|
13
|
+
|
|
14
|
+
**Completion Gate: `cd <repo>; <command>` is a checked command, so a finished release is recognized by the live Stop hook.**
|
|
15
|
+
- Live root cause (3.4.40/3.4.41 still blocked): agents run every Bash call as `cd <repo>; <cmd>`. The receipt classifier treated that `;` as shell masking, so `npm view`, `git ls-remote` and `git status --porcelain` minted no release/remote/clean-tree proof and Stop kept reporting `operation-verification, release-publication`. The runtime mirrors were byte-identical to the template; the evaluator was correct, the receipts never existed.
|
|
16
|
+
- Fix (`cron-operation-adapter.mjs`): exactly one leading `cd <dir>` followed by `;`/`&&` is dropped before classification (it prints nothing; the exit status is the next command's). Any other `;`, `||`, pipe, `echo $?` or second `cd` still masks, mints no proof, and cannot produce a false ACCEPT. A masked `npm publish > log; echo $?` still mints no publish receipt; release completion never required one: registry view == package.json version plus a tag that dereferences to HEAD plus live remote parity is the proof, so an immutable version is never republished to manufacture a receipt.
|
|
17
|
+
- Regressions: `tests/hooks/stopHookRelease.test.js` drives the real router, record and Stop hooks (bare, `cd ;`, `cd &&`; BLOCK then ACCEPT; wrong version, remote divergence, wrong tag, dirty tree, masked compounds, other session, runtime/template parity) and the adapter command shapes. Against the 3.4.41 adapter five of them fail.
|
|
18
|
+
|
|
19
|
+
## 3.4.41 - 2026-10-13
|
|
20
|
+
|
|
21
|
+
**Completion Gate: a published release is recognized, and tests no longer depend on the developer's real `~/.ukit`.**
|
|
22
|
+
- Root cause (release false block): the git-state `status` observation overwrote the standalone `git ls-remote` remote-parity proof stored under the same operation-bank key, so a verified release (registry, tag, remote, clean tree) still reported `operation-verification, release-publication` missing.
|
|
23
|
+
- Fix (`operation-evidence.mjs`): a later successful, complete git-state observation carries forward a still-valid remote-sync proof only when it was observed after every mutation attempt; the proof is judged against live HEAD/branch/tracking remote, so a diverged remote, wrong branch, other repo, dirty tree or later failed push still blocks.
|
|
24
|
+
- Verification integrity: the hook-spawning completion tests inherited the real user playbook directory when run inside the full suite (a bug-fix playbook then demanded repro receipts) and failed only there. They now run the hooks with an empty HOME.
|
|
25
|
+
- `integrationP1`: the external Laya offline harness is stale against its own server (NameError). That is reported as a loud per-test SKIPPED naming the cause instead of failing; it is not a UKit Completion Gate criterion. No `UKIT_LAYA_ROOT` bypass is used.
|
|
26
|
+
- Regressions: `tests/hooks/releaseCompletionEvidence.test.js` (R01–R08, R11, R13), extended `completionGateRequestScope.test.js` (R09, R10).
|
|
27
|
+
|
|
12
28
|
## 3.4.40 - 2026-10-12
|
|
13
29
|
|
|
14
30
|
**Completion Gate: a read-only request no longer owes `impact-evidence` it can never produce.**
|
package/package.json
CHANGED
|
@@ -57,12 +57,20 @@ const SERVICE_MUTATION = /^(?:docker(?:\s+compose)?|docker-compose)\s+(?:restart
|
|
|
57
57
|
const GIT_LS_REMOTE = /^git\s+ls-remote\s+(\S+)\s+(refs\/heads\/\S+)\s*$/;
|
|
58
58
|
const DRY_RUN = /(?:^|\s)(?:--dry-run|--check|--no-act|-n)(?:\s|=|$)/;
|
|
59
59
|
const CURL = /^curl\s/;
|
|
60
|
+
// A leading `cd <dir>;` / `cd <dir> &&` only selects the working directory: it prints nothing and the shell's
|
|
61
|
+
// exit status is the next command's, so it must not turn an otherwise single, checked command into a "masked"
|
|
62
|
+
// compound. Only ONE leading cd is dropped; any other `;`, `||`, pipe or extra segment still masks.
|
|
63
|
+
const LEADING_CD = /^\s*cd\s+(?:"[^"\n]*"|'[^'\n]*'|[^\s;&|<>$`()]+)\s*(?:;|&&)\s*(?=\S)/;
|
|
64
|
+
export function stripLeadingCd(command) {
|
|
65
|
+
return typeof command === 'string' ? command.replace(LEADING_CD, '') : command;
|
|
66
|
+
}
|
|
60
67
|
function requiredTargetOf(segment, required) {
|
|
61
68
|
if (!required.length) return null;
|
|
62
69
|
const tokens = segment.split(/\s+/);
|
|
63
70
|
return required.find((name) => tokens.some((tok) => tok === name || tok.endsWith(`/${name}`))) || '(other)';
|
|
64
71
|
}
|
|
65
|
-
export function bashOperationReceipt(
|
|
72
|
+
export function bashOperationReceipt(rawCommand, response, transportSuccess, goal, { sessionId, harness } = {}) {
|
|
73
|
+
const command = stripLeadingCd(rawCommand);
|
|
66
74
|
const domain = goal?.domain;
|
|
67
75
|
const scope = typeof sessionId === 'string' && sessionId.trim() ? `session:${sessionId.trim()}` : null;
|
|
68
76
|
if (!['git-integrity', 'service-runtime'].includes(domain) || typeof command !== 'string') return null;
|
|
@@ -134,8 +142,9 @@ export function bashOperationReceipt(command, response, transportSuccess, goal,
|
|
|
134
142
|
const NPM_PUBLISH = /^npm\s+publish(?:\s|$)/;
|
|
135
143
|
const NPM_VIEW = /^npm\s+view\s+(\S+?)(?:@\S+)?\s+version(?:\s+2>&1)?\s*$/;
|
|
136
144
|
const SEMVER_LINE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
137
|
-
export function releaseReceipt(
|
|
138
|
-
if (typeof
|
|
145
|
+
export function releaseReceipt(rawCommand, response, transportSuccess) {
|
|
146
|
+
if (typeof rawCommand !== 'string') return null;
|
|
147
|
+
const command = stripLeadingCd(rawCommand);
|
|
139
148
|
const masked = /;|\n|\|/.test(command.replace(/'[^']*'|"[^"]*"/g, '""'));
|
|
140
149
|
const segments = command.split('&&').map((seg) => seg.trim()).filter((seg) => seg && !/^cd\s+\S+$/.test(seg));
|
|
141
150
|
if (segments.length !== 1) return null;
|
|
@@ -40,6 +40,19 @@ export function foldOperationEvidence(bank = {}, receipt, ledger) {
|
|
|
40
40
|
if (receipt.toolUseId && (seen.includes(receipt.toolUseId) || Object.values(bank).some((entry) =>
|
|
41
41
|
[entry?.mutation, entry?.state].some((proof) => proof?.toolUseId === receipt.toolUseId)))) return bank;
|
|
42
42
|
if (receipt.toolUseId && (!bounded(receipt.toolUseId) || seen.length >= 256)) return { ...bank, overflow: true };
|
|
43
|
+
// A remote-parity observation (git ls-remote) must survive the clean-tree status check that normally follows
|
|
44
|
+
// it: both are `state` receipts under one key, so the later status used to overwrite the remote proof. The
|
|
45
|
+
// proof is stamped with the sequence it was observed at and carried over only while no mutation (failed or
|
|
46
|
+
// not) of this domain/scope was recorded after it; the gate still compares its sha to live HEAD.
|
|
47
|
+
const lastMutation = Object.values(bank).reduce((max, entry) => {
|
|
48
|
+
const m = entry?.mutation;
|
|
49
|
+
return m?.operation?.domain === op.domain && m.operation.scope === op.scope && Number.isSafeInteger(m.sequence) ? Math.max(max, m.sequence) : max;
|
|
50
|
+
}, 0);
|
|
51
|
+
const priorSync = prior.state?.operation?.remoteSync;
|
|
52
|
+
const remoteSync = validRemoteSync(op.remoteSync) ? { ...op.remoteSync, observedSequence: receipt.sequence }
|
|
53
|
+
: (op.role === 'state' && op.domain === 'git-integrity' && receipt.success === true && op.complete === true
|
|
54
|
+
&& validRemoteSync(priorSync) && Number.isSafeInteger(priorSync.observedSequence) && priorSync.observedSequence > lastMutation)
|
|
55
|
+
? { ...priorSync } : null;
|
|
43
56
|
const proof = { success: receipt.success === true, sequence: receipt.sequence,
|
|
44
57
|
toolUseId: receipt.toolUseId, taskInstanceId: receipt.taskInstanceId,
|
|
45
58
|
sessionKey: receipt.sessionKey, operation: {
|
|
@@ -47,7 +60,7 @@ export function foldOperationEvidence(bank = {}, receipt, ledger) {
|
|
|
47
60
|
...(op.state !== undefined ? { state: op.state } : {}),
|
|
48
61
|
...(op.presentTargets !== undefined ? { presentTargets: [...op.presentTargets] } : {}),
|
|
49
62
|
complete: op.complete === true,
|
|
50
|
-
...(
|
|
63
|
+
...(remoteSync ? { remoteSync } : {}),
|
|
51
64
|
} };
|
|
52
65
|
return { ...bank, seenToolUseIds: receipt.toolUseId ? [...seen, receipt.toolUseId] : seen,
|
|
53
66
|
[key]: { ...prior, [op.role]: proof } };
|
|
@@ -83,9 +96,14 @@ export function operationEvidenceStatus(goal, ledger = {}) {
|
|
|
83
96
|
const auth = ledger.gitAuthority;
|
|
84
97
|
if (goal.domain === 'git-integrity' && validRemoteSync(op.remoteSync) && auth && auth.clean === true
|
|
85
98
|
&& requiredTargets.every((t) => t === 'commit' || t === 'push')
|
|
86
|
-
&& attempts.every((r) => observation.sequence > r.sequence)
|
|
99
|
+
&& attempts.every((r) => (Number.isSafeInteger(op.remoteSync.observedSequence) ? op.remoteSync.observedSequence : observation.sequence) > r.sequence)
|
|
87
100
|
&& op.remoteSync.sha === auth.head && op.remoteSync.ref === `refs/heads/${auth.branch}`
|
|
88
101
|
&& op.remoteSync.remote === auth.remote) return { complete: true, operationProof: 'remote-parity' };
|
|
102
|
+
// A remote observation that disagrees with live local truth is divergence, not a verified state: it can never
|
|
103
|
+
// be discharged by the fallback below just because a push command exited 0.
|
|
104
|
+
if (goal.domain === 'git-integrity' && validRemoteSync(op.remoteSync) && auth
|
|
105
|
+
&& (op.remoteSync.sha !== auth.head || op.remoteSync.ref !== `refs/heads/${auth.branch}`
|
|
106
|
+
|| (auth.remote && op.remoteSync.remote !== auth.remote))) return incomplete;
|
|
89
107
|
if (goal.completionSemantics === 'ensure-state' && attempts.length === 0) {
|
|
90
108
|
return { complete: true, operationProof: 'desired-state-only' };
|
|
91
109
|
}
|