@ngockhoale/ukit 3.4.59 → 3.4.60
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.60 - 2026-10-22
|
|
6
|
+
|
|
7
|
+
**Completion Gate: a background completion footer is no longer trusted on its own.**
|
|
8
|
+
- Gap: 3.4.59 accepted `[exited with code N]` at the end of `.../tasks/<id>.output` as the runner's exit status. That file holds the runner's own stdout, so a process (or a test fixture) that prints the footer while still running, or before a non-zero exit, could produce a false PASS.
|
|
9
|
+
- Fix: a completion is accepted only when the session transcript holds the HOST's `<task-notification>` for the same task id (queue event or plain user turn; tool-result text, including a Read of the output file, never counts) and its exit code equals the footer. No notification -> `no-host-notification`; disagreement -> `host-footer-mismatch`. A host-reported failure is recorded as failed.
|
|
10
|
+
- Regression: `tests/hooks/backgroundCompletionForgery.test.js` (footer printed while running, imitation notification inside a tool result, matching notification, mismatching exit, host failure, other task id); lifecycle tests now mirror the host notification.
|
|
11
|
+
- Limit: needs `transcript_path` in the hook payload (Claude Code provides it); without it a background completion stays unproven. omp/Codex shapes not covered; replay, not a live host.
|
|
12
|
+
|
|
5
13
|
## 3.4.59 - 2026-10-22
|
|
6
14
|
|
|
7
15
|
**Completion Gate: a host-backgrounded verification can now complete through a supported, provenance-bound result capture.**
|
package/package.json
CHANGED
|
@@ -803,6 +803,29 @@ function flattenText(value, depth = 0) {
|
|
|
803
803
|
return ['content', 'text', 'stdout', 'output', 'file'].map((k) => flattenText(value[k], depth + 1)).filter(Boolean).join('\n');
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
+
// Exit status the HOST reported for a background task: a `<task-notification>` delivered as a queue event or a
|
|
807
|
+
// plain user turn. Tool results (runner-controlled text, including a Read of the output file) never count.
|
|
808
|
+
function hostCompletionExit(transcriptPath, id) {
|
|
809
|
+
if (!transcriptPath || !id) return null;
|
|
810
|
+
let raw;
|
|
811
|
+
try { raw = fsSync.readFileSync(transcriptPath, 'utf8'); } catch { return null; }
|
|
812
|
+
const marker = `<task-id>${id}</task-id>`;
|
|
813
|
+
let found = null;
|
|
814
|
+
for (const line of raw.split('\n')) {
|
|
815
|
+
if (!line.includes(marker)) continue;
|
|
816
|
+
let o;
|
|
817
|
+
try { o = JSON.parse(line); } catch { continue; }
|
|
818
|
+
let text = null;
|
|
819
|
+
if (o?.type === 'queue-operation' && o.operation === 'enqueue' && typeof o.content === 'string') text = o.content;
|
|
820
|
+
else if (o?.type === 'user' && typeof o.message?.content === 'string') text = o.message.content;
|
|
821
|
+
if (!text || !text.trimStart().startsWith('<task-notification>')) continue;
|
|
822
|
+
if (!text.includes(marker)) continue;
|
|
823
|
+
const m = /\(exit code (-?\d+)\)/.exec(text);
|
|
824
|
+
if (m) found = Number(m[1]);
|
|
825
|
+
}
|
|
826
|
+
return found;
|
|
827
|
+
}
|
|
828
|
+
|
|
806
829
|
// A host-backgrounded run completes OUTSIDE any tool event the hooks see (a task-notification system
|
|
807
830
|
// event). The supported capture is the host-written task output file: when the agent Reads
|
|
808
831
|
// `.../tasks/<id>.output` and its LAST line is the host footer `[exited with code N]`, that status is the
|
|
@@ -816,6 +839,11 @@ async function backgroundCompletionFor({ projectRoot, payload, toolInput, toolRe
|
|
|
816
839
|
const tail = flattenText(toolResponse).replace(/\s+$/, '');
|
|
817
840
|
const footer = /\[exited with code (-?\d+)\]$/.exec(tail);
|
|
818
841
|
if (!footer) return { rejected: 'no-exit-footer' };
|
|
842
|
+
// The footer lives in a file the runner can write to, so it is only a cross-check. The authority is the
|
|
843
|
+
// host's own task-notification in the session transcript (a runner cannot write there).
|
|
844
|
+
const hostExit = hostCompletionExit(payload?.transcript_path || payload?.transcriptPath, id);
|
|
845
|
+
if (hostExit === null) return { rejected: 'no-host-notification' };
|
|
846
|
+
if (hostExit !== Number(footer[1])) return { rejected: 'host-footer-mismatch' };
|
|
819
847
|
const ledger = await readExecutionLedger(projectRoot, payload);
|
|
820
848
|
const receipts = ledger?.receipts || [];
|
|
821
849
|
const launch = receipts.find((r) => r.kind === 'verification' && r.backgroundTaskId === id && !r.backgroundCompletion);
|