@ngockhoale/ukit 3.4.38 → 3.4.40
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,21 @@ 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.40 - 2026-10-12
|
|
13
|
+
|
|
14
|
+
**Completion Gate: a read-only request no longer owes `impact-evidence` it can never produce.**
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
## 3.4.39 - 2026-10-12
|
|
20
|
+
|
|
21
|
+
**Completion Gate: unrelated compound commands no longer erase authoritative git evidence; seals honour operation freshness.**
|
|
22
|
+
- Root cause: a compound command such as `git rev-parse HEAD && git status --porcelain && npm view …` was parsed as a whole-tree `git status`, its mixed stdout read as a dirty tree, and that failed state record replaced the earlier valid `git ls-remote` proof (`missing operation-verification`).
|
|
23
|
+
- Fix: a git state observation is only minted from ONE unmasked, transport-successful command; compound/masked/failed ones mint nothing and cannot overwrite proof. A standalone dirty status or a diverged remote still invalidates earlier success.
|
|
24
|
+
- Seals now record the operation sequence: a later git mutation, failed state observation, or remote sha != HEAD voids the seal (previously only content fingerprint did).
|
|
25
|
+
- Regressions: `completionClosure.test.js` (full 7-step sequence through the real hook chain), `gitSyncPostcondition.test.js`. No real Windows run.
|
|
26
|
+
|
|
12
27
|
## 3.4.38 - 2026-10-12
|
|
13
28
|
|
|
14
29
|
**Completion Gate safety closure: mutation orders always get a contract; git sync needs authoritative remote proof.**
|
package/package.json
CHANGED
|
@@ -115,6 +115,11 @@ export function bashOperationReceipt(command, response, transportSuccess, goal,
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
if (!role) return null;
|
|
118
|
+
// A git STATE observation is attributable only when it is one unmasked, transport-successful command: in a
|
|
119
|
+
// compound/masked chain the stdout of its parts is mixed, so it can neither prove nor refute the state and
|
|
120
|
+
// must not overwrite earlier authoritative evidence. (A standalone dirty status still records as failed.)
|
|
121
|
+
if (domain === 'git-integrity' && role === 'state'
|
|
122
|
+
&& (masked || transportSuccess !== true || real.filter((seg) => seg && !/^cd\s+\S+$/.test(seg)).length !== 1)) return null;
|
|
118
123
|
const ok = transportSuccess === true && !masked && Boolean(scope) && (role === 'mutation' || proven);
|
|
119
124
|
return { kind: 'operation', success: ok, transportSuccess: transportSuccess === true, harness,
|
|
120
125
|
...(masked ? { exitStatusUnproven: true } : {}),
|
|
@@ -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
|
|
@@ -3793,14 +3803,39 @@ async function readSeal(projectRoot, payload, taskInstanceId) {
|
|
|
3793
3803
|
return seal && typeof seal === 'object' ? seal : null;
|
|
3794
3804
|
}
|
|
3795
3805
|
|
|
3796
|
-
|
|
3806
|
+
// Operation freshness of a seal: the newest operation-proof sequence at seal time. After it, any NEW mutation
|
|
3807
|
+
// attempt, any failed state observation, or a remote observation whose sha is no longer HEAD voids the seal
|
|
3808
|
+
// (successful unrelated observations do not). Content fingerprint alone cannot see remote/mutation changes.
|
|
3809
|
+
function operationSeq(ledger) {
|
|
3810
|
+
let max = 0;
|
|
3811
|
+
for (const [key, entry] of Object.entries(ledger?.operationBank || {})) {
|
|
3812
|
+
if (key === 'seenToolUseIds' || key === 'overflow') continue;
|
|
3813
|
+
for (const proof of [entry?.mutation, entry?.state]) if (Number.isSafeInteger(proof?.sequence)) max = Math.max(max, proof.sequence);
|
|
3814
|
+
}
|
|
3815
|
+
return max;
|
|
3816
|
+
}
|
|
3817
|
+
function sealOperationsFresh(seal, ledger, projectRoot) {
|
|
3818
|
+
const since = Number.isSafeInteger(seal?.opSeq) ? seal.opSeq : 0;
|
|
3819
|
+
const head = gitOutput(['rev-parse', 'HEAD'], projectRoot);
|
|
3820
|
+
for (const [key, entry] of Object.entries(ledger?.operationBank || {})) {
|
|
3821
|
+
if (key === 'seenToolUseIds' || key === 'overflow') continue;
|
|
3822
|
+
if (entry?.mutation?.sequence > since) return false;
|
|
3823
|
+
const st = entry?.state;
|
|
3824
|
+
if (!st) continue;
|
|
3825
|
+
if (st.sequence > since && st.success !== true) return false;
|
|
3826
|
+
if (st.operation?.remoteSync && st.operation.remoteSync.sha !== head) return false;
|
|
3827
|
+
}
|
|
3828
|
+
return !ledger?.operationBank?.overflow;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
async function writeSeal(projectRoot, payload, state, result, ledger) {
|
|
3797
3832
|
const taskInstanceId = state?.taskInstanceId;
|
|
3798
3833
|
const contentFp = contentFingerprint(projectRoot);
|
|
3799
3834
|
if (!taskInstanceId || !contentFp || result.complete !== true) return;
|
|
3800
3835
|
if (!(requiredEvidence(state).length > 0)) return;
|
|
3801
3836
|
const file = sealPath(projectRoot, payload);
|
|
3802
3837
|
const seals = await readJson(file, {}) || {};
|
|
3803
|
-
seals[taskInstanceId] = { contentFp, headSha: gitOutput(['rev-parse', 'HEAD'], projectRoot), ts: Date.now() };
|
|
3838
|
+
seals[taskInstanceId] = { contentFp, headSha: gitOutput(['rev-parse', 'HEAD'], projectRoot), opSeq: operationSeq(ledger), ts: Date.now() };
|
|
3804
3839
|
const keep = Object.entries(seals).sort((a, b) => (b[1]?.ts || 0) - (a[1]?.ts || 0)).slice(0, SEAL_MAX);
|
|
3805
3840
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
3806
3841
|
await writeJsonAtomic(file, Object.fromEntries(keep));
|
|
@@ -3843,12 +3878,12 @@ async function runEvaluateStop() {
|
|
|
3843
3878
|
if (result.continue === true && !result.finalNotice) {
|
|
3844
3879
|
try {
|
|
3845
3880
|
const seal = await readSeal(projectRoot, payload, state?.taskInstanceId);
|
|
3846
|
-
if (seal?.contentFp && contentFingerprint(projectRoot) === seal.contentFp) {
|
|
3881
|
+
if (seal?.contentFp && contentFingerprint(projectRoot) === seal.contentFp && sealOperationsFresh(seal, ledger, projectRoot)) {
|
|
3847
3882
|
result = { continue: false, notify: false, complete: true, missingEvidence: [], gateRelease: 'sealed-complete' };
|
|
3848
3883
|
}
|
|
3849
3884
|
} catch { /* a seal read failure keeps the strict gate */ }
|
|
3850
3885
|
} else if (result.complete === true && (!result.gateRelease || result.gateRelease === 'native-operation-verified')) {
|
|
3851
|
-
try { await writeSeal(projectRoot, payload, state, result); } catch { /* advisory */ }
|
|
3886
|
+
try { await writeSeal(projectRoot, payload, state, result, ledger); } catch { /* advisory */ }
|
|
3852
3887
|
}
|
|
3853
3888
|
|
|
3854
3889
|
// TASK-C85-019 fix C: a mutating-mode route released by a no-mutation valve
|