@ngockhoale/ukit 2.4.2 → 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.
Files changed (43) hide show
  1. package/manifests/platform.full.yaml +19 -111
  2. package/package.json +2 -1
  3. package/scripts/index/refresh-index.mjs +47 -22
  4. package/src/core/compact/threshold.js +36 -6
  5. package/src/diagnostics/classifyHang.js +246 -0
  6. package/src/index/buildIndex.js +1033 -62
  7. package/templates/.claude/hooks/auto-allow-bash.sh +82 -93
  8. package/templates/.claude/hooks/block-dangerous.sh +31 -5
  9. package/templates/.claude/hooks/completion-gate.sh +51 -10
  10. package/templates/.claude/hooks/compress-output.sh +38 -6
  11. package/templates/.claude/hooks/context-hardcap-gate.sh +35 -6
  12. package/templates/.claude/hooks/context-window-guard.sh +128 -18
  13. package/templates/.claude/hooks/handoff-model-guard.sh +31 -5
  14. package/templates/.claude/hooks/handoff-resume.sh +31 -5
  15. package/templates/.claude/hooks/post-edit-verify.sh +31 -5
  16. package/templates/.claude/hooks/pre-edit-backup.sh +31 -5
  17. package/templates/.claude/hooks/protect-files.sh +31 -5
  18. package/templates/.claude/hooks/record-execution.sh +31 -5
  19. package/templates/.claude/hooks/sensitive-data-guard.sh +44 -8
  20. package/templates/.claude/hooks/skill-router.sh +31 -5
  21. package/templates/.claude/hooks/stale-spec-guard.sh +31 -5
  22. package/templates/.claude/hooks/task-watchdog.sh +108 -123
  23. package/templates/.claude/hooks/verification-guard.sh +107 -112
  24. package/templates/.claude/hooks/vision-router.sh +49 -13
  25. package/templates/.claude/settings.json +0 -5
  26. package/templates/.claude/ukit/index/lib/index-core.mjs +960 -63
  27. package/templates/.claude/ukit/index/refresh-index.mjs +47 -22
  28. package/templates/.claude/ukit/index/route-task.mjs +610 -4
  29. package/templates/.claude/ukit/runtime/async-lock.mjs +340 -0
  30. package/templates/.claude/ukit/runtime/compact-threshold.mjs +73 -24
  31. package/templates/.claude/ukit/runtime/context-capacity.mjs +144 -0
  32. package/templates/.claude/ukit/runtime/execution-ledger.mjs +664 -170
  33. package/templates/.claude/ukit/runtime/hook-chain-budget.mjs +92 -0
  34. package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +84 -29
  35. package/templates/.claude/ukit/runtime/hook-input.sh +85 -5
  36. package/templates/.claude/ukit/runtime/hook-payload-store.mjs +160 -0
  37. package/templates/.claude/ukit/runtime/hook-process.mjs +250 -0
  38. package/templates/.claude/ukit/runtime/hook-telemetry.mjs +255 -0
  39. package/templates/.claude/ukit/runtime/hook-telemetry.sh +60 -0
  40. package/templates/.claude/ukit/runtime/stop-coordinator.mjs +509 -0
  41. package/templates/.claude/ukit/runtime/task-watchdog.mjs +180 -6
  42. package/templates/.claude/ukit/runtime/transcript-tail.mjs +1 -1
  43. package/templates/.omp/hooks/pre/ukit-bridge.js +171 -57
@@ -25,16 +25,42 @@
25
25
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
26
  # shellcheck source=/dev/null
27
27
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
28
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
29
+ # shared cleanup path before the staged payload is removed. A missing
30
+ # runtime (pre-install tree) simply leaves telemetry off.
31
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
28
32
  trap ukit_cleanup_hook_input EXIT
29
33
  # Bounded stdin (H01): stage before anything reads it; Node gets a path, never the payload.
30
34
  ukit_stage_hook_input 2097152 truncate || exit 0
31
35
  else
32
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
33
- # advisory/fail-open behavior below alive with a capped payload file.
36
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
37
+ # inline - `cat >/dev/null` used to block forever on a producer that never
38
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
34
39
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
35
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
36
- cat >/dev/null 2>&1
37
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
40
+ if [ -e /dev/fd/0 ]; then
41
+ exec 8<&0
42
+ head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
43
+ else
44
+ head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
45
+ fi
46
+ __ukit_reader=$!
47
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
48
+ case "$__ukit_stage_ms" in
49
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
50
+ esac
51
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
52
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
53
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
54
+ __ukit_waiter=$!
55
+ wait "$__ukit_reader" 2>/dev/null
56
+ __ukit_stage_rc=$?
57
+ kill "$__ukit_waiter" 2>/dev/null
58
+ wait "$__ukit_waiter" 2>/dev/null
59
+ exec 8<&-
60
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
61
+ # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
62
+ # already degrades on - the shared helper's truncate posture (R4.5).
63
+ if [ "$__ukit_size" -gt 2097152 ]; then
38
64
  truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
39
65
  fi
40
66
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -80,12 +106,18 @@ const projectRoot = process.env.PROJECT_ROOT;
80
106
  const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path.trim() : '';
81
107
  if (!transcriptPath) process.exit(0);
82
108
 
83
- function loadHardCap() {
84
- try {
85
- const raw = fs.readFileSync(path.join(projectRoot, '.ukit', 'storage', 'config.json'), 'utf8');
86
- const value = JSON.parse(raw)?.compact?.hardCapTokens;
87
- if (typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value) && value > 0) return value;
88
- } catch { /* fall through to the default */ }
109
+ // Operator config is read once; a missing/corrupt file just means the defaults.
110
+ let runtimeConfig = {};
111
+ try {
112
+ runtimeConfig = JSON.parse(fs.readFileSync(path.join(projectRoot, '.ukit', 'storage', 'config.json'), 'utf8'));
113
+ } catch { /* fall through to the defaults */ }
114
+ if (!runtimeConfig || typeof runtimeConfig !== 'object') runtimeConfig = {};
115
+
116
+ // Absolute operator ceiling (compact.hardCapTokens). Negotiated capacity can only ever
117
+ // tighten this, never raise it.
118
+ function operatorHardCap() {
119
+ const value = runtimeConfig?.compact?.hardCapTokens;
120
+ if (typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value) && value > 0) return value;
89
121
  return 500_000;
90
122
  }
91
123
 
@@ -160,9 +192,89 @@ for (let i = start; i < entries.length; i += 1) {
160
192
  }
161
193
 
162
194
  const estimatedTokens = Math.round(contentChars / CHARS_PER_TOKEN);
163
- const hardCap = loadHardCap();
195
+
196
+ // Latest assistant model in the live window — this is the model that must fit the NEXT
197
+ // request, so capacity negotiation follows it. A mid-session gateway swap to a smaller
198
+ // model tightens the cap immediately; the earlier model's window is never sticky.
199
+ // (Hoisted above the ratio so both capacity negotiation and the swap note share it.)
200
+ const RECENT_MODEL_WINDOW = 40;
201
+ const recentModels = modelSequence.slice(-RECENT_MODEL_WINDOW);
202
+ const currentModel = recentModels[recentModels.length - 1] || null;
203
+
204
+ // Negotiated capacity (H22): derive the advisory cap from the best available evidence —
205
+ // env override > config override > verified model table > conservative default — instead
206
+ // of assuming one hard-coded 500k window. The source is surfaced in the warning lines
207
+ // below (capacitySource=...) so a mis-negotiated cap is diagnosable at a glance. A
208
+ // missing runtime helper (pre-install tree) keeps the shipped fallback behavior —
209
+ // advisory and fail-open either way.
210
+ async function loadCapacityResolver() {
211
+ try {
212
+ return await import(pathToFileURL(path.join(
213
+ process.env.SCRIPT_DIR || process.cwd(),
214
+ '..',
215
+ 'ukit',
216
+ 'runtime',
217
+ 'context-capacity.mjs',
218
+ )).href);
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
223
+
224
+ let hardCap = operatorHardCap();
225
+ let capacitySource = 'default';
226
+ let negotiatedCapacity = null;
227
+ try {
228
+ const capacityModule = await loadCapacityResolver();
229
+ if (capacityModule && typeof capacityModule.resolveContextCapTokens === 'function') {
230
+ const resolved = capacityModule.resolveContextCapTokens({
231
+ env: process.env,
232
+ config: runtimeConfig,
233
+ modelMetadata: { model: currentModel },
234
+ });
235
+ hardCap = resolved.capTokens;
236
+ capacitySource = resolved.capacity?.source ?? 'default';
237
+ negotiatedCapacity = resolved.capacity ?? null;
238
+ }
239
+ } catch { /* negotiation is advisory — the operator ceiling already applies */ }
164
240
  const ratio = estimatedTokens / hardCap;
165
241
 
242
+ // Publish what this hook negotiated so the compact-threshold/hard-cap-gate path stops
243
+ // assuming a 500k window: this is the only place with live evidence (env + config + the
244
+ // transcript's assistant models). Without the record, automatic compact waits until 240k
245
+ // and the fail-closed gate until 500k on a verified 200k route — after the provider already
246
+ // rejected the request. Best-effort: an unwritable cache dir must never break the guard.
247
+ const CAPACITY_RECORD_PATH = path.join(projectRoot, '.ukit', 'storage', 'cache', 'context-capacity.json');
248
+ if (negotiatedCapacity) {
249
+ try {
250
+ fs.mkdirSync(path.dirname(CAPACITY_RECORD_PATH), { recursive: true });
251
+ const previous = readCapacityRecord();
252
+ // Only rewrite when the evidence changed, so prompt caching sees a stable file.
253
+ if (!previous
254
+ || previous.tokens !== negotiatedCapacity.tokens
255
+ || previous.source !== negotiatedCapacity.source
256
+ || previous.model !== (currentModel ?? null)) {
257
+ const record = {
258
+ tokens: negotiatedCapacity.tokens,
259
+ source: negotiatedCapacity.source,
260
+ confidence: negotiatedCapacity.confidence,
261
+ model: currentModel ?? null,
262
+ updatedAt: Date.now(),
263
+ };
264
+ fs.writeFileSync(CAPACITY_RECORD_PATH, `${JSON.stringify(record)}\n`, 'utf8');
265
+ }
266
+ } catch { /* fail open: negotiation is advisory */ }
267
+ }
268
+
269
+ function readCapacityRecord() {
270
+ try {
271
+ const parsed = JSON.parse(fs.readFileSync(CAPACITY_RECORD_PATH, 'utf8'));
272
+ return parsed && typeof parsed === 'object' ? parsed : null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
166
278
  // Debounce/state file shared by the context warning and the gateway model-swap note.
167
279
  const GUARD_STATE_PATH = path.join(projectRoot, '.ukit', 'storage', 'cache', 'context-guard-state.json');
168
280
 
@@ -194,11 +306,9 @@ let persistedState = { ...guardState };
194
306
  // keep working through swaps, and on a tool error after a swap, re-check the tool and
195
307
  // call it again instead of stopping. Runs before the ratio early-exit because a swap
196
308
  // can happen at any context level.
197
- const RECENT_MODEL_WINDOW = 40;
198
309
  const SWAP_NOTE_COOLDOWN_MS = 60 * 60 * 1000;
199
- // modelSequence was collected in the single pass over entries above.
200
- const recentModels = modelSequence.slice(-RECENT_MODEL_WINDOW);
201
- const currentModel = recentModels[recentModels.length - 1] || null;
310
+ // recentModels/currentModel were hoisted above for capacity negotiation; modelSequence
311
+ // was collected in the single pass over entries.
202
312
  let previousModel = null;
203
313
  for (let i = recentModels.length - 2; i >= 0; i -= 1) {
204
314
  if (recentModels[i] !== currentModel) {
@@ -286,10 +396,10 @@ const run = readRunCursor();
286
396
 
287
397
  const lines_out = [];
288
398
  if (ratio >= 1) {
289
- lines_out.push(`UKIT CONTEXT ALERT — live context ~${estimatedTokens.toLocaleString()} tokens, at/over the ${hardCap.toLocaleString()} cap (${pct}%).`);
399
+ lines_out.push(`UKIT CONTEXT ALERT — live context ~${estimatedTokens.toLocaleString()} tokens, at/over the ${hardCap.toLocaleString()} cap (${pct}%, capacitySource=${capacitySource}).`);
290
400
  lines_out.push('The next few turns risk "Your input exceeds the context window of this model".');
291
401
  } else {
292
- lines_out.push(`UKIT CONTEXT WARNING — live context ~${estimatedTokens.toLocaleString()} tokens (${pct}% of the ${hardCap.toLocaleString()} cap).`);
402
+ lines_out.push(`UKIT CONTEXT WARNING — live context ~${estimatedTokens.toLocaleString()} tokens (${pct}% of the ${hardCap.toLocaleString()} cap, capacitySource=${capacitySource}).`);
293
403
  }
294
404
 
295
405
  if (estimatePartial) {
@@ -14,16 +14,42 @@
14
14
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
15
  # shellcheck source=/dev/null
16
16
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
17
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
18
+ # shared cleanup path before the staged payload is removed. A missing
19
+ # runtime (pre-install tree) simply leaves telemetry off.
20
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
17
21
  trap ukit_cleanup_hook_input EXIT
18
22
  # Bounded stdin (H01): stage before anything reads it; the shell var is capped.
19
23
  ukit_stage_hook_input 2097152 truncate || exit 0
20
24
  else
21
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
22
- # wrapper's own fail-loud/advisory behavior below alive.
25
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
26
+ # inline - `cat >/dev/null` used to block forever on a producer that never
27
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
23
28
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
24
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
25
- cat >/dev/null 2>&1
26
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
29
+ if [ -e /dev/fd/0 ]; then
30
+ exec 8<&0
31
+ head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
32
+ else
33
+ head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
34
+ fi
35
+ __ukit_reader=$!
36
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
37
+ case "$__ukit_stage_ms" in
38
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
39
+ esac
40
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
41
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
42
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
43
+ __ukit_waiter=$!
44
+ wait "$__ukit_reader" 2>/dev/null
45
+ __ukit_stage_rc=$?
46
+ kill "$__ukit_waiter" 2>/dev/null
47
+ wait "$__ukit_waiter" 2>/dev/null
48
+ exec 8<&-
49
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
50
+ # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
51
+ # already degrades on - the shared helper's truncate posture (R4.5).
52
+ if [ "$__ukit_size" -gt 2097152 ]; then
27
53
  truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
28
54
  fi
29
55
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -22,16 +22,42 @@
22
22
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
23
  # shellcheck source=/dev/null
24
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
25
29
  trap ukit_cleanup_hook_input EXIT
26
30
  # Bounded stdin (H01): stage before anything reads it; Node gets a path, never the payload.
27
31
  ukit_stage_hook_input 2097152 truncate || exit 0
28
32
  else
29
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
30
- # advisory/fail-open behavior below alive with a capped payload file.
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.
31
36
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
32
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
33
- cat >/dev/null 2>&1
34
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
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
35
61
  truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
36
62
  fi
37
63
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -5,16 +5,42 @@
5
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  # shellcheck source=/dev/null
7
7
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
8
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
9
+ # shared cleanup path before the staged payload is removed. A missing
10
+ # runtime (pre-install tree) simply leaves telemetry off.
11
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
8
12
  trap ukit_cleanup_hook_input EXIT
9
13
  # Bounded stdin (H01): stage before anything reads it; the shell var is capped.
10
14
  ukit_stage_hook_input 2097152 truncate || exit 0
11
15
  else
12
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
13
- # wrapper's own fail-loud/advisory behavior below alive.
16
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
17
+ # inline - `cat >/dev/null` used to block forever on a producer that never
18
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
14
19
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
15
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
16
- cat >/dev/null 2>&1
17
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
20
+ if [ -e /dev/fd/0 ]; then
21
+ exec 8<&0
22
+ head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
23
+ else
24
+ head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
25
+ fi
26
+ __ukit_reader=$!
27
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
28
+ case "$__ukit_stage_ms" in
29
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
30
+ esac
31
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
32
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
33
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
34
+ __ukit_waiter=$!
35
+ wait "$__ukit_reader" 2>/dev/null
36
+ __ukit_stage_rc=$?
37
+ kill "$__ukit_waiter" 2>/dev/null
38
+ wait "$__ukit_waiter" 2>/dev/null
39
+ exec 8<&-
40
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
41
+ # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
42
+ # already degrades on - the shared helper's truncate posture (R4.5).
43
+ if [ "$__ukit_size" -gt 2097152 ]; then
18
44
  truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
19
45
  fi
20
46
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -5,16 +5,42 @@
5
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  # shellcheck source=/dev/null
7
7
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
8
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
9
+ # shared cleanup path before the staged payload is removed. A missing
10
+ # runtime (pre-install tree) simply leaves telemetry off.
11
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
8
12
  trap ukit_cleanup_hook_input EXIT
9
13
  # Bounded stdin (H01): stage before anything reads it; the shell var is capped.
10
14
  ukit_stage_hook_input 2097152 truncate || exit 0
11
15
  else
12
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
13
- # wrapper's own fail-loud/advisory behavior below alive.
16
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
17
+ # inline - `cat >/dev/null` used to block forever on a producer that never
18
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
14
19
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
15
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
16
- cat >/dev/null 2>&1
17
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
20
+ if [ -e /dev/fd/0 ]; then
21
+ exec 8<&0
22
+ head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
23
+ else
24
+ head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
25
+ fi
26
+ __ukit_reader=$!
27
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
28
+ case "$__ukit_stage_ms" in
29
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
30
+ esac
31
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
32
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
33
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
34
+ __ukit_waiter=$!
35
+ wait "$__ukit_reader" 2>/dev/null
36
+ __ukit_stage_rc=$?
37
+ kill "$__ukit_waiter" 2>/dev/null
38
+ wait "$__ukit_waiter" 2>/dev/null
39
+ exec 8<&-
40
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
41
+ # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
42
+ # already degrades on - the shared helper's truncate posture (R4.5).
43
+ if [ "$__ukit_size" -gt 2097152 ]; then
18
44
  truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
19
45
  fi
20
46
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -5,16 +5,42 @@
5
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  # shellcheck source=/dev/null
7
7
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
8
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
9
+ # shared cleanup path before the staged payload is removed. A missing
10
+ # runtime (pre-install tree) simply leaves telemetry off.
11
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
8
12
  trap ukit_cleanup_hook_input EXIT
9
13
  # Bounded stdin (H01): stage before anything reads it; the shell var is capped.
10
14
  ukit_stage_hook_input 2097152 truncate || exit 0
11
15
  else
12
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
13
- # wrapper's own fail-loud/advisory behavior below alive.
16
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
17
+ # inline - `cat >/dev/null` used to block forever on a producer that never
18
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
14
19
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
15
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
16
- cat >/dev/null 2>&1
17
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
20
+ if [ -e /dev/fd/0 ]; then
21
+ exec 8<&0
22
+ head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
23
+ else
24
+ head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
25
+ fi
26
+ __ukit_reader=$!
27
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
28
+ case "$__ukit_stage_ms" in
29
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
30
+ esac
31
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
32
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
33
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
34
+ __ukit_waiter=$!
35
+ wait "$__ukit_reader" 2>/dev/null
36
+ __ukit_stage_rc=$?
37
+ kill "$__ukit_waiter" 2>/dev/null
38
+ wait "$__ukit_waiter" 2>/dev/null
39
+ exec 8<&-
40
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
41
+ # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
42
+ # already degrades on - the shared helper's truncate posture (R4.5).
43
+ if [ "$__ukit_size" -gt 2097152 ]; then
18
44
  truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
19
45
  fi
20
46
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -5,17 +5,43 @@
5
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  # shellcheck source=/dev/null
7
7
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
8
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
9
+ # shared cleanup path before the staged payload is removed. A missing
10
+ # runtime (pre-install tree) simply leaves telemetry off.
11
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
8
12
  trap ukit_cleanup_hook_input EXIT
9
13
  # Bounded stdin (H01): receipts can legitimately be large, so the staged file
10
14
  # is the transport and the shell var only mirrors the capped payload.
11
15
  ukit_stage_hook_input 33554432 temp-file || exit 0
12
16
  else
13
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
14
- # wrapper's own fail-loud/advisory behavior below alive.
17
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
18
+ # inline - `cat >/dev/null` used to block forever on a producer that never
19
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
15
20
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
16
- head -c 33554433 > "$UKIT_INPUT_FILE" 2>/dev/null
17
- cat >/dev/null 2>&1
18
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 33554432 ]; then
21
+ if [ -e /dev/fd/0 ]; then
22
+ exec 8<&0
23
+ head -c 33554433 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
24
+ else
25
+ head -c 33554433 > "$UKIT_INPUT_FILE" 2>/dev/null &
26
+ fi
27
+ __ukit_reader=$!
28
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
29
+ case "$__ukit_stage_ms" in
30
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
31
+ esac
32
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
33
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
34
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
35
+ __ukit_waiter=$!
36
+ wait "$__ukit_reader" 2>/dev/null
37
+ __ukit_stage_rc=$?
38
+ kill "$__ukit_waiter" 2>/dev/null
39
+ wait "$__ukit_waiter" 2>/dev/null
40
+ exec 8<&-
41
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
42
+ # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
43
+ # already degrades on - the shared helper's truncate posture (R4.5).
44
+ if [ "$__ukit_size" -gt 33554432 ]; then
19
45
  truncate -s 33554432 "$UKIT_INPUT_FILE" 2>/dev/null
20
46
  fi
21
47
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
@@ -11,7 +11,8 @@
11
11
  # 1. Read/Grep of secret files (.env*, *.pem, id_rsa, credentials.json, ...)
12
12
  # 2. Bash commands that dump secrets (cat .env, bare `env`, curl -u user:pass,
13
13
  # URL-embedded credentials, any high-confidence token in the command text)
14
- # 3. UserPromptSubmit prompt text containing high-confidence secret patterns
14
+ # 3. UserPromptSubmit prompt text containing high-confidence secret patterns;
15
+ # complete machine-generated envelopes are delivered with a redacted advisory
15
16
  #
16
17
  # FAIL-CLOSED on detection (exit 2). Never echoes the detected value — only a
17
18
  # redacted preview plus a truncated sha256 (12 hex chars: enough for the human
@@ -32,6 +33,10 @@ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
32
33
  # unknown overflow and never materializes it in a shell variable.
33
34
  # shellcheck source=/dev/null
34
35
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
36
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
37
+ # shared cleanup path before the staged payload is removed. A missing
38
+ # runtime (pre-install tree) simply leaves telemetry off.
39
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
35
40
  trap ukit_cleanup_hook_input EXIT
36
41
  ukit_stage_hook_input 2097152 refuse
37
42
  STAGE_RC=$?
@@ -39,11 +44,34 @@ if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
39
44
  exit "$STAGE_RC"
40
45
  fi
41
46
  else
42
- # Runtime helper missing (pre-install tree): same refuse posture, inline.
43
- UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")" || exit 2
44
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
45
- cat >/dev/null 2>&1
46
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
47
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
48
+ # inline - `cat >/dev/null` used to block forever on a producer that never
49
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
50
+ UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
51
+ if [ -e /dev/fd/0 ]; then
52
+ exec 8<&0
53
+ head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
54
+ else
55
+ head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
56
+ fi
57
+ __ukit_reader=$!
58
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
59
+ case "$__ukit_stage_ms" in
60
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
61
+ esac
62
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
63
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
64
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
65
+ __ukit_waiter=$!
66
+ wait "$__ukit_reader" 2>/dev/null
67
+ __ukit_stage_rc=$?
68
+ kill "$__ukit_waiter" 2>/dev/null
69
+ wait "$__ukit_waiter" 2>/dev/null
70
+ exec 8<&-
71
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
72
+ if [ "$__ukit_stage_rc" -ne 0 ] || [ "$__ukit_size" -gt 2097152 ]; then
73
+ # Fail-closed: an unstaged/oversized payload was never inspected
74
+ # (exit 2 blocks; 1 would be a non-blocking hook error).
47
75
  rm -f "$UKIT_INPUT_FILE"
48
76
  exit 2
49
77
  fi
@@ -289,9 +317,17 @@ if (findings.length === 0) {
289
317
  // ("Waiting for N background agents"). These envelopes flow with a redacted
290
318
  // advisory: labels + counts only, never values. Everything a human types —
291
319
  // and every other event/channel — stays fail-closed below.
292
- const MACHINE_ENVELOPE_RE = /^\s*<(?:task-notification|system-reminder|cross-session-message)\b/i;
320
+ const MACHINE_ENVELOPE_RE = /^(?:task-notification|system-reminder|cross-session-message)$/i;
321
+ function isCompleteMachineEnvelope(text) {
322
+ const trimmed = String(text || '').trim();
323
+ const opening = /^<([a-z-]+)(?:\s[^>]*)?>/i.exec(trimmed);
324
+ if (!opening || !MACHINE_ENVELOPE_RE.test(opening[1])) return false;
325
+ const tag = opening[1];
326
+ const closing = new RegExp(`</${tag}>$`, 'i');
327
+ return closing.test(trimmed);
328
+ }
293
329
  const promptText = textChannels.length > 0 ? textChannels[0].text : null;
294
- if (event === 'UserPromptSubmit' && promptText && MACHINE_ENVELOPE_RE.test(promptText)) {
330
+ if (event === 'UserPromptSubmit' && promptText && isCompleteMachineEnvelope(promptText)) {
295
331
  const labels = [...new Set(findings.map((f) => f.label))];
296
332
  process.stdout.write(`${JSON.stringify({
297
333
  systemMessage: `UKit sensitive-data gate: ${findings.length} secret-shaped value(s) inside a machine-generated notification (${labels.slice(0, 3).join('; ')}${labels.length > 3 ? '; …' : ''}). Delivered without blocking — treat as fixture/log plumbing, not a typed secret. Never repeat the values in your reply.`,
@@ -5,17 +5,43 @@
5
5
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  # shellcheck source=/dev/null
7
7
  if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
8
+ # TASK-019: arm advisory latency telemetry; the finish row is written by the
9
+ # shared cleanup path before the staged payload is removed. A missing
10
+ # runtime (pre-install tree) simply leaves telemetry off.
11
+ source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
8
12
  trap ukit_cleanup_hook_input EXIT
9
13
  # Bounded stdin (H01): stage before anything reads it; the shell var is capped,
10
14
  # env passes only the staged-file path — never the payload.
11
15
  ukit_stage_hook_input 2097152 truncate || exit 0
12
16
  else
13
- # Runtime helper missing (pre-install tree): bounded inline staging keeps the
14
- # wrapper's own fail-loud/advisory behavior below alive.
17
+ # Runtime helper missing (pre-install tree): the SAME bounded staging,
18
+ # inline - `cat >/dev/null` used to block forever on a producer that never
19
+ # closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
15
20
  UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
16
- head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null
17
- cat >/dev/null 2>&1
18
- if [ "$(wc -c < "$UKIT_INPUT_FILE" | tr -d '[:space:]')" -gt 2097152 ]; then
21
+ if [ -e /dev/fd/0 ]; then
22
+ exec 8<&0
23
+ head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
24
+ else
25
+ head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
26
+ fi
27
+ __ukit_reader=$!
28
+ __ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
29
+ case "$__ukit_stage_ms" in
30
+ ''|*[!0-9]*) __ukit_stage_ms=2000 ;;
31
+ esac
32
+ [ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
33
+ printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
34
+ ( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
35
+ __ukit_waiter=$!
36
+ wait "$__ukit_reader" 2>/dev/null
37
+ __ukit_stage_rc=$?
38
+ kill "$__ukit_waiter" 2>/dev/null
39
+ wait "$__ukit_waiter" 2>/dev/null
40
+ exec 8<&-
41
+ __ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
42
+ # A bounded cut (rc != 0) leaves a short payload that the JSON consumer
43
+ # already degrades on - the shared helper's truncate posture (R4.5).
44
+ if [ "$__ukit_size" -gt 2097152 ]; then
19
45
  truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
20
46
  fi
21
47
  trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT