@ngockhoale/ukit 3.4.60 → 3.4.61
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 +11 -0
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/template_project/.claude/ukit/runtime/async-lock.mjs +17 -6
- package/template_project/.claude/ukit/runtime/omp-bash-call-guard.mjs +53 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +10 -0
- package/template_project/AGENTS.md +1 -1
- package/template_project/CLAUDE.md +1 -1
- package/template_project/instructions/core.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.4.61 - 2026-10-22
|
|
6
|
+
|
|
7
|
+
**omp bash: stop sending the call shape the host rejects (`name` + `async`/`timeout`).**
|
|
8
|
+
- Field evidence (BAGuide session log, 2026-10-19..22): 1,771 of 6,541 bash calls failed with "Service mode does not accept async or timeout; use ready.timeout for readiness." Every failing call carried `name` with `async`/`timeout`; calls without `name` never failed and 66 immediate retries that dropped `name` succeeded. `name` was used as a label on finite commands.
|
|
9
|
+
- Host contract (omp 18.12.5 bash `execute`, read from the installed binary): `name` selects SERVICE mode, which rejects `async === true` and any `timeout`; readiness is `ready.timeout`. Without `name`, `async`/`timeout` are valid and `ready` is only ignored with a warning.
|
|
10
|
+
- Change: `omp-bash-call-guard.mjs` (block-only, stateless, never rewrites input or changes a command's meaning) wired into the omp bridge `tool_call` for `bash`; the block reason separates the two intents (finite command: drop `name`/`ready`; service: drop `async`/`timeout`, use `ready.timeout`). Same rule added to the Stall & Wait instructions (STALL-02) for all three engines.
|
|
11
|
+
- Live smoke (omp 18.12.5, isolated temp project with the template bridge, model `unic/unic-lite`, BAGuide and the omp binary untouched): `name`+`timeout` was blocked with the UKit preflight text (not the host's "Service mode..." text), so the `tool_call` hook runs before the host validator; a finite command ran; the corrected retry (name dropped) ran and was not blocked. NOT verified live: a genuine service call (the test model kept adding a top-level `timeout`; one attempt hung with no output and was stopped; no process was left running). The service-allowed path is covered only by the bridge-boundary tests. Guidance, not host enforcement.
|
|
12
|
+
- Flaky test root cause + fix: `withTransientFsRetry` compared `Date.now()` against a deadline while sleeping with `setTimeout`; node timers can fire up to ~2% early at small delays, so after a clamped sleep the clock still showed time remaining and an extra attempt ran. It now measures with `performance.now()` (injectable `now`/`sleep`) and stops after a sleep that exhausted the deadline. Regression: `tests/hooks/transientFsRetry.test.js` (controlled clock incl. early-timer skew, plus one real-timer smoke); 30/30 repeated runs.
|
|
13
|
+
- Review freshness through a real Bash mutation (git fixture, real PostToolUse receipt): review approved -> `sed -i` via Bash -> `changeSeq` advances, review is `review-stale`, verification is `edited-after-verification`; a no-op Bash keeps the approval. No defect reproduced; regression `tests/hooks/playbookReviewBashMutation.test.js`.
|
|
14
|
+
- Regression: `tests/hooks/ompBashCallGuard.test.js` (normal finite command with timeout/async allowed; genuine service with `ready.timeout` allowed; `name`+`timeout` / `name`+`async` blocked repeatably; blank `name` is not service mode; input never mutated).
|
|
15
|
+
|
|
5
16
|
## 3.4.60 - 2026-10-22
|
|
6
17
|
|
|
7
18
|
**Completion Gate: a background completion footer is no longer trusted on its own.**
|
|
@@ -451,6 +451,17 @@ entries:
|
|
|
451
451
|
validation: [manual]
|
|
452
452
|
archive_policy: replace-with-snapshot
|
|
453
453
|
notes: Phase A read-only concurrency audit; provisional metrics labelled, pilot not run
|
|
454
|
+
- id: docs-analysis-concurrency-checkpoint
|
|
455
|
+
path: docs/analysis/CONCURRENCY-CHECKPOINT-2026-10-22.md
|
|
456
|
+
class: canonical
|
|
457
|
+
audience: [owner, maintainer]
|
|
458
|
+
owner: runtime
|
|
459
|
+
source_of_truth: docs/analysis/CONCURRENCY-CHECKPOINT-2026-10-22.md
|
|
460
|
+
merge_strategy: none
|
|
461
|
+
load_policy: on-demand
|
|
462
|
+
validation: [manual]
|
|
463
|
+
archive_policy: replace-with-snapshot
|
|
464
|
+
notes: continuity checkpoint for the concurrency audit (R6 corrections, bash guard, prepared unpaid pilot)
|
|
454
465
|
- id: docs-analysis-subagent-cost-audit
|
|
455
466
|
path: docs/analysis/SUBAGENT-COST-AUDIT.md
|
|
456
467
|
class: archive
|
package/package.json
CHANGED
|
@@ -113,7 +113,10 @@ export const TRANSIENT_FS_CODES = new Set(['EAGAIN', 'EBUSY', 'EMFILE', 'ENFILE'
|
|
|
113
113
|
* maxDelayMs?: number,
|
|
114
114
|
* deadlineMs?: number,
|
|
115
115
|
* signal?: AbortSignal,
|
|
116
|
-
*
|
|
116
|
+
* now?: () => number,
|
|
117
|
+
* sleep?: (ms: number, signal?: AbortSignal) => Promise<boolean>,
|
|
118
|
+
* }} [options] `now` (monotonic ms) and `sleep` (resolves true when it elapsed fully)
|
|
119
|
+
* are injectable so deadline semantics can be tested without wall-clock timing.
|
|
117
120
|
* @returns {Promise<*>} op's resolved value
|
|
118
121
|
*/
|
|
119
122
|
export async function withTransientFsRetry(op, {
|
|
@@ -122,8 +125,10 @@ export async function withTransientFsRetry(op, {
|
|
|
122
125
|
maxDelayMs = 150,
|
|
123
126
|
deadlineMs = Infinity,
|
|
124
127
|
signal,
|
|
128
|
+
now = () => performance.now(),
|
|
129
|
+
sleep = sleepWithAbort,
|
|
125
130
|
} = {}) {
|
|
126
|
-
const startedAt =
|
|
131
|
+
const startedAt = now();
|
|
127
132
|
const maxAttempts = Number.isFinite(retries) && retries >= 0 ? Math.floor(retries) + 1 : 1;
|
|
128
133
|
let lastError = null;
|
|
129
134
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
@@ -135,12 +140,18 @@ export async function withTransientFsRetry(op, {
|
|
|
135
140
|
} catch (error) {
|
|
136
141
|
lastError = error;
|
|
137
142
|
if (!TRANSIENT_FS_CODES.has(error?.code) || attempt + 1 >= maxAttempts) throw error;
|
|
138
|
-
const remaining = deadlineMs - (
|
|
143
|
+
const remaining = deadlineMs - (now() - startedAt);
|
|
139
144
|
if (remaining <= 0) throw error;
|
|
140
145
|
const backoff = Math.min(baseDelayMs * (2 ** attempt), maxDelayMs);
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
146
|
+
const wanted = backoff * (0.5 + Math.random());
|
|
147
|
+
const waitMs = Math.min(wanted, remaining);
|
|
148
|
+
// A sleep clamped to `remaining` consumes the whole deadline BY CONSTRUCTION. Re-reading a
|
|
149
|
+
// clock afterwards is wrong: timers (libuv, ms-truncated) and the clock can disagree by a
|
|
150
|
+
// millisecond, so a fired 5 ms timer can read as 4 ms elapsed and grant one extra attempt
|
|
151
|
+
// past the deadline (observed: setTimeout(5) woke with Date.now() +4 ms in ~0.4% of runs).
|
|
152
|
+
const exhaustsDeadline = wanted >= remaining;
|
|
153
|
+
const sleptFully = await sleep(Math.max(0, waitMs), signal);
|
|
154
|
+
if (!sleptFully || exhaustsDeadline || now() - startedAt >= deadlineMs) throw error;
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
throw lastError;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// omp-bash-call-guard.mjs — block-only preflight for OMP `bash` call shape.
|
|
2
|
+
//
|
|
3
|
+
// Field evidence (BAGuide, 2026-10-19..22): 1,771 of 6,541 bash calls failed with
|
|
4
|
+
// "Service mode does not accept async or timeout; use ready.timeout for readiness."
|
|
5
|
+
// Every failing call carried `name` (the supervised-SERVICE mode) together with `async`
|
|
6
|
+
// or `timeout`; calls without `name` never failed, and 66 immediate retries that dropped
|
|
7
|
+
// `name` succeeded. The agent used `name` as a label on an ordinary finite command.
|
|
8
|
+
//
|
|
9
|
+
// Host contract (omp 18.12.5, bash tool `execute`, read from the binary):
|
|
10
|
+
// - `name` non-empty => service mode. `async === true` or any `timeout` is rejected;
|
|
11
|
+
// readiness is `ready.timeout` (seconds) with `ready.log` / `ready.port`.
|
|
12
|
+
// - no `name` => ordinary command; `async` / `timeout` are valid; `ready` is
|
|
13
|
+
// ignored with a warning (not an error), so it is not validated here.
|
|
14
|
+
//
|
|
15
|
+
// This guard blocks only the combination the host is known to reject, with guidance that
|
|
16
|
+
// separates the two intents. It never rewrites input and never changes the meaning of a
|
|
17
|
+
// command — choosing finite vs service is the caller's job. The host owns the schema; this
|
|
18
|
+
// is a tolerant, version-pinned mirror of one rule. It can only act where the host runs the
|
|
19
|
+
// tool_call hook before its own validator; where the host validates first, the native
|
|
20
|
+
// error is the only signal and this guard cannot intervene (guidance in RULES.md still applies).
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} args - bash tool input
|
|
24
|
+
* @returns {{ valid: boolean, violations: Array<{code: string, field: string}> }}
|
|
25
|
+
*/
|
|
26
|
+
export function validateOmpBashArgs(args) {
|
|
27
|
+
const violations = [];
|
|
28
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) return { valid: true, violations };
|
|
29
|
+
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
30
|
+
if (name === '') return { valid: true, violations };
|
|
31
|
+
if (args.async === true) violations.push({ code: 'service-with-async', field: 'async' });
|
|
32
|
+
if (args.timeout !== undefined && args.timeout !== null) {
|
|
33
|
+
violations.push({ code: 'service-with-timeout', field: 'timeout' });
|
|
34
|
+
}
|
|
35
|
+
return { valid: violations.length === 0, violations };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} args - bash tool input
|
|
40
|
+
* @returns {{ action: 'allow' } | { action: 'block', violations: object[], guidance: string }}
|
|
41
|
+
*/
|
|
42
|
+
export function preflightOmpBashCall(args) {
|
|
43
|
+
const { valid, violations } = validateOmpBashArgs(args);
|
|
44
|
+
if (valid) return { action: 'allow' };
|
|
45
|
+
const fields = violations.map((v) => `\`${v.field}\``).join(' + ');
|
|
46
|
+
const guidance = [
|
|
47
|
+
`omp bash: \`name\` selects SERVICE mode, which rejects ${fields}. Do not resend this invocation unchanged.`,
|
|
48
|
+
'Pick one intent:',
|
|
49
|
+
'- Ordinary finite command (tests, build, one-shot script): remove `name` and `ready`; keep `timeout` / `async` as needed. `name` is not a label.',
|
|
50
|
+
'- Genuine long-running service/watcher: keep `name`, remove `async` and `timeout`, and set readiness with `ready: { log|port, timeout }` (seconds). Inspect it via `read proc://<name>`.',
|
|
51
|
+
].join('\n');
|
|
52
|
+
return { action: 'block', violations, guidance };
|
|
53
|
+
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
// for Edit|Write is a fail-closed transport failure = every edit blocked).
|
|
31
31
|
import { resolveChainExecTimeoutMs } from '../../../.claude/ukit/runtime/hook-chain-budget.mjs';
|
|
32
32
|
import { preflightOmpTaskDispatch } from '../../../.claude/ukit/runtime/omp-task-dispatch-guard.mjs';
|
|
33
|
+
import { preflightOmpBashCall } from '../../../.claude/ukit/runtime/omp-bash-call-guard.mjs';
|
|
33
34
|
|
|
34
35
|
// OMP-3 (TASK-002): pi.exec forwards only {cwd, signal, timeout} to its spawn —
|
|
35
36
|
// an environment option is dropped (verified against the bundled omp exec), and the host's
|
|
@@ -929,6 +930,15 @@ export async function runToolCall(pi, event, { projectRoot, context: extensionCo
|
|
|
929
930
|
reason: `UKit OMP task preflight: ${dispatch.guidance}` };
|
|
930
931
|
}
|
|
931
932
|
}
|
|
933
|
+
// omp bash: `name` = service mode, which the host rejects together with async/timeout.
|
|
934
|
+
// Block-only (the host return cannot rewrite input); stateless, so a corrected call is allowed.
|
|
935
|
+
if (String(rawToolName ?? '').trim().toLowerCase() === 'bash') {
|
|
936
|
+
const bashCall = preflightOmpBashCall(event.input);
|
|
937
|
+
if (bashCall.action === 'block') {
|
|
938
|
+
return { block: true, toolName: 'Bash', invoked: [], context: [],
|
|
939
|
+
reason: `UKit OMP bash preflight: ${bashCall.guidance}` };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
932
942
|
const payload = buildHookPayload('PreToolUse', {
|
|
933
943
|
toolName,
|
|
934
944
|
toolInput: normalizeToolInput(toolName, event.input),
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<!-- RULE: STALL-01 -->
|
|
30
30
|
- Announce long waits: `Waiting for API response / tool result — will keep retrying; check your network/API provider if this persists`. Surface errors verbatim; suspect network/API — **never blame UKit**.
|
|
31
31
|
<!-- RULE: STALL-02 -->
|
|
32
|
-
- Stall rules extend EXEC-04 (every stop says why); they do not replace it. Retry guidance applies only to transient network/API failures, never deterministic invalid arguments or unsupported capabilities (e.g. fork + remote isolation): do not repeat the same call; choose a supported approach within user constraints or report the limitation.
|
|
32
|
+
- Stall rules extend EXEC-04 (every stop says why); they do not replace it. Retry guidance applies only to transient network/API failures, never deterministic invalid arguments or unsupported capabilities (e.g. fork + remote isolation): do not repeat the same call; choose a supported approach within user constraints or report the limitation. omp `bash`: `name` = supervised SERVICE (rejects `async`/`timeout`; readiness = `ready.timeout`) — finite commands omit `name`/`ready`; after a "Service mode does not accept async or timeout" error drop `name` (finite) or drop `async`/`timeout` (service). UKit's omp bridge blocks that combination where the host runs hooks first; otherwise this is guidance only.
|
|
33
33
|
<!-- RULE: STALL-03 -->
|
|
34
34
|
- **Conditional workaround only:** one tool call per assistant message only when malformed/concatenated tool-input errors are observed or the harness is known-affected — healthy hosts keep parallel calls.
|
|
35
35
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<!-- RULE: STALL-01 -->
|
|
30
30
|
- Announce long waits: `Waiting for API response / tool result — will keep retrying; check your network/API provider if this persists`. Surface errors verbatim; suspect network/API — **never blame UKit**.
|
|
31
31
|
<!-- RULE: STALL-02 -->
|
|
32
|
-
- Stall rules extend EXEC-04 (every stop says why); they do not replace it. Retry guidance applies only to transient network/API failures, never deterministic invalid arguments or unsupported capabilities (e.g. fork + remote isolation): do not repeat the same call; choose a supported approach within user constraints or report the limitation.
|
|
32
|
+
- Stall rules extend EXEC-04 (every stop says why); they do not replace it. Retry guidance applies only to transient network/API failures, never deterministic invalid arguments or unsupported capabilities (e.g. fork + remote isolation): do not repeat the same call; choose a supported approach within user constraints or report the limitation. omp `bash`: `name` = supervised SERVICE (rejects `async`/`timeout`; readiness = `ready.timeout`) — finite commands omit `name`/`ready`; after a "Service mode does not accept async or timeout" error drop `name` (finite) or drop `async`/`timeout` (service). UKit's omp bridge blocks that combination where the host runs hooks first; otherwise this is guidance only.
|
|
33
33
|
<!-- RULE: STALL-03 -->
|
|
34
34
|
- **Conditional workaround only:** one tool call per assistant message only when malformed/concatenated tool-input errors are observed or the harness is known-affected — healthy hosts keep parallel calls.
|
|
35
35
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<!-- RULE: STALL-01 -->
|
|
27
27
|
- Announce long waits: `Waiting for API response / tool result — will keep retrying; check your network/API provider if this persists`. Surface errors verbatim; suspect network/API — **never blame UKit**.
|
|
28
28
|
<!-- RULE: STALL-02 -->
|
|
29
|
-
- Stall rules extend EXEC-04 (every stop says why); they do not replace it. Retry guidance applies only to transient network/API failures, never deterministic invalid arguments or unsupported capabilities (e.g. fork + remote isolation): do not repeat the same call; choose a supported approach within user constraints or report the limitation.
|
|
29
|
+
- Stall rules extend EXEC-04 (every stop says why); they do not replace it. Retry guidance applies only to transient network/API failures, never deterministic invalid arguments or unsupported capabilities (e.g. fork + remote isolation): do not repeat the same call; choose a supported approach within user constraints or report the limitation. omp `bash`: `name` = supervised SERVICE (rejects `async`/`timeout`; readiness = `ready.timeout`) — finite commands omit `name`/`ready`; after a "Service mode does not accept async or timeout" error drop `name` (finite) or drop `async`/`timeout` (service). UKit's omp bridge blocks that combination where the host runs hooks first; otherwise this is guidance only.
|
|
30
30
|
<!-- RULE: STALL-03 -->
|
|
31
31
|
- **Conditional workaround only:** one tool call per assistant message only when malformed/concatenated tool-input errors are observed or the harness is known-affected — healthy hosts keep parallel calls.
|
|
32
32
|
|