@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.
- package/manifests/platform.full.yaml +19 -111
- package/package.json +2 -1
- package/scripts/index/refresh-index.mjs +47 -22
- package/src/core/compact/threshold.js +36 -6
- package/src/diagnostics/classifyHang.js +246 -0
- package/src/index/buildIndex.js +1033 -62
- package/templates/.claude/hooks/auto-allow-bash.sh +82 -93
- package/templates/.claude/hooks/block-dangerous.sh +31 -5
- package/templates/.claude/hooks/completion-gate.sh +51 -10
- package/templates/.claude/hooks/compress-output.sh +38 -6
- package/templates/.claude/hooks/context-hardcap-gate.sh +35 -6
- package/templates/.claude/hooks/context-window-guard.sh +128 -18
- package/templates/.claude/hooks/handoff-model-guard.sh +31 -5
- package/templates/.claude/hooks/handoff-resume.sh +31 -5
- package/templates/.claude/hooks/post-edit-verify.sh +31 -5
- package/templates/.claude/hooks/pre-edit-backup.sh +31 -5
- package/templates/.claude/hooks/protect-files.sh +31 -5
- package/templates/.claude/hooks/record-execution.sh +31 -5
- package/templates/.claude/hooks/sensitive-data-guard.sh +44 -8
- package/templates/.claude/hooks/skill-router.sh +31 -5
- package/templates/.claude/hooks/stale-spec-guard.sh +31 -5
- package/templates/.claude/hooks/task-watchdog.sh +108 -123
- package/templates/.claude/hooks/verification-guard.sh +107 -112
- package/templates/.claude/hooks/vision-router.sh +49 -13
- package/templates/.claude/settings.json +0 -5
- package/templates/.claude/ukit/index/lib/index-core.mjs +960 -63
- package/templates/.claude/ukit/index/refresh-index.mjs +47 -22
- package/templates/.claude/ukit/index/route-task.mjs +610 -4
- 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 +664 -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.sh +85 -5
- 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/stop-coordinator.mjs +509 -0
- package/templates/.claude/ukit/runtime/task-watchdog.mjs +180 -6
- package/templates/.claude/ukit/runtime/transcript-tail.mjs +1 -1
- package/templates/.omp/hooks/pre/ukit-bridge.js +171 -57
|
@@ -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
|
|
13
|
-
#
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
@@ -86,24 +112,30 @@ MAX_RULES="${CLAUDE_AUTO_ALLOW_MAX_RULES:-150}"
|
|
|
86
112
|
# stale snapshot and rewrite the whole file, silently dropping each other's allow rules
|
|
87
113
|
# (verified: 30 concurrent invocations kept 26/30 rules and 23/30 usage records). A lost
|
|
88
114
|
# rule means the NEXT invocation of that command prompts for permission again — the agent
|
|
89
|
-
# stalls waiting for a human click. The lock
|
|
90
|
-
# runtime token-utils withFileLock /
|
|
91
|
-
#
|
|
115
|
+
# stalls waiting for a human click. The lock is the shared runtime async-lock utility
|
|
116
|
+
# (TASK-028): protocol-compatible with runtime token-utils withFileLock /
|
|
117
|
+
# src/core/fileOps.js (mkdir-based `<file>.lock` + owner file), with the usage file as
|
|
118
|
+
# the single serialization point for both files this hook writes.
|
|
92
119
|
#
|
|
93
120
|
# Why atomic writes: Claude Code re-reads settings.local.json to decide permissions.
|
|
94
121
|
# A torn/partial write would fail JSON parsing and drop the ENTIRE allow list — every
|
|
95
122
|
# command would prompt. tmp+rename makes readers see old-or-new, never half.
|
|
96
123
|
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
# TASK-028 fail-closed policy: the acquisition budget derives from the remaining hook
|
|
125
|
+
# deadline minus a cleanup reserve, capped at a short slice, so a contended lock can
|
|
126
|
+
# never hold this hook near its own deadline. On busy/abort the mutation is SKIPPED —
|
|
127
|
+
# sensitive permission state is never written unlocked, and the command simply falls
|
|
128
|
+
# back to the normal permission prompt (the pre-UKit behaviour, always safe).
|
|
129
|
+
UKIT_RUNTIME_DIR="$SCRIPT_DIR/../ukit/runtime" node -e '
|
|
130
|
+
// Deadline must exceed the derived lock budget in this block (remaining deadline minus
|
|
131
|
+
// reserve, capped at a short slice) so the watchdog can never fire while the mutation
|
|
132
|
+
// is still legally waiting for the lock.
|
|
102
133
|
const HOOK_DEADLINE_MS = Number.parseInt(process.env.UKIT_HOOK_DEADLINE_MS || "", 10) || 8000;
|
|
134
|
+
const LOCK_STARTED_AT = Date.now();
|
|
103
135
|
setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
|
|
104
|
-
const crypto = require("crypto");
|
|
105
136
|
const fs = require("fs");
|
|
106
137
|
const path = require("path");
|
|
138
|
+
const { pathToFileURL } = require("url");
|
|
107
139
|
|
|
108
140
|
const settingsPath = process.argv[1];
|
|
109
141
|
const usagePath = process.argv[2];
|
|
@@ -139,84 +171,7 @@ function appendAudit(event, payload) {
|
|
|
139
171
|
fs.appendFileSync(auditPath, JSON.stringify({ ts: nowUtc, event, ...payload }) + "\n");
|
|
140
172
|
}
|
|
141
173
|
|
|
142
|
-
function
|
|
143
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
144
|
-
try {
|
|
145
|
-
process.kill(pid, 0);
|
|
146
|
-
return true;
|
|
147
|
-
} catch (error) {
|
|
148
|
-
return error?.code === "EPERM";
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function readLockOwner(lockPath) {
|
|
153
|
-
const owner = readJson(path.join(lockPath, "owner"), null);
|
|
154
|
-
return owner && Number.isInteger(owner.pid) && typeof owner.token === "string"
|
|
155
|
-
? owner
|
|
156
|
-
: null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function withLock(lockPath, fn) {
|
|
160
|
-
const staleMs = 10000;
|
|
161
|
-
const maxWaitMs = 5000;
|
|
162
|
-
const startedAt = Date.now();
|
|
163
|
-
const ownerToken = `${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
|
|
164
|
-
let held = false;
|
|
165
|
-
while (!held) {
|
|
166
|
-
try {
|
|
167
|
-
fs.mkdirSync(lockPath);
|
|
168
|
-
try {
|
|
169
|
-
fs.writeFileSync(path.join(lockPath, "owner"), JSON.stringify({
|
|
170
|
-
pid: process.pid,
|
|
171
|
-
token: ownerToken,
|
|
172
|
-
ts: Date.now(),
|
|
173
|
-
}));
|
|
174
|
-
} catch (error) {
|
|
175
|
-
try {
|
|
176
|
-
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
177
|
-
} catch {}
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
held = true;
|
|
181
|
-
} catch (error) {
|
|
182
|
-
if (!error || error.code !== "EEXIST") return fn();
|
|
183
|
-
try {
|
|
184
|
-
const stat = fs.statSync(lockPath);
|
|
185
|
-
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
186
|
-
const owner = readLockOwner(lockPath);
|
|
187
|
-
if (!owner || !isPidAlive(owner.pid)) {
|
|
188
|
-
try {
|
|
189
|
-
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
190
|
-
continue;
|
|
191
|
-
} catch {}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} catch {}
|
|
195
|
-
if (Date.now() - startedAt > maxWaitMs) break;
|
|
196
|
-
await new Promise((resolve) => setTimeout(resolve, 3 + Math.floor(Math.random() * 9)));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
return await fn();
|
|
201
|
-
} finally {
|
|
202
|
-
if (held) {
|
|
203
|
-
const owner = readLockOwner(lockPath);
|
|
204
|
-
if (owner?.token === ownerToken) {
|
|
205
|
-
try {
|
|
206
|
-
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
207
|
-
} catch {}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// The lock directory lives beside the usage file, so its parent must exist BEFORE the
|
|
214
|
-
// lock is taken — a first-ever run in a fresh project would otherwise fail mkdir with
|
|
215
|
-
// ENOENT, fail open, and race.
|
|
216
|
-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
217
|
-
fs.mkdirSync(path.dirname(usagePath), { recursive: true });
|
|
218
|
-
|
|
219
|
-
withLock(`${usagePath}.lock`, () => {
|
|
174
|
+
async function applyMutation() {
|
|
220
175
|
const settings = readJson(settingsPath, {});
|
|
221
176
|
if (!settings.permissions || typeof settings.permissions !== "object") {
|
|
222
177
|
settings.permissions = {};
|
|
@@ -268,7 +223,41 @@ withLock(`${usagePath}.lock`, () => {
|
|
|
268
223
|
writeJsonAtomic(settingsPath, settings);
|
|
269
224
|
}
|
|
270
225
|
writeJsonAtomic(usagePath, usage);
|
|
271
|
-
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
(async () => {
|
|
229
|
+
// The lock directory lives beside the usage file, so its parent must exist BEFORE
|
|
230
|
+
// the lock is taken — a first-ever run in a fresh project would otherwise fail
|
|
231
|
+
// mkdir with ENOENT.
|
|
232
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
233
|
+
fs.mkdirSync(path.dirname(usagePath), { recursive: true });
|
|
234
|
+
|
|
235
|
+
const runtimeDir = process.env.UKIT_RUNTIME_DIR || "";
|
|
236
|
+
const lockModulePath = runtimeDir ? path.join(runtimeDir, "async-lock.mjs") : "";
|
|
237
|
+
let lockModule = null;
|
|
238
|
+
if (lockModulePath && fs.existsSync(lockModulePath)) {
|
|
239
|
+
try {
|
|
240
|
+
lockModule = await import(pathToFileURL(lockModulePath).href);
|
|
241
|
+
} catch {
|
|
242
|
+
lockModule = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!lockModule || typeof lockModule.withAsyncLock !== "function") {
|
|
246
|
+
// Degraded install (runtime module missing): fail closed. Writing permission
|
|
247
|
+
// state unlocked silently dropped allow rules under concurrency; skipping only
|
|
248
|
+
// costs the normal permission prompt for this one command.
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const deadlineMs = lockModule.lockBudgetMs({
|
|
252
|
+
hookDeadlineMs: HOOK_DEADLINE_MS,
|
|
253
|
+
startedAt: LOCK_STARTED_AT,
|
|
254
|
+
});
|
|
255
|
+
const outcome = await lockModule.withAsyncLock(usagePath, { deadlineMs }, applyMutation);
|
|
256
|
+
if (!outcome.ok) {
|
|
257
|
+
// busy or aborted: fail closed — write nothing unlocked, exit 0.
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
})().catch(() => {});
|
|
272
261
|
' "$SETTINGS_LOCAL" "$USAGE_FILE" "$AUDIT_FILE" "$RULE" "$NOW_UTC" "$MAX_RULES" >/dev/null 2>&1 || true
|
|
273
262
|
|
|
274
263
|
exit 0
|
|
@@ -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
|
|
13
|
-
#
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
@@ -1,30 +1,68 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Stop hook:
|
|
3
|
-
#
|
|
2
|
+
# Stop hook: the ONE Stop delegate (TASK-025). Blocks premature terminal stops while
|
|
3
|
+
# routed completion evidence is missing, and — via stop-coordinator.mjs — merges the
|
|
4
|
+
# task watchdog's budget policy into the same single decision (PLAN §2 H17: both hooks
|
|
5
|
+
# used to block Stop independently).
|
|
6
|
+
#
|
|
7
|
+
# This script is a thin delegate: it stages the hook payload, hands it to
|
|
8
|
+
# .claude/ukit/runtime/stop-coordinator.mjs once, and passes the single decision
|
|
9
|
+
# through. Failure posture:
|
|
10
|
+
# - coordinator script missing (pre-install tree) → loud advisory release ("ukit install")
|
|
11
|
+
# - coordinator crashed (non-zero exit) → fail-closed BLOCK with a fixed,
|
|
12
|
+
# redacted infrastructure reason;
|
|
13
|
+
# a crashed gate must not silently
|
|
14
|
+
# release the stop.
|
|
4
15
|
|
|
5
16
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
17
|
# shellcheck source=/dev/null
|
|
7
18
|
if source "$SCRIPT_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
19
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
20
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
21
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
22
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
8
23
|
trap ukit_cleanup_hook_input EXIT
|
|
9
24
|
# Bounded stdin (H01): stage before anything reads it; the shell var is capped.
|
|
10
25
|
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
11
26
|
else
|
|
12
|
-
#
|
|
13
|
-
#
|
|
27
|
+
# wrapper's own fail-loud behavior below alive.
|
|
28
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
29
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
30
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
14
31
|
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
if [ -e /dev/fd/0 ]; then
|
|
33
|
+
exec 8<&0
|
|
34
|
+
head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
35
|
+
else
|
|
36
|
+
head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
37
|
+
fi
|
|
38
|
+
__ukit_reader=$!
|
|
39
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
40
|
+
case "$__ukit_stage_ms" in
|
|
41
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
42
|
+
esac
|
|
43
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
44
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
45
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
46
|
+
__ukit_waiter=$!
|
|
47
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
48
|
+
__ukit_stage_rc=$?
|
|
49
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
50
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
51
|
+
exec 8<&-
|
|
52
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
53
|
+
# A bounded cut (rc != 0) leaves a short payload that the JSON consumer
|
|
54
|
+
# already degrades on - the shared helper's truncate posture (R4.5).
|
|
55
|
+
if [ "$__ukit_size" -gt 2097152 ]; then
|
|
18
56
|
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
19
57
|
fi
|
|
20
58
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
21
59
|
fi
|
|
22
60
|
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
23
61
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
24
|
-
SCRIPT="$PROJECT_ROOT/.claude/ukit/runtime/
|
|
62
|
+
SCRIPT="$PROJECT_ROOT/.claude/ukit/runtime/stop-coordinator.mjs"
|
|
25
63
|
|
|
26
64
|
if [ ! -f "$SCRIPT" ]; then
|
|
27
|
-
printf '%s\n' '{"systemMessage":"UKit
|
|
65
|
+
printf '%s\n' '{"systemMessage":"UKit stop coordinator: runtime script missing — run: ukit install"}'
|
|
28
66
|
exit 0
|
|
29
67
|
fi
|
|
30
68
|
|
|
@@ -32,7 +70,10 @@ OUTPUT=$(printf '%s' "$INPUT" | UKIT_HARNESS=claude-code UKIT_HOOK_DEADLINE_MS="
|
|
|
32
70
|
STATUS=$?
|
|
33
71
|
|
|
34
72
|
if [ "$STATUS" -ne 0 ]; then
|
|
35
|
-
|
|
73
|
+
# TASK-025: the Stop coordinator is the fail-closed blocker — a crash must not
|
|
74
|
+
# release the stop. The reason is a fixed redacted string: crash detail (already
|
|
75
|
+
# written to stderr by the coordinator) never leaks into the decision.
|
|
76
|
+
printf '%s\n' '{"decision":"block","reason":"UKit stop coordinator: infrastructure failure while evaluating this stop — the stop is blocked fail-closed (details withheld). Run: ukit install, then re-send the task in a new message."}'
|
|
36
77
|
exit 0
|
|
37
78
|
fi
|
|
38
79
|
|
|
@@ -11,19 +11,51 @@ SCRIPT_PATH="$HOOK_DIR/../ukit/runtime/output-compression.mjs"
|
|
|
11
11
|
if [ -f "$SCRIPT_PATH" ]; then
|
|
12
12
|
# shellcheck source=/dev/null
|
|
13
13
|
if source "$HOOK_DIR/../ukit/runtime/hook-input.sh" 2>/dev/null; then
|
|
14
|
+
# TASK-019: arm advisory latency telemetry; the finish row is written by the
|
|
15
|
+
# shared cleanup path before the staged payload is removed. A missing
|
|
16
|
+
# runtime (pre-install tree) simply leaves telemetry off.
|
|
17
|
+
source "$HOOK_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
14
18
|
trap ukit_cleanup_hook_input EXIT
|
|
15
19
|
# Bounded stdin (H01): compressed tool output can be large, so the staged
|
|
16
20
|
# file is the transport and the shell var only mirrors the capped payload.
|
|
17
21
|
ukit_stage_hook_input 33554432 temp-file || exit 0
|
|
18
22
|
else
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
24
|
+
# inline — `cat >/dev/null` used to block forever on a producer that never
|
|
25
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
26
|
+
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
27
|
+
if [ -e /dev/fd/0 ]; then
|
|
28
|
+
exec 8<&0
|
|
29
|
+
head -c 33554433 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
30
|
+
else
|
|
31
|
+
head -c 33554433 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
32
|
+
fi
|
|
33
|
+
__ukit_reader=$!
|
|
34
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
35
|
+
case "$__ukit_stage_ms" in
|
|
36
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
37
|
+
esac
|
|
38
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
39
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
40
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
41
|
+
__ukit_waiter=$!
|
|
42
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
43
|
+
__ukit_stage_rc=$?
|
|
44
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
45
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
46
|
+
exec 8<&-
|
|
47
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
48
|
+
if [ "$__ukit_stage_rc" -ne 0 ]; then
|
|
49
|
+
# Unstaged: the hook cannot inspect this payload.
|
|
50
|
+
rm -f "$UKIT_INPUT_FILE"
|
|
51
|
+
UKIT_INPUT_FILE=""
|
|
52
|
+
else
|
|
53
|
+
if [ "$__ukit_size" -gt 33554432 ]; then
|
|
54
|
+
truncate -s 33554432 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
55
|
+
fi
|
|
25
56
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
26
57
|
fi
|
|
58
|
+
fi
|
|
27
59
|
INPUT="$(cat "$UKIT_INPUT_FILE")"
|
|
28
60
|
printf '%s' "$INPUT" | UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-8000}" node "$SCRIPT_PATH"
|
|
29
61
|
exit $?
|
|
@@ -33,16 +33,42 @@ PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
|
33
33
|
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
34
34
|
# shellcheck source=/dev/null
|
|
35
35
|
if source "$HOOK_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 "$HOOK_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
36
40
|
trap ukit_cleanup_hook_input EXIT
|
|
37
41
|
# Bounded stdin (H01): stage before anything reads it; Node gets a path, never the payload.
|
|
38
42
|
ukit_stage_hook_input 2097152 truncate || exit 0
|
|
39
43
|
else
|
|
40
|
-
# Runtime helper missing (pre-install tree): bounded
|
|
41
|
-
#
|
|
44
|
+
# Runtime helper missing (pre-install tree): the SAME bounded staging,
|
|
45
|
+
# inline - `cat >/dev/null` used to block forever on a producer that never
|
|
46
|
+
# closes the pipe (R4.5). Read cap+1 in the background, bound the wait.
|
|
42
47
|
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-hook-in.XXXXXX")"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if [ -e /dev/fd/0 ]; then
|
|
49
|
+
exec 8<&0
|
|
50
|
+
head -c 2097153 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
51
|
+
else
|
|
52
|
+
head -c 2097153 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
53
|
+
fi
|
|
54
|
+
__ukit_reader=$!
|
|
55
|
+
__ukit_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
56
|
+
case "$__ukit_stage_ms" in
|
|
57
|
+
''|*[!0-9]*) __ukit_stage_ms=2000 ;;
|
|
58
|
+
esac
|
|
59
|
+
[ "$__ukit_stage_ms" -gt 0 ] 2>/dev/null || __ukit_stage_ms=2000
|
|
60
|
+
printf -v __ukit_stage_s '%d.%03d' $((__ukit_stage_ms / 1000)) $((__ukit_stage_ms % 1000))
|
|
61
|
+
( sleep "$__ukit_stage_s" 2>/dev/null; kill -9 "$__ukit_reader" 2>/dev/null ) <&- >/dev/null 2>&1 &
|
|
62
|
+
__ukit_waiter=$!
|
|
63
|
+
wait "$__ukit_reader" 2>/dev/null
|
|
64
|
+
__ukit_stage_rc=$?
|
|
65
|
+
kill "$__ukit_waiter" 2>/dev/null
|
|
66
|
+
wait "$__ukit_waiter" 2>/dev/null
|
|
67
|
+
exec 8<&-
|
|
68
|
+
__ukit_size="$(wc -c < "$UKIT_INPUT_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
69
|
+
# A bounded cut (rc != 0) leaves a short payload that the JSON consumer
|
|
70
|
+
# already degrades on - the shared helper's truncate posture (R4.5).
|
|
71
|
+
if [ "$__ukit_size" -gt 2097152 ]; then
|
|
46
72
|
truncate -s 2097152 "$UKIT_INPUT_FILE" 2>/dev/null
|
|
47
73
|
fi
|
|
48
74
|
trap '[ -n "$UKIT_INPUT_FILE" ] && rm -f "$UKIT_INPUT_FILE"' EXIT
|
|
@@ -155,7 +181,10 @@ function readRunCursor() {
|
|
|
155
181
|
} catch {
|
|
156
182
|
state = mod.buildCompactPressureState(rawState, sessionConfig);
|
|
157
183
|
}
|
|
158
|
-
|
|
184
|
+
// projectRoot is passed explicitly so the cap is negotiated from THIS project's
|
|
185
|
+
// published capacity record rather than from whatever cwd the host happened to use —
|
|
186
|
+
// the gate is fail-closed, so it must read the same evidence the guard wrote.
|
|
187
|
+
const thresholds = mod.buildCompactThresholds({ ...config, projectRoot });
|
|
159
188
|
|
|
160
189
|
// The grace budget must survive parallel subagents hitting the cap in the same episode.
|
|
161
190
|
// A read-modify-write of one JSON file under-counts (two processes both read used=3 and
|