@windyroad/risk-scorer 0.16.4-preview.921 → 0.16.5-preview.927
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/hooks/hooks.json
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/staleness-check.sh" }] }
|
|
9
9
|
],
|
|
10
10
|
"PreToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"matcher": "Bash|Write|Edit|Read|Glob|Grep|Task|WebFetch|WebSearch",
|
|
13
|
+
"hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/quota-pace-throttle.sh" }]
|
|
14
|
+
},
|
|
11
15
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/secret-leak-gate.sh" }, { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/wip-risk-gate.sh" }] },
|
|
12
16
|
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/git-push-gate.sh" }] },
|
|
13
17
|
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-score-commit-gate.sh" }] },
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# quota-pace-throttle.sh — PreToolUse hook (P160 / ADR-093 / RFC-046).
|
|
3
|
+
#
|
|
4
|
+
# Fires before every tool call across ALL work (interactive + AFK). Mechanically
|
|
5
|
+
# paces token burn so it never sprints into a mid-flight quota hard-stop: it
|
|
6
|
+
# compares cumulative window usage% against elapsed% and, when AHEAD of pace,
|
|
7
|
+
# sleeps the calculated catch-up time (capped per firing). When behind/on pace
|
|
8
|
+
# it is a fast no-op. It NEVER blocks, NEVER asks, and fails OPEN on every
|
|
9
|
+
# abnormal path — a broken throttle must not break the session.
|
|
10
|
+
#
|
|
11
|
+
# Data source: ~/.claude/quota-state.json, written by the statusline (the only
|
|
12
|
+
# surface Claude Code passes `.rate_limits` to). Fields:
|
|
13
|
+
# { "five_used_pct": N, "five_resets_at": <unix>,
|
|
14
|
+
# "week_used_pct": N, "week_resets_at": <unix> }
|
|
15
|
+
# If the cache is missing/stale/malformed, the hook no-ops (fail-open) — the
|
|
16
|
+
# statusline-writer install is a separate, user-consented step.
|
|
17
|
+
#
|
|
18
|
+
# ponytail: pure arithmetic + one sleep; the 60s/firing cap + fail-open keep the
|
|
19
|
+
# blast radius to "at worst a slightly-slower tool call", never a stall or block.
|
|
20
|
+
|
|
21
|
+
set +e
|
|
22
|
+
CACHE="${WR_QUOTA_CACHE:-${HOME}/.claude/quota-state.json}"
|
|
23
|
+
MARKER="${WR_QUOTA_MARKER:-${TMPDIR:-/tmp}/wr-quota-throttle-last}"
|
|
24
|
+
FIVE_WINDOW=18000 # 5h in seconds
|
|
25
|
+
WEEK_WINDOW=604800 # 7d in seconds
|
|
26
|
+
WEEK_HEADROOM_PP=5 # leave 5pp weekly headroom for non-Claude-Code surfaces
|
|
27
|
+
CAP_SECONDS=60 # max sleep per firing
|
|
28
|
+
|
|
29
|
+
emit_ok() { exit 0; } # PreToolUse: silent allow, never a permissionDecision deny
|
|
30
|
+
|
|
31
|
+
# Fail-open preconditions.
|
|
32
|
+
command -v jq >/dev/null 2>&1 || emit_ok
|
|
33
|
+
[ -r "$CACHE" ] || emit_ok
|
|
34
|
+
|
|
35
|
+
now=$(date +%s 2>/dev/null) || emit_ok
|
|
36
|
+
case "$now" in ''|*[!0-9]*) emit_ok;; esac
|
|
37
|
+
|
|
38
|
+
# Recent-check no-op: if we checked <5s ago, skip recompute (keeps per-call
|
|
39
|
+
# latency negligible when we're firing on every tool call).
|
|
40
|
+
if [ -f "$MARKER" ]; then
|
|
41
|
+
last=$(cat "$MARKER" 2>/dev/null)
|
|
42
|
+
case "$last" in
|
|
43
|
+
''|*[!0-9]*) : ;;
|
|
44
|
+
*) [ $(( now - last )) -lt 5 ] && emit_ok ;;
|
|
45
|
+
esac
|
|
46
|
+
fi
|
|
47
|
+
printf '%s' "$now" > "$MARKER" 2>/dev/null
|
|
48
|
+
|
|
49
|
+
# Read the cache; fail-open on any parse miss.
|
|
50
|
+
read -r five_used five_reset week_used week_reset < <(
|
|
51
|
+
jq -r '[.five_used_pct, .five_resets_at, .week_used_pct, .week_resets_at]
|
|
52
|
+
| map(tostring) | join(" ")' "$CACHE" 2>/dev/null
|
|
53
|
+
) || emit_ok
|
|
54
|
+
for v in "$five_used" "$five_reset" "$week_used" "$week_reset"; do
|
|
55
|
+
case "$v" in ''|null|*[!0-9.]*) emit_ok;; esac
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
# elapsed% of a window = (window - remaining) / window * 100, clamped [0,100].
|
|
59
|
+
elapsed_pct() { # resets_at window_secs
|
|
60
|
+
local remaining=$(( $1 - now )) win="$2" e
|
|
61
|
+
[ "$remaining" -lt 0 ] && remaining=0
|
|
62
|
+
[ "$remaining" -gt "$win" ] && remaining="$win"
|
|
63
|
+
e=$(( (win - remaining) * 100 / win ))
|
|
64
|
+
printf '%s' "$e"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# integer-truncate the used% (cache may carry a decimal)
|
|
68
|
+
five_used_i=${five_used%%.*}; week_used_i=${week_used%%.*}
|
|
69
|
+
five_elapsed=$(elapsed_pct "$five_reset" "$FIVE_WINDOW")
|
|
70
|
+
week_elapsed=$(elapsed_pct "$week_reset" "$WEEK_WINDOW")
|
|
71
|
+
|
|
72
|
+
# pace gap per window (positive = ahead of pace = burning too fast).
|
|
73
|
+
# weekly target leaves headroom, so effective gap = used - (elapsed - headroom).
|
|
74
|
+
gap5=$(( five_used_i - five_elapsed ))
|
|
75
|
+
gap7=$(( week_used_i - (week_elapsed - WEEK_HEADROOM_PP) ))
|
|
76
|
+
|
|
77
|
+
# Take the tighter (larger-gap) window and its window length.
|
|
78
|
+
if [ "$gap5" -ge "$gap7" ]; then gap="$gap5"; win="$FIVE_WINDOW"; else gap="$gap7"; win="$WEEK_WINDOW"; fi
|
|
79
|
+
[ "$gap" -le 0 ] && emit_ok # behind/on pace → fast no-op
|
|
80
|
+
|
|
81
|
+
# Catch-up sleep: elapsed% rises at 100/win per second, so closing `gap` pp of
|
|
82
|
+
# lead needs gap/100 * win seconds. Cap per firing so no single call stalls long.
|
|
83
|
+
sleep_secs=$(( gap * win / 100 ))
|
|
84
|
+
[ "$sleep_secs" -gt "$CAP_SECONDS" ] && sleep_secs="$CAP_SECONDS"
|
|
85
|
+
[ "$sleep_secs" -le 0 ] && emit_ok
|
|
86
|
+
|
|
87
|
+
sleep "$sleep_secs" 2>/dev/null
|
|
88
|
+
emit_ok
|
package/package.json
CHANGED