@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
|
@@ -22,26 +22,80 @@
|
|
|
22
22
|
# loud early warning is the thing that actually helps: it arrives while compacting or
|
|
23
23
|
# starting a fresh session still works.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
# shellcheck source=/dev/null
|
|
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
|
|
32
|
+
trap ukit_cleanup_hook_input EXIT
|
|
33
|
+
# Bounded stdin (H01): stage before anything reads it; Node gets a path, never the payload.
|
|
34
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
35
|
+
else
|
|
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.
|
|
39
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
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
|
|
64
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
65
|
+
fi
|
|
66
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
67
|
+
fi
|
|
26
68
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
27
69
|
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
70
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" SCRIPT_DIR="$SCRIPT_DIR" node --input-type=module <<'NODE' || true
|
|
71
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
|
|
72
|
+
// H04: the transcript scan below is async and tail-bounded. It must be abandoned past
|
|
73
|
+
// the existing 3 s self-deadline: this controller fires a moment BEFORE the hard exit so
|
|
74
|
+
// the in-flight read unwinds cooperatively, and the timer underneath still guarantees the
|
|
75
|
+
// process is gone at the deadline no matter what. Every path exits 0 — advisory only.
|
|
76
|
+
const scanAbort = new AbortController();
|
|
77
|
+
setTimeout(() => { try { scanAbort.abort(); } catch { /* already aborted */ } }, Math.max(0, HOOK_DEADLINE_MS - 250)).unref();
|
|
78
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
79
|
+
import fs from 'node:fs';
|
|
80
|
+
import path from 'node:path';
|
|
81
|
+
import { pathToFileURL } from 'node:url';
|
|
31
82
|
|
|
32
|
-
// Reading
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
83
|
+
// Reading only the TAIL is also the safe direction: if the last compact boundary is
|
|
84
|
+
// older than the tail, we count the whole tail as live context, which over-estimates and
|
|
85
|
+
// warns earlier. The byte ceiling itself lives in transcript-tail.mjs
|
|
86
|
+
// (DEFAULT_TAIL_MAX_BYTES) so the reader and its regression tests share one number —
|
|
87
|
+
// never again the old whole-transcript sync stat/read/split of up to 24 MiB per prompt.
|
|
36
88
|
|
|
37
89
|
// Chars-per-token. Calibrated against a real 2843-line transcript: whole-file bytes read
|
|
38
90
|
// ~1.44M "tokens" (constant false alarms), while message content after the last compact
|
|
39
91
|
// boundary read ~145k for a session that did in fact need compacting right after.
|
|
40
92
|
const CHARS_PER_TOKEN = 4;
|
|
41
93
|
|
|
94
|
+
let rawInput = '';
|
|
95
|
+
try { rawInput = fs.readFileSync(process.env.INPUT_FILE || '', 'utf8'); } catch {}
|
|
42
96
|
const payload = (() => {
|
|
43
97
|
try {
|
|
44
|
-
const parsed = JSON.parse(
|
|
98
|
+
const parsed = JSON.parse(rawInput);
|
|
45
99
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
46
100
|
} catch {
|
|
47
101
|
return {};
|
|
@@ -52,70 +106,74 @@ const projectRoot = process.env.PROJECT_ROOT;
|
|
|
52
106
|
const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path.trim() : '';
|
|
53
107
|
if (!transcriptPath) process.exit(0);
|
|
54
108
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
121
|
+
return 500_000;
|
|
68
122
|
}
|
|
69
123
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
124
|
+
// H04: bounded async tail scan instead of a whole-transcript sync stat/read/split. The
|
|
125
|
+
// runtime module owns the byte ceiling (DEFAULT_TAIL_MAX_BYTES, regression-tested well
|
|
126
|
+
// under the old 24 MiB) and the fail-open posture. A missing runtime helper (pre-install
|
|
127
|
+
// tree) simply means nothing to measure: the advisory guard exits 0.
|
|
128
|
+
async function loadTailScanner() {
|
|
73
129
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
return await import(pathToFileURL(path.join(
|
|
131
|
+
process.env.SCRIPT_DIR || process.cwd(),
|
|
132
|
+
'..',
|
|
133
|
+
'ukit',
|
|
134
|
+
'runtime',
|
|
135
|
+
'transcript-tail.mjs',
|
|
136
|
+
)).href);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
79
140
|
}
|
|
80
141
|
|
|
81
|
-
|
|
82
|
-
|
|
142
|
+
const scanner = await loadTailScanner();
|
|
143
|
+
if (!scanner || typeof scanner.scanTranscriptTail !== 'function') process.exit(0);
|
|
144
|
+
let tail;
|
|
83
145
|
try {
|
|
84
|
-
({
|
|
146
|
+
tail = await scanner.scanTranscriptTail(transcriptPath, { signal: scanAbort.signal });
|
|
85
147
|
} catch {
|
|
86
148
|
// Transcript unreadable (first prompt of a session, permissions, races). Nothing to
|
|
87
149
|
// measure and nothing worth reporting.
|
|
88
150
|
process.exit(0);
|
|
89
151
|
}
|
|
90
|
-
|
|
91
|
-
const lines = text.split('\n').filter(Boolean);
|
|
92
|
-
if (truncated) lines.shift(); // a tail read almost certainly split the first line
|
|
152
|
+
const entries = Array.isArray(tail?.entries) ? tail.entries : [];
|
|
93
153
|
|
|
94
154
|
// Everything before the last compact boundary is already summarised away and is NOT in
|
|
95
155
|
// the live context. Counting it is what makes naive file-size estimates useless.
|
|
96
156
|
let start = 0;
|
|
97
157
|
let boundaryTs = null;
|
|
98
|
-
for (let i =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
} catch { /* not a usable boundary line */ }
|
|
158
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
159
|
+
const entry = entries[i];
|
|
160
|
+
if (entry?.type === 'system' && entry?.subtype === 'compact_boundary') {
|
|
161
|
+
start = i + 1;
|
|
162
|
+
boundaryTs = Date.parse(entry.timestamp) || null;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
108
165
|
}
|
|
109
166
|
|
|
167
|
+
// If the tail window cuts off history AND holds no boundary, the newest compact boundary
|
|
168
|
+
// may sit before the window: the estimate counts only what was read and is a lower
|
|
169
|
+
// bound. Say so instead of pretending whole-session precision.
|
|
170
|
+
const estimatePartial = Boolean(tail?.truncated) && boundaryTs === null;
|
|
171
|
+
|
|
110
172
|
let contentChars = 0;
|
|
111
173
|
let sidechainEntries = 0;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
entry = JSON.parse(lines[i]);
|
|
116
|
-
} catch {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
174
|
+
const modelSequence = [];
|
|
175
|
+
for (let i = start; i < entries.length; i += 1) {
|
|
176
|
+
const entry = entries[i];
|
|
119
177
|
// Subagent traffic lives in its own context window, not the main one. Count it
|
|
120
178
|
// separately: it is a leading indicator of the "many teammates at once" blow-up, but
|
|
121
179
|
// adding it to the main estimate would overstate the main window.
|
|
@@ -123,6 +181,10 @@ for (let i = start; i < lines.length; i += 1) {
|
|
|
123
181
|
sidechainEntries += 1;
|
|
124
182
|
continue;
|
|
125
183
|
}
|
|
184
|
+
if (entry?.type === 'assistant') {
|
|
185
|
+
const model = entry?.message?.model;
|
|
186
|
+
if (typeof model === 'string' && model.trim()) modelSequence.push(model.trim());
|
|
187
|
+
}
|
|
126
188
|
if (entry?.type !== 'user' && entry?.type !== 'assistant' && entry?.type !== 'attachment') continue;
|
|
127
189
|
try {
|
|
128
190
|
contentChars += JSON.stringify(entry.message ?? entry.attachment ?? '').length;
|
|
@@ -130,9 +192,89 @@ for (let i = start; i < lines.length; i += 1) {
|
|
|
130
192
|
}
|
|
131
193
|
|
|
132
194
|
const estimatedTokens = Math.round(contentChars / CHARS_PER_TOKEN);
|
|
133
|
-
|
|
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 */ }
|
|
134
240
|
const ratio = estimatedTokens / hardCap;
|
|
135
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
|
+
|
|
136
278
|
// Debounce/state file shared by the context warning and the gateway model-swap note.
|
|
137
279
|
const GUARD_STATE_PATH = path.join(projectRoot, '.ukit', 'storage', 'cache', 'context-guard-state.json');
|
|
138
280
|
|
|
@@ -164,22 +306,9 @@ let persistedState = { ...guardState };
|
|
|
164
306
|
// keep working through swaps, and on a tool error after a swap, re-check the tool and
|
|
165
307
|
// call it again instead of stopping. Runs before the ratio early-exit because a swap
|
|
166
308
|
// can happen at any context level.
|
|
167
|
-
const RECENT_MODEL_WINDOW = 40;
|
|
168
309
|
const SWAP_NOTE_COOLDOWN_MS = 60 * 60 * 1000;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let entry;
|
|
172
|
-
try {
|
|
173
|
-
entry = JSON.parse(lines[i]);
|
|
174
|
-
} catch {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (entry?.isSidechain || entry?.type !== 'assistant') continue;
|
|
178
|
-
const model = entry?.message?.model;
|
|
179
|
-
if (typeof model === 'string' && model.trim()) modelSequence.push(model.trim());
|
|
180
|
-
}
|
|
181
|
-
const recentModels = modelSequence.slice(-RECENT_MODEL_WINDOW);
|
|
182
|
-
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.
|
|
183
312
|
let previousModel = null;
|
|
184
313
|
for (let i = recentModels.length - 2; i >= 0; i -= 1) {
|
|
185
314
|
if (recentModels[i] !== currentModel) {
|
|
@@ -267,10 +396,14 @@ const run = readRunCursor();
|
|
|
267
396
|
|
|
268
397
|
const lines_out = [];
|
|
269
398
|
if (ratio >= 1) {
|
|
270
|
-
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}).`);
|
|
271
400
|
lines_out.push('The next few turns risk "Your input exceeds the context window of this model".');
|
|
272
401
|
} else {
|
|
273
|
-
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}).`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (estimatePartial) {
|
|
406
|
+
lines_out.push(`Note: this estimate is partial — only the last ~${Math.max(1, Math.round(Number(tail?.bytesRead || 0) / 1024))} KiB of transcript were read, so real live context may be higher.`);
|
|
274
407
|
}
|
|
275
408
|
|
|
276
409
|
if (withinCooldown) {
|
|
@@ -11,7 +11,50 @@
|
|
|
11
11
|
# Always hard-blocks (exit 2). No advisory/soft mode — the user explicitly asked for
|
|
12
12
|
# strict enforcement of this contract.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
# shellcheck source=/dev/null
|
|
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
|
|
21
|
+
trap ukit_cleanup_hook_input EXIT
|
|
22
|
+
# Bounded stdin (H01): stage before anything reads it; the shell var is capped.
|
|
23
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
24
|
+
else
|
|
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.
|
|
28
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
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
|
|
53
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
54
|
+
fi
|
|
55
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
56
|
+
fi
|
|
57
|
+
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
15
58
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
16
59
|
|
|
17
60
|
# Fast path — skip the node spawn (~70ms) when this hook provably has nothing to say.
|
|
@@ -26,12 +69,16 @@ if ! printf '%s' "$INPUT" | grep -qE 'AI_HANDOFF|git[[:space:]]+push'; then
|
|
|
26
69
|
exit 0
|
|
27
70
|
fi
|
|
28
71
|
|
|
29
|
-
|
|
72
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" node <<'NODE'
|
|
73
|
+
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
|
|
74
|
+
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
30
75
|
const fs = require('fs');
|
|
31
76
|
const path = require('path');
|
|
32
77
|
|
|
33
78
|
const payload = (() => {
|
|
34
|
-
|
|
79
|
+
let raw = '';
|
|
80
|
+
try { raw = fs.readFileSync(process.env.INPUT_FILE || '', 'utf8'); } catch {}
|
|
81
|
+
try { return JSON.parse(raw || '{}'); } catch { return {}; }
|
|
35
82
|
})();
|
|
36
83
|
const projectRoot = process.env.PROJECT_ROOT;
|
|
37
84
|
const toolName = payload?.tool_name || '';
|
|
@@ -19,10 +19,52 @@
|
|
|
19
19
|
# ADVISORY ONLY — always exit 0. A resume hint must never be able to block a session from
|
|
20
20
|
# starting.
|
|
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
|
-
|
|
67
|
+
INPUT_FILE="$UKIT_INPUT_FILE" PROJECT_ROOT="$PROJECT_ROOT" node <<'NODE' || true
|
|
26
68
|
const fs = require('fs');
|
|
27
69
|
const path = require('path');
|
|
28
70
|
const { pathToFileURL } = require('url');
|
|
@@ -35,9 +77,11 @@ const { pathToFileURL } = require('url');
|
|
|
35
77
|
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || '', 10) || 3000;
|
|
36
78
|
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
37
79
|
|
|
80
|
+
let rawInput = '';
|
|
81
|
+
try { rawInput = fs.readFileSync(process.env.INPUT_FILE || '', 'utf8'); } catch {}
|
|
38
82
|
const payload = (() => {
|
|
39
83
|
try {
|
|
40
|
-
const parsed = JSON.parse(
|
|
84
|
+
const parsed = JSON.parse(rawInput);
|
|
41
85
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
42
86
|
} catch {
|
|
43
87
|
return {};
|
|
@@ -2,7 +2,50 @@
|
|
|
2
2
|
# PostToolUse hook: verify risky Edit|Write delta after rollback bytes exist.
|
|
3
3
|
# Matched on: Edit|Write
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# shellcheck source=/dev/null
|
|
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
|
|
12
|
+
trap ukit_cleanup_hook_input EXIT
|
|
13
|
+
# Bounded stdin (H01): stage before anything reads it; the shell var is capped.
|
|
14
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
15
|
+
else
|
|
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.
|
|
19
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
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
|
|
44
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
45
|
+
fi
|
|
46
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
47
|
+
fi
|
|
48
|
+
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
6
49
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
50
|
SCRIPT="$PROJECT_ROOT/.claude/ukit/index/post-edit-verify.mjs"
|
|
8
51
|
|
|
@@ -10,4 +53,4 @@ if [ ! -f "$SCRIPT" ]; then
|
|
|
10
53
|
exit 0
|
|
11
54
|
fi
|
|
12
55
|
|
|
13
|
-
printf '%s' "$INPUT" | node "$SCRIPT"
|
|
56
|
+
printf '%s' "$INPUT" | UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT"
|
|
@@ -2,7 +2,50 @@
|
|
|
2
2
|
# PreToolUse hook: create rollback bytes for risky Edit|Write operations.
|
|
3
3
|
# Matched on: Edit|Write
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# shellcheck source=/dev/null
|
|
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
|
|
12
|
+
trap ukit_cleanup_hook_input EXIT
|
|
13
|
+
# Bounded stdin (H01): stage before anything reads it; the shell var is capped.
|
|
14
|
+
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
15
|
+
else
|
|
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.
|
|
19
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
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
|
|
44
|
+
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
45
|
+
fi
|
|
46
|
+
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
47
|
+
fi
|
|
48
|
+
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
6
49
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
50
|
SCRIPT="$PROJECT_ROOT/.claude/ukit/index/pre-edit-backup.mjs"
|
|
8
51
|
|
|
@@ -10,4 +53,4 @@ if [ ! -f "$SCRIPT" ]; then
|
|
|
10
53
|
exit 0
|
|
11
54
|
fi
|
|
12
55
|
|
|
13
|
-
printf '%s' "$INPUT" | node "$SCRIPT"
|
|
56
|
+
printf '%s' "$INPUT" | UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT"
|