@ngockhoale/ukit 3.4.58 → 3.4.59
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
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.59 - 2026-10-22
|
|
6
|
+
|
|
7
|
+
**Completion Gate: a host-backgrounded verification can now complete through a supported, provenance-bound result capture.**
|
|
8
|
+
- Gap: 3.4.57 made a background launch acknowledgement unproven, but nothing could later turn it into evidence, so the only route was re-running a long suite in the foreground. Claude Code delivers the completion as a task-notification the hooks never see; the host does write the runner's output to `.../tasks/<id>.output` ending in `[exited with code N]`.
|
|
9
|
+
- Fix: when the agent reads that file back, the ledger records a verification receipt bound to the launch receipt by `backgroundTaskId` (same task ledger, so another task/session finds no launch), exit status from the host footer, judged against the LAUNCH tree fingerprint. Rejected: no footer (still running / "passed" text only), no matching launch, duplicate completion, output file older than the launch. Exit 1 yields a failed verdict.
|
|
10
|
+
- Regression: `tests/hooks/backgroundCompletionLifecycle.test.js` (success, failure, partial, duplicate, other task, other session, late, stale file, unrelated file, Stop-gate outcomes). Also `tests/orchestration/schedulingHarness.test.js`: a deterministic offline scheduling-logic model (not a throughput benchmark).
|
|
11
|
+
- Limit: replay of the host file format observed on Claude Code; omp/Codex background shapes are not covered, and no hook receives the notification itself.
|
|
12
|
+
|
|
5
13
|
## 3.4.58 - 2026-10-22
|
|
6
14
|
|
|
7
15
|
**Completion Gate: plain publish orders create the registry obligation, new tasks do not inherit the previous task's evidence, and a Vietnamese status line about a config is not a config order.**
|
package/package.json
CHANGED
|
@@ -786,6 +786,56 @@ function isBackgroundLaunchAck(response) {
|
|
|
786
786
|
&& /^\s*Command running in background with ID: \S+/.test(response);
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
+
// Id of the host background task a launch acknowledgement names (structured field first).
|
|
790
|
+
function backgroundTaskIdOf(response) {
|
|
791
|
+
if (response && typeof response === 'object' && !Array.isArray(response)) {
|
|
792
|
+
if (typeof response.backgroundTaskId === 'string' && response.backgroundTaskId) return response.backgroundTaskId;
|
|
793
|
+
return backgroundTaskIdOf(response.stdout ?? response.output ?? response.content);
|
|
794
|
+
}
|
|
795
|
+
const m = typeof response === 'string' ? /^\s*Command running in background with ID: ([A-Za-z0-9_-]+)/.exec(response) : null;
|
|
796
|
+
return m ? m[1] : null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function flattenText(value, depth = 0) {
|
|
800
|
+
if (typeof value === 'string') return value;
|
|
801
|
+
if (!value || typeof value !== 'object' || depth > 4) return '';
|
|
802
|
+
if (Array.isArray(value)) return value.map((v) => flattenText(v, depth + 1)).join('\n');
|
|
803
|
+
return ['content', 'text', 'stdout', 'output', 'file'].map((k) => flattenText(value[k], depth + 1)).filter(Boolean).join('\n');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// A host-backgrounded run completes OUTSIDE any tool event the hooks see (a task-notification system
|
|
807
|
+
// event). The supported capture is the host-written task output file: when the agent Reads
|
|
808
|
+
// `.../tasks/<id>.output` and its LAST line is the host footer `[exited with code N]`, that status is the
|
|
809
|
+
// runner's exit status. It is bound to the launch receipt by `backgroundTaskId` (same task ledger, so a
|
|
810
|
+
// completion for another task/session finds no launch), counted once, and judged against the tree that
|
|
811
|
+
// was tested at LAUNCH. A bare "passed" line, a partial read, or a path whose id has no launch is no proof.
|
|
812
|
+
async function backgroundCompletionFor({ projectRoot, payload, toolInput, toolResponse }) {
|
|
813
|
+
const file = String(toolInput?.file_path || toolInput?.path || '');
|
|
814
|
+
const id = /[\\/]tasks[\\/]([A-Za-z0-9_-]+)\.output$/.exec(file)?.[1];
|
|
815
|
+
if (!id) return null;
|
|
816
|
+
const tail = flattenText(toolResponse).replace(/\s+$/, '');
|
|
817
|
+
const footer = /\[exited with code (-?\d+)\]$/.exec(tail);
|
|
818
|
+
if (!footer) return { rejected: 'no-exit-footer' };
|
|
819
|
+
const ledger = await readExecutionLedger(projectRoot, payload);
|
|
820
|
+
const receipts = ledger?.receipts || [];
|
|
821
|
+
const launch = receipts.find((r) => r.kind === 'verification' && r.backgroundTaskId === id && !r.backgroundCompletion);
|
|
822
|
+
if (!launch) return { rejected: 'no-matching-launch' };
|
|
823
|
+
if (receipts.some((r) => r.backgroundCompletion === id)) return { rejected: 'duplicate-completion' };
|
|
824
|
+
let mtime = null;
|
|
825
|
+
try { mtime = fsSync.statSync(file).mtimeMs; } catch { return { rejected: 'output-file-missing' }; }
|
|
826
|
+
if (mtime + 2000 < launch.ts) return { rejected: 'output-predates-launch' };
|
|
827
|
+
const exitCode = Number(footer[1]);
|
|
828
|
+
const success = exitCode === 0;
|
|
829
|
+
const lv = launch.verdict || {};
|
|
830
|
+
const kind = success ? (isTestGateCommand(launch.command) ? 'test-verified' : 'check-only') : 'failed';
|
|
831
|
+
return {
|
|
832
|
+
kind: 'verification', command: launch.command, success, exitCode, backgroundCompletion: id,
|
|
833
|
+
...(launch.scope ? { scope: launch.scope } : {}),
|
|
834
|
+
...(launch.workspaceFp ? { workspaceFp: launch.workspaceFp } : {}),
|
|
835
|
+
verdict: { ...lv, kind, evidence: launch.command, verifier: lv.verifier || 'self', ts: new Date().toISOString() },
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
789
839
|
function noteUnprovenExitStatus(stage) {
|
|
790
840
|
if (!ledgerMessageEmitter) return;
|
|
791
841
|
const message =
|
|
@@ -803,7 +853,7 @@ function compactReceipt(receipt) {
|
|
|
803
853
|
kind: receipt.kind,
|
|
804
854
|
success: receipt.success,
|
|
805
855
|
};
|
|
806
|
-
for (const key of ['toolName', 'toolUseId', 'file', 'command', 'exitCode', 'scope', 'error', 'verdict', 'evidence', 'evidenceSurface', 'class', 'status', 'detail', 'operation', 'transportSuccess', 'taskInstanceId', 'sessionKey', 'sequence', 'exitStatusUnproven', 'agentId', 'agentType', 'contentSha']) {
|
|
856
|
+
for (const key of ['toolName', 'toolUseId', 'file', 'command', 'exitCode', 'scope', 'error', 'verdict', 'evidence', 'evidenceSurface', 'class', 'status', 'detail', 'operation', 'transportSuccess', 'taskInstanceId', 'sessionKey', 'sequence', 'exitStatusUnproven', 'backgroundTaskId', 'backgroundCompletion', 'agentId', 'agentType', 'contentSha']) {
|
|
807
857
|
if (receipt[key] !== undefined && receipt[key] !== null && receipt[key] !== '') {
|
|
808
858
|
compact[key] = key === 'operation' ? compactOperation(receipt[key]) : receipt[key];
|
|
809
859
|
}
|
|
@@ -1728,7 +1778,7 @@ function newEventId() {
|
|
|
1728
1778
|
return `${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
1729
1779
|
}
|
|
1730
1780
|
|
|
1731
|
-
const JOURNAL_RECEIPT_FIELDS = ['ts', 'kind', 'toolName', 'toolUseId', 'success', 'exitCode', 'file', 'command', 'scope', 'workspaceFp', 'workspaceRoot', 'workspaceHeadTs', 'bashReadOnly', 'exitStatusUnproven', 'detail', 'operation', 'transportSuccess', 'taskInstanceId', 'sessionKey', 'sequence', 'agentId', 'agentType', 'contentSha', 'handback', 'review', 'verdict'];
|
|
1781
|
+
const JOURNAL_RECEIPT_FIELDS = ['ts', 'kind', 'toolName', 'toolUseId', 'success', 'exitCode', 'file', 'command', 'scope', 'workspaceFp', 'workspaceRoot', 'workspaceHeadTs', 'bashReadOnly', 'exitStatusUnproven', 'backgroundTaskId', 'backgroundCompletion', 'detail', 'operation', 'transportSuccess', 'taskInstanceId', 'sessionKey', 'sequence', 'agentId', 'agentType', 'contentSha', 'handback', 'review', 'verdict'];
|
|
1732
1782
|
|
|
1733
1783
|
// TASK-009 (BL-011): evidence attestations are whitelisted verbatim — class /
|
|
1734
1784
|
// surface / result / claim, the ARCH §Evidence Schema fields a receipt may
|
|
@@ -3150,13 +3200,17 @@ export async function recordExecutionReceipt({
|
|
|
3150
3200
|
// command has not finished, so that response carries no exit status. It must never mint
|
|
3151
3201
|
// verification or operation evidence (completion arrives later, outside this tool event).
|
|
3152
3202
|
const backgroundLaunchAck = toolName === 'Bash' && isBackgroundLaunchAck(toolResponse);
|
|
3203
|
+
const bgCompletion = toolName === 'Read'
|
|
3204
|
+
? await backgroundCompletionFor({ projectRoot, payload, toolInput, toolResponse }) : null;
|
|
3153
3205
|
const nativeGoal = routeState?.routeSummary?.executionContract?.operationGoal;
|
|
3154
3206
|
const operationReceipt = backgroundLaunchAck ? null : cronOperationReceipt(toolName, toolInput, toolResponse, success, { sessionId: explicitSessionId(payload), harness })
|
|
3155
3207
|
|| (toolName === 'Bash' && !bashScope.foreign
|
|
3156
3208
|
? (bashOperationReceipt(toolInput.command, toolResponse, success, nativeGoal, { sessionId: explicitSessionId(payload), harness })
|
|
3157
3209
|
|| releaseOperationReceipt(toolInput.command, toolResponse, success, { sessionId: explicitSessionId(payload), harness }))
|
|
3158
3210
|
: null);
|
|
3159
|
-
if (
|
|
3211
|
+
if (bgCompletion && !bgCompletion.rejected) {
|
|
3212
|
+
Object.assign(receipt, bgCompletion);
|
|
3213
|
+
} else if (operationReceipt) {
|
|
3160
3214
|
Object.assign(receipt, operationReceipt);
|
|
3161
3215
|
} else if (toolName === 'Read' || toolName === 'Grep' || toolName === 'Glob') {
|
|
3162
3216
|
receipt.kind = 'source';
|
|
@@ -3182,6 +3236,7 @@ export async function recordExecutionReceipt({
|
|
|
3182
3236
|
receipt.command = String(toolInput.command || '').trim();
|
|
3183
3237
|
// A pipe-masked runner status (exit 0 from `| tail`) is not evidence of a pass.
|
|
3184
3238
|
const maskedStage = isVerificationCommand(toolInput.command) ? maskedVerificationStage(toolInput.command) : null;
|
|
3239
|
+
if (backgroundLaunchAck) receipt.backgroundTaskId = backgroundTaskIdOf(toolResponse);
|
|
3185
3240
|
if (backgroundLaunchAck && receipt.success) {
|
|
3186
3241
|
receipt.success = false;
|
|
3187
3242
|
receipt.exitStatusUnproven = true;
|
|
@@ -3243,7 +3298,8 @@ export async function recordExecutionReceipt({
|
|
|
3243
3298
|
}
|
|
3244
3299
|
}
|
|
3245
3300
|
if (workspaceFp) {
|
|
3246
|
-
|
|
3301
|
+
// A background completion is judged against the tree that was tested at LAUNCH, never today's tree.
|
|
3302
|
+
receipt.workspaceFp = bgCompletion && !bgCompletion.rejected && bgCompletion.workspaceFp ? bgCompletion.workspaceFp : workspaceFp;
|
|
3247
3303
|
if (workspaceRoot !== projectRoot) {
|
|
3248
3304
|
receipt.workspaceRoot = workspaceRoot;
|
|
3249
3305
|
const ct = Number(gitOutput(['log', '-1', '--format=%ct'], workspaceRoot));
|