@ngockhoale/ukit 3.4.39 → 3.4.41
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.41 - 2026-10-13
|
|
13
|
+
|
|
14
|
+
**Completion Gate: a published release is recognized, and tests no longer depend on the developer's real `~/.ukit`.**
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- `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.
|
|
19
|
+
- Regressions: `tests/hooks/releaseCompletionEvidence.test.js` (R01–R08, R11, R13), extended `completionGateRequestScope.test.js` (R09, R10).
|
|
20
|
+
|
|
21
|
+
## 3.4.40 - 2026-10-12
|
|
22
|
+
|
|
23
|
+
**Completion Gate: a read-only request no longer owes `impact-evidence` it can never produce.**
|
|
24
|
+
- Root cause: a `READ_ONLY` contract (analysis of Markdown/logs/prompt text, read-only production inspection) carried `impact-evidence`, satisfied only by a source-read receipt. Answers drawn from docs, logs or text already in the prompt yield none, so Stop demanded further tool activity and looped into the loop breaker.
|
|
25
|
+
- Fix: for `READ_ONLY` with no named target files and no attempted write, `impact-evidence` is not applicable. A named target still needs its read; any attempted write (unauthorized side effect) keeps the obligation; mutation orders keep write/verification/operation proof.
|
|
26
|
+
- Regressions: `tests/hooks/completionGateRequestScope.test.js` (cross-request scope after an edit request, read-only inspection, missing contract, unauthorized write). Test allowlist gains the runtime-only `docs/AI_HANDOFF/RUN.md` cursor reference.
|
|
27
|
+
|
|
12
28
|
## 3.4.39 - 2026-10-12
|
|
13
29
|
|
|
14
30
|
**Completion Gate: unrelated compound commands no longer erase authoritative git evidence; seals honour operation freshness.**
|
package/package.json
CHANGED
|
@@ -3413,6 +3413,16 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3413
3413
|
const operationalTask = operationStatus !== null;
|
|
3414
3414
|
const missingEvidence = evidence.filter((item) => !evidenceSatisfied(item, effectiveLedger, state, { cwd }));
|
|
3415
3415
|
if (operationalTask && !operationStatus.complete && !missingEvidence.includes('operation-verification')) missingEvidence.push('operation-verification');
|
|
3416
|
+
// A READ_ONLY request's deliverable is the reply: it may be answered from Markdown, logs or
|
|
3417
|
+
// text already in the prompt, none of which yields a source-read receipt, so impact-evidence
|
|
3418
|
+
// is NOT_APPLICABLE to it when no target files were named (request-scoped — never inherited from an earlier edit request in
|
|
3419
|
+
// the same session). Any attempted write keeps the obligation: an unauthorized side effect
|
|
3420
|
+
// of a read-only request is never treated as irrelevant.
|
|
3421
|
+
if (routeSummary.executionContract?.taskKind === 'READ_ONLY' && effectiveLedger.writeAttempted !== true
|
|
3422
|
+
&& !(Array.isArray(routeSummary.expectedSourceFiles) && routeSummary.expectedSourceFiles.length > 0)) {
|
|
3423
|
+
const at = missingEvidence.indexOf('impact-evidence');
|
|
3424
|
+
if (at !== -1) missingEvidence.splice(at, 1);
|
|
3425
|
+
}
|
|
3416
3426
|
const releaseMissing = releasePublicationMissing(state, effectiveLedger, cwd);
|
|
3417
3427
|
if (releaseMissing) missingEvidence.push(releaseMissing);
|
|
3418
3428
|
// TASK-009 (BL-011): the bug-fix floor. On a `playbookId: bug-fix` route a
|
|
@@ -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
|
}
|