@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 +14 -0
- package/package.json +1 -1
- package/template_project/.claude/hooks/skill-router.sh +3 -0
- package/template_project/.claude/ukit/index/route-task.mjs +3 -0
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +16 -3
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +17 -2
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +12 -1
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
|
@@ -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
|
-
|
|
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
|
}
|