@ngockhoale/ukit 2.4.1 → 2.4.3
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 +65 -0
- package/manifests/platform.full.yaml +19 -111
- package/package.json +2 -1
- package/scripts/index/refresh-index.mjs +48 -18
- package/src/cli/commands/doctor.js +59 -2
- package/src/core/compact/threshold.js +36 -6
- package/src/core/gatewayProbe.js +143 -15
- package/src/core/gatewayResilienceEnv.js +136 -7
- package/src/diagnostics/classifyHang.js +246 -0
- package/src/index/buildIndex.js +1096 -75
- package/templates/.claude/hooks/auto-allow-bash.sh +99 -87
- package/templates/.claude/hooks/auto-prune-bash.sh +4 -0
- package/templates/.claude/hooks/block-dangerous.sh +46 -1
- package/templates/.claude/hooks/completion-gate.sh +65 -7
- package/templates/.claude/hooks/compress-output.sh +49 -2
- package/templates/.claude/hooks/context-hardcap-gate.sh +52 -4
- package/templates/.claude/hooks/context-window-guard.sh +204 -71
- package/templates/.claude/hooks/handoff-model-guard.sh +50 -3
- package/templates/.claude/hooks/handoff-resume.sh +47 -3
- package/templates/.claude/hooks/post-edit-verify.sh +45 -2
- package/templates/.claude/hooks/pre-edit-backup.sh +45 -2
- package/templates/.claude/hooks/protect-files.sh +46 -1
- package/templates/.claude/hooks/record-execution.sh +46 -2
- package/templates/.claude/hooks/reinject-context.sh +1 -1
- package/templates/.claude/hooks/reset-compact-pressure.sh +4 -0
- package/templates/.claude/hooks/sensitive-data-guard.sh +101 -18
- package/templates/.claude/hooks/skill-router.sh +59 -5
- package/templates/.claude/hooks/stale-spec-guard.sh +47 -2
- package/templates/.claude/hooks/task-watchdog.sh +129 -126
- package/templates/.claude/hooks/verification-guard.sh +136 -106
- package/templates/.claude/hooks/vision-router.sh +138 -18
- package/templates/.claude/settings.json +0 -5
- package/templates/.claude/ukit/index/lib/index-core.mjs +1027 -68
- package/templates/.claude/ukit/index/post-edit-verify.mjs +8 -0
- package/templates/.claude/ukit/index/pre-edit-backup.mjs +8 -0
- package/templates/.claude/ukit/index/refresh-index.mjs +48 -18
- package/templates/.claude/ukit/index/route-task.mjs +610 -4
- package/templates/.claude/ukit/index/stale-spec-check.mjs +8 -0
- package/templates/.claude/ukit/runtime/async-lock.mjs +340 -0
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +73 -24
- package/templates/.claude/ukit/runtime/context-capacity.mjs +144 -0
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +672 -170
- package/templates/.claude/ukit/runtime/hook-chain-budget.mjs +92 -0
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +84 -29
- package/templates/.claude/ukit/runtime/hook-input.mjs +120 -0
- package/templates/.claude/ukit/runtime/hook-input.sh +140 -0
- package/templates/.claude/ukit/runtime/hook-payload-store.mjs +160 -0
- package/templates/.claude/ukit/runtime/hook-process.mjs +250 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +255 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.sh +60 -0
- package/templates/.claude/ukit/runtime/output-compression.mjs +8 -0
- package/templates/.claude/ukit/runtime/reinject-context.mjs +8 -0
- package/templates/.claude/ukit/runtime/stop-coordinator.mjs +509 -0
- package/templates/.claude/ukit/runtime/task-watchdog.mjs +180 -6
- package/templates/.claude/ukit/runtime/transcript-tail.mjs +107 -0
- package/templates/.omp/hooks/pre/ukit-bridge.js +171 -57
|
@@ -19,19 +19,84 @@
|
|
|
19
19
|
# every unrelated prompt is the false-positive loop this version exists to kill.
|
|
20
20
|
# A named path that does not resolve from the project root gets a short note only.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
# shellcheck source=/dev/null
|
|
24
|
+
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
25
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
26
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
27
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
28
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
29
|
+
trap ukit_cleanup_hook_input EXIT
|
|
30
|
+
# Bounded stdin (H01): stage before anything reads it; Node gets a path, never the payload.
|
|
31
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
32
|
+
else
|
|
33
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
34
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
35
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
36
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
37
|
+
if [ -e /dev/fd/0 ]; then
|
|
38
|
+
exec 8<&0
|
|
39
|
+
head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
40
|
+
else
|
|
41
|
+
head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
42
|
+
fi
|
|
43
|
+
__ukit_reader=$!
|
|
44
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
45
|
+
case "$__ukit_stage_ms" in
|
|
46
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
47
|
+
esac
|
|
48
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
49
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
50
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
51
|
+
__ukit_waiter=$!
|
|
52
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
53
|
+
__ukit_stage_rc=$?
|
|
54
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
55
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
56
|
+
exec 8<&-
|
|
57
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
58
|
+
# A bounded cut (rc != 0) leaves a short payload that the JSON consumer
|
|
59
|
+
# already degrades on - the shared helper's truncate posture (R4.5).
|
|
60
|
+
if [ "$__ukit_size" -gt 2097152 ]; then
|
|
61
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
62
|
+
fi
|
|
63
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
64
|
+
fi
|
|
23
65
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
24
66
|
|
|
25
|
-
|
|
26
|
-
const
|
|
67
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" node <<'NODE' || true
|
|
68
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 8000;
|
|
69
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
70
|
+
const fs = require('fs').promises;
|
|
27
71
|
const path = require('path');
|
|
28
|
-
const {
|
|
72
|
+
const { spawn } = require('child_process');
|
|
29
73
|
const { pathToFileURL } = require('url');
|
|
30
74
|
|
|
75
|
+
// TASK-015: a self-deadline can only preempt work that yields to the event
|
|
76
|
+
// loop. The extractor is spawned asynchronously with an abort timer armed
|
|
77
|
+
// strictly BEFORE HOOK_DEADLINE_MS (so the deadline always wins), killed with
|
|
78
|
+
// SIGKILL (no child can ignore it), and every live child is reaped on process
|
|
79
|
+
// exit as a backstop. The old synchronous spawn blocked the loop and made the
|
|
80
|
+
// unref'd watchdog above un-fireable — banned on this hot path.
|
|
81
|
+
const liveChildren = new Set();
|
|
82
|
+
process.on('exit', () => {
|
|
83
|
+
for (const child of liveChildren) {
|
|
84
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// TASK-015 fix round 1: every hot-path fs call is awaited. A synchronous read
|
|
89
|
+
// parks the event loop for the whole syscall, so the unref'd self-deadline above
|
|
90
|
+
// could never fire while a project filesystem stalled.
|
|
91
|
+
async function readTextSafe(filePath) {
|
|
92
|
+
try { return await fs.readFile(filePath, 'utf8'); } catch { return ''; }
|
|
93
|
+
}
|
|
94
|
+
|
|
31
95
|
(async () => {
|
|
96
|
+
const rawInput = await readTextSafe(process.env.INPUT_FILE || '');
|
|
32
97
|
const payload = (() => {
|
|
33
98
|
try {
|
|
34
|
-
const parsed = JSON.parse(
|
|
99
|
+
const parsed = JSON.parse(rawInput);
|
|
35
100
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
36
101
|
} catch {
|
|
37
102
|
return {};
|
|
@@ -53,7 +118,7 @@ const { pathToFileURL } = require('url');
|
|
|
53
118
|
async function detectUnicModeSafe() {
|
|
54
119
|
try {
|
|
55
120
|
const gatewayPath = path.join(projectRoot, '.claude', 'ukit', 'index', 'unic-gateway.mjs');
|
|
56
|
-
|
|
121
|
+
try { await fs.access(gatewayPath); } catch { return null; }
|
|
57
122
|
const mod = await import(pathToFileURL(gatewayPath).href);
|
|
58
123
|
if (typeof mod.detectUnicGateway !== 'function') return null;
|
|
59
124
|
return !!mod.detectUnicGateway({ rootDir: projectRoot })?.unicMode;
|
|
@@ -70,8 +135,9 @@ const { pathToFileURL } = require('url');
|
|
|
70
135
|
// whose receipt could never be accepted. Only a POSITIVE false short-circuits; null
|
|
71
136
|
// (detection failed) still routes, matching the gate's fail-safe direction.
|
|
72
137
|
//
|
|
73
|
-
// Cheaper than the old order, too: this replaces
|
|
74
|
-
// with one local import on the overwhelmingly common off-gateway
|
|
138
|
+
// Cheaper than the old order, too: this replaces a synchronous node spawn
|
|
139
|
+
// per prompt with one local import on the overwhelmingly common off-gateway
|
|
140
|
+
// path.
|
|
75
141
|
if (unicMode === false) {
|
|
76
142
|
process.exit(0);
|
|
77
143
|
}
|
|
@@ -101,7 +167,7 @@ const { pathToFileURL } = require('url');
|
|
|
101
167
|
// pair) and silently skips it. Kept here as a small inline copy rather than
|
|
102
168
|
// a cross-file import — the contract is a few lines, the cost of drift is
|
|
103
169
|
// small, and the hook must stay self-contained (no module resolution).
|
|
104
|
-
function stripPathNoise(raw) {
|
|
170
|
+
async function stripPathNoise(raw) {
|
|
105
171
|
const value = String(raw ?? '').trim();
|
|
106
172
|
if (!value) return value;
|
|
107
173
|
const pairs = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'" };
|
|
@@ -134,7 +200,8 @@ const { pathToFileURL } = require('url');
|
|
|
134
200
|
for (const c of candidates) {
|
|
135
201
|
if (!c) continue;
|
|
136
202
|
try {
|
|
137
|
-
|
|
203
|
+
await fs.access(path.resolve(c));
|
|
204
|
+
return c;
|
|
138
205
|
} catch {
|
|
139
206
|
// ignore
|
|
140
207
|
}
|
|
@@ -142,6 +209,56 @@ const { pathToFileURL } = require('url');
|
|
|
142
209
|
return noSoft !== value ? noSoft : value;
|
|
143
210
|
}
|
|
144
211
|
|
|
212
|
+
// TASK-015: async spawn, one settle path per child. The abort timer fires
|
|
213
|
+
// strictly before the hook deadline (min floor keeps it meaningful if the
|
|
214
|
+
// deadline is configured tiny) and is unref'd so it never holds the loop
|
|
215
|
+
// open by itself. Stdout capture is bounded; stream/child errors are absorbed
|
|
216
|
+
// so an abort racing a child exit settles exactly once with no unhandled
|
|
217
|
+
// rejection. Any failure degrades to a bounded diagnostic + advisory flow.
|
|
218
|
+
function runExtractorAsync(args) {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
let child;
|
|
221
|
+
try {
|
|
222
|
+
child = spawn('node', args, { cwd: projectRoot, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
reject(err);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
liveChildren.add(child);
|
|
228
|
+
let stdout = '';
|
|
229
|
+
let settled = false;
|
|
230
|
+
let abortTimer = null;
|
|
231
|
+
const settle = (err, text) => {
|
|
232
|
+
if (settled) return;
|
|
233
|
+
settled = true;
|
|
234
|
+
if (abortTimer) clearTimeout(abortTimer);
|
|
235
|
+
liveChildren.delete(child);
|
|
236
|
+
if (err) reject(err);
|
|
237
|
+
else resolve(text);
|
|
238
|
+
};
|
|
239
|
+
abortTimer = setTimeout(() => {
|
|
240
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
241
|
+
settle(new Error(`extractor killed at ${HOOK_DEADLINE_MS}ms hook deadline`));
|
|
242
|
+
}, Math.max(250, HOOK_DEADLINE_MS - 500));
|
|
243
|
+
abortTimer.unref();
|
|
244
|
+
child.stdout.on('error', () => {});
|
|
245
|
+
child.stderr.on('error', () => {});
|
|
246
|
+
child.stdout.on('data', (chunk) => {
|
|
247
|
+
if (stdout.length < 1000000) stdout += chunk; // bounded capture
|
|
248
|
+
});
|
|
249
|
+
child.on('error', (err) => settle(err));
|
|
250
|
+
child.on('close', (code) => {
|
|
251
|
+
if (code === 0) {
|
|
252
|
+
settle(null, stdout);
|
|
253
|
+
} else {
|
|
254
|
+
settle(new Error(code === null
|
|
255
|
+
? 'extractor was killed before exiting'
|
|
256
|
+
: `extractor exited with code ${code}`));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
145
262
|
const urlMatches = promptText.match(URL_IMAGE_RE) || [];
|
|
146
263
|
const textWithoutUrls = promptText.replace(URL_IMAGE_RE, ' ');
|
|
147
264
|
const localMatches = textWithoutUrls.match(LOCAL_PATH_RE) || [];
|
|
@@ -152,7 +269,8 @@ const { pathToFileURL } = require('url');
|
|
|
152
269
|
// string inside the extractor.
|
|
153
270
|
let extractorJson = null;
|
|
154
271
|
const extractorPath = path.join(projectRoot, '.claude', 'ukit', 'index', 'extract-image.mjs');
|
|
155
|
-
|
|
272
|
+
try {
|
|
273
|
+
await fs.access(extractorPath);
|
|
156
274
|
const args = [extractorPath, '--detect', '--mark-pending', '--json'];
|
|
157
275
|
if (typeof payload?.transcript_path === 'string' && payload.transcript_path.trim()) {
|
|
158
276
|
args.push('--session', payload.transcript_path.trim());
|
|
@@ -164,18 +282,20 @@ const { pathToFileURL } = require('url');
|
|
|
164
282
|
// Mirror extractor's resolveRefs input contract: the LLM sometimes leaves
|
|
165
283
|
// trailing punctuation/quotes on a path named in a prompt. Clean before
|
|
166
284
|
// handing off so the hook and the extractor agree on the ref string.
|
|
167
|
-
args.push('--ref', stripPathNoise(ref));
|
|
285
|
+
args.push('--ref', await stripPathNoise(ref));
|
|
168
286
|
}
|
|
287
|
+
let out = null;
|
|
169
288
|
try {
|
|
170
|
-
|
|
171
|
-
cwd: projectRoot,
|
|
172
|
-
encoding: 'utf8',
|
|
173
|
-
timeout: 6000,
|
|
174
|
-
});
|
|
289
|
+
out = await runExtractorAsync(args);
|
|
175
290
|
extractorJson = JSON.parse(out);
|
|
176
291
|
} catch (err) {
|
|
177
|
-
|
|
292
|
+
// Bounded diagnostic: the head of whatever the extractor emitted (if
|
|
293
|
+
// anything) plus the failure reason. Never the whole payload.
|
|
294
|
+
const head = typeof out === 'string' && out ? `; output head: ${out.slice(0, 200)}` : '';
|
|
295
|
+
process.stderr.write(`vision-router: extractor call skipped (${err?.message ?? err})${head}\n`);
|
|
178
296
|
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Missing extractor is a normal fail-open condition.
|
|
179
299
|
}
|
|
180
300
|
|
|
181
301
|
// TASK-006 contract: in --mark-pending --json mode `images[]` is the
|
|
@@ -208,11 +208,6 @@
|
|
|
208
208
|
"type": "command",
|
|
209
209
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/completion-gate.sh\"",
|
|
210
210
|
"timeout": 4
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
"type": "command",
|
|
214
|
-
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/task-watchdog.sh\"",
|
|
215
|
-
"timeout": 4
|
|
216
211
|
}
|
|
217
212
|
]
|
|
218
213
|
}
|