@miller-tech/uap 1.13.12 → 1.13.14
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/dist/.tsbuildinfo +1 -1
- package/dist/benchmarks/speculative-autotune.d.ts +46 -0
- package/dist/benchmarks/speculative-autotune.d.ts.map +1 -0
- package/dist/benchmarks/speculative-autotune.js +145 -0
- package/dist/benchmarks/speculative-autotune.js.map +1 -0
- package/dist/benchmarks/token-throughput.d.ts +46 -46
- package/dist/bin/cli.js +2 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/llama-server-optimize.js +176 -0
- package/dist/bin/llama-server-optimize.js.map +1 -1
- package/dist/bin/policy.js +0 -0
- package/dist/cli/hooks.js +1 -0
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +18 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/systemd-services.d.ts +12 -0
- package/dist/cli/systemd-services.d.ts.map +1 -0
- package/dist/cli/systemd-services.js +179 -0
- package/dist/cli/systemd-services.js.map +1 -0
- package/dist/models/types.d.ts +12 -12
- package/dist/policies/schemas/policy.d.ts +12 -12
- package/dist/types/config.d.ts +24 -24
- package/docs/deployment/QWEN35_LLAMA_CPP.md +49 -0
- package/docs/deployment/UAP_LLAMA_ANTHROPIC_PROXY_BOOTSTRAP.md +279 -0
- package/package.json +1 -1
- package/templates/hooks/loop-protection.sh +250 -0
- package/templates/hooks/post-compact.sh +14 -0
- package/templates/hooks/post-tool-use-edit-write.sh +15 -0
- package/templates/hooks/pre-compact.sh +9 -0
- package/templates/hooks/pre-tool-use-bash.sh +6 -0
- package/templates/hooks/pre-tool-use-edit-write.sh +10 -0
- package/templates/hooks/session-start.sh +64 -44
- package/templates/hooks/stop.sh +9 -0
- package/tools/agents/scripts/anthropic_proxy.py +716 -166
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +51 -0
- package/tools/agents/scripts/__pycache__/anthropic_proxy.cpython-313.pyc +0 -0
- package/tools/agents/scripts/__pycache__/tool_call_wrapper.cpython-313.pyc +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# UAP Loop Protection & Token Budget Circuit Breaker
|
|
4
|
+
# ============================================================
|
|
5
|
+
# Shared library sourced by all UAP hooks.
|
|
6
|
+
# Tracks hook invocation frequency and detects runaway loops
|
|
7
|
+
# that waste tokens (and money) by emitting the same system
|
|
8
|
+
# reminders, build-gate warnings, or compliance blocks
|
|
9
|
+
# hundreds of times in a single session.
|
|
10
|
+
#
|
|
11
|
+
# MECHANISM:
|
|
12
|
+
# - Maintains a lightweight state file per session
|
|
13
|
+
# - Counts hook invocations per type within sliding windows
|
|
14
|
+
# - Suppresses redundant output after thresholds are hit
|
|
15
|
+
# - Logs suppressed events for post-mortem analysis
|
|
16
|
+
# - Provides hard circuit-breaker after extreme loop counts
|
|
17
|
+
#
|
|
18
|
+
# USAGE (source from any hook script):
|
|
19
|
+
# source "$(dirname "$0")/loop-protection.sh"
|
|
20
|
+
# if lp_should_suppress "post-tool-use-edit-write"; then
|
|
21
|
+
# exit 0 # skip output, loop detected
|
|
22
|
+
# fi
|
|
23
|
+
# lp_record_invocation "post-tool-use-edit-write"
|
|
24
|
+
#
|
|
25
|
+
# CONFIGURATION (environment variables):
|
|
26
|
+
# UAP_LP_DISABLED=1 Disable loop protection entirely
|
|
27
|
+
# UAP_LP_SOFT_LIMIT=15 Warn after N invocations per hook (default: 15)
|
|
28
|
+
# UAP_LP_HARD_LIMIT=50 Suppress output after N (default: 50)
|
|
29
|
+
# UAP_LP_CIRCUIT_BREAK=200 Hard stop — emit circuit breaker msg (default: 200)
|
|
30
|
+
# UAP_LP_WINDOW_SECS=300 Sliding window in seconds (default: 300 = 5 min)
|
|
31
|
+
# UAP_LP_DEDUP_SECS=5 Min seconds between identical outputs (default: 5)
|
|
32
|
+
# ============================================================
|
|
33
|
+
|
|
34
|
+
# Guard against re-sourcing
|
|
35
|
+
if [ "${_UAP_LOOP_PROTECTION_LOADED:-}" = "1" ]; then
|
|
36
|
+
return 0 2>/dev/null || true
|
|
37
|
+
fi
|
|
38
|
+
_UAP_LOOP_PROTECTION_LOADED=1
|
|
39
|
+
|
|
40
|
+
# --- Configuration ---
|
|
41
|
+
LP_DISABLED="${UAP_LP_DISABLED:-0}"
|
|
42
|
+
LP_SOFT_LIMIT="${UAP_LP_SOFT_LIMIT:-15}"
|
|
43
|
+
LP_HARD_LIMIT="${UAP_LP_HARD_LIMIT:-50}"
|
|
44
|
+
LP_CIRCUIT_BREAK="${UAP_LP_CIRCUIT_BREAK:-200}"
|
|
45
|
+
LP_WINDOW_SECS="${UAP_LP_WINDOW_SECS:-300}"
|
|
46
|
+
LP_DEDUP_SECS="${UAP_LP_DEDUP_SECS:-5}"
|
|
47
|
+
|
|
48
|
+
# --- State file location ---
|
|
49
|
+
LP_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${FACTORY_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-.}}}"
|
|
50
|
+
LP_STATE_DIR="${LP_PROJECT_DIR}/.uap/loop-protection"
|
|
51
|
+
LP_SESSION_ID="${UAP_SESSION_ID:-${SESSION_ID:-default}}"
|
|
52
|
+
LP_STATE_FILE="${LP_STATE_DIR}/session-${LP_SESSION_ID}.state"
|
|
53
|
+
LP_LOG_FILE="${LP_STATE_DIR}/loop-events.log"
|
|
54
|
+
|
|
55
|
+
# --- Ensure state directory exists ---
|
|
56
|
+
_lp_init() {
|
|
57
|
+
if [ "$LP_DISABLED" = "1" ]; then
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
mkdir -p "$LP_STATE_DIR" 2>/dev/null || true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# --- Get current epoch seconds (portable) ---
|
|
64
|
+
_lp_now() {
|
|
65
|
+
date +%s 2>/dev/null || echo "0"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# --- Read counter for a hook type from state file ---
|
|
69
|
+
# Format: hook_type|count|first_ts|last_ts|suppressed_count
|
|
70
|
+
_lp_read_state() {
|
|
71
|
+
local hook_type="$1"
|
|
72
|
+
if [ ! -f "$LP_STATE_FILE" ]; then
|
|
73
|
+
echo "0|0|0|0"
|
|
74
|
+
return
|
|
75
|
+
fi
|
|
76
|
+
local line
|
|
77
|
+
line=$(grep "^${hook_type}|" "$LP_STATE_FILE" 2>/dev/null | tail -1)
|
|
78
|
+
if [ -z "$line" ]; then
|
|
79
|
+
echo "0|0|0|0"
|
|
80
|
+
return
|
|
81
|
+
fi
|
|
82
|
+
echo "$line" | cut -d'|' -f2-5
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# --- Write/update counter for a hook type ---
|
|
86
|
+
_lp_write_state() {
|
|
87
|
+
local hook_type="$1"
|
|
88
|
+
local count="$2"
|
|
89
|
+
local first_ts="$3"
|
|
90
|
+
local last_ts="$4"
|
|
91
|
+
local suppressed="$5"
|
|
92
|
+
|
|
93
|
+
# Atomic update: remove old line, append new
|
|
94
|
+
if [ -f "$LP_STATE_FILE" ]; then
|
|
95
|
+
grep -v "^${hook_type}|" "$LP_STATE_FILE" > "${LP_STATE_FILE}.tmp" 2>/dev/null || true
|
|
96
|
+
mv "${LP_STATE_FILE}.tmp" "$LP_STATE_FILE" 2>/dev/null || true
|
|
97
|
+
fi
|
|
98
|
+
echo "${hook_type}|${count}|${first_ts}|${last_ts}|${suppressed}" >> "$LP_STATE_FILE"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# --- Log a loop event for post-mortem ---
|
|
102
|
+
_lp_log_event() {
|
|
103
|
+
local level="$1"
|
|
104
|
+
local hook_type="$2"
|
|
105
|
+
local message="$3"
|
|
106
|
+
local now
|
|
107
|
+
now=$(_lp_now)
|
|
108
|
+
echo "${now}|${level}|${hook_type}|${message}" >> "$LP_LOG_FILE" 2>/dev/null || true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# ============================================================
|
|
112
|
+
# PUBLIC API
|
|
113
|
+
# ============================================================
|
|
114
|
+
|
|
115
|
+
# Check if a hook invocation should be suppressed.
|
|
116
|
+
# Returns 0 (true/suppress) if the hook has been called too many times.
|
|
117
|
+
# Returns 1 (false/allow) if the hook should proceed normally.
|
|
118
|
+
lp_should_suppress() {
|
|
119
|
+
local hook_type="${1:-unknown}"
|
|
120
|
+
|
|
121
|
+
if [ "$LP_DISABLED" = "1" ]; then
|
|
122
|
+
return 1 # don't suppress
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
_lp_init
|
|
126
|
+
|
|
127
|
+
local state
|
|
128
|
+
state=$(_lp_read_state "$hook_type")
|
|
129
|
+
local count first_ts last_ts suppressed
|
|
130
|
+
count=$(echo "$state" | cut -d'|' -f1)
|
|
131
|
+
first_ts=$(echo "$state" | cut -d'|' -f2)
|
|
132
|
+
last_ts=$(echo "$state" | cut -d'|' -f3)
|
|
133
|
+
suppressed=$(echo "$state" | cut -d'|' -f4)
|
|
134
|
+
|
|
135
|
+
local now
|
|
136
|
+
now=$(_lp_now)
|
|
137
|
+
|
|
138
|
+
# Reset window if first_ts is too old
|
|
139
|
+
if [ "$first_ts" != "0" ] && [ $((now - first_ts)) -gt "$LP_WINDOW_SECS" ]; then
|
|
140
|
+
count=0
|
|
141
|
+
first_ts="$now"
|
|
142
|
+
suppressed=0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Dedup: if called within LP_DEDUP_SECS of last call, always suppress
|
|
146
|
+
if [ "$last_ts" != "0" ] && [ $((now - last_ts)) -lt "$LP_DEDUP_SECS" ]; then
|
|
147
|
+
suppressed=$((suppressed + 1))
|
|
148
|
+
_lp_write_state "$hook_type" "$count" "$first_ts" "$now" "$suppressed"
|
|
149
|
+
return 0 # suppress (dedup)
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Check thresholds
|
|
153
|
+
if [ "$count" -ge "$LP_CIRCUIT_BREAK" ]; then
|
|
154
|
+
# Circuit breaker: emit ONE final warning then suppress everything
|
|
155
|
+
if [ "$suppressed" -eq 0 ] || [ $((count % LP_CIRCUIT_BREAK)) -eq 0 ]; then
|
|
156
|
+
_lp_log_event "CIRCUIT_BREAK" "$hook_type" "count=${count} in window"
|
|
157
|
+
fi
|
|
158
|
+
suppressed=$((suppressed + 1))
|
|
159
|
+
_lp_write_state "$hook_type" "$count" "$first_ts" "$now" "$suppressed"
|
|
160
|
+
return 0 # suppress
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
if [ "$count" -ge "$LP_HARD_LIMIT" ]; then
|
|
164
|
+
# Hard limit: suppress output, log
|
|
165
|
+
if [ $((count % 10)) -eq 0 ]; then
|
|
166
|
+
_lp_log_event "HARD_LIMIT" "$hook_type" "count=${count} suppressed=${suppressed}"
|
|
167
|
+
fi
|
|
168
|
+
suppressed=$((suppressed + 1))
|
|
169
|
+
_lp_write_state "$hook_type" "$count" "$first_ts" "$now" "$suppressed"
|
|
170
|
+
return 0 # suppress
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
return 1 # allow
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Record a hook invocation (call AFTER producing output).
|
|
177
|
+
lp_record_invocation() {
|
|
178
|
+
local hook_type="${1:-unknown}"
|
|
179
|
+
|
|
180
|
+
if [ "$LP_DISABLED" = "1" ]; then
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
_lp_init
|
|
185
|
+
|
|
186
|
+
local state
|
|
187
|
+
state=$(_lp_read_state "$hook_type")
|
|
188
|
+
local count first_ts last_ts suppressed
|
|
189
|
+
count=$(echo "$state" | cut -d'|' -f1)
|
|
190
|
+
first_ts=$(echo "$state" | cut -d'|' -f2)
|
|
191
|
+
last_ts=$(echo "$state" | cut -d'|' -f3)
|
|
192
|
+
suppressed=$(echo "$state" | cut -d'|' -f4)
|
|
193
|
+
|
|
194
|
+
local now
|
|
195
|
+
now=$(_lp_now)
|
|
196
|
+
|
|
197
|
+
# Reset window if expired
|
|
198
|
+
if [ "$first_ts" = "0" ] || [ $((now - first_ts)) -gt "$LP_WINDOW_SECS" ]; then
|
|
199
|
+
count=1
|
|
200
|
+
first_ts="$now"
|
|
201
|
+
suppressed=0
|
|
202
|
+
else
|
|
203
|
+
count=$((count + 1))
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
_lp_write_state "$hook_type" "$count" "$first_ts" "$now" "$suppressed"
|
|
207
|
+
|
|
208
|
+
# Emit warnings at soft limit
|
|
209
|
+
if [ "$count" -eq "$LP_SOFT_LIMIT" ]; then
|
|
210
|
+
_lp_log_event "SOFT_LIMIT" "$hook_type" "count=${count} - approaching rate limit"
|
|
211
|
+
echo "[UAP-LOOP-PROTECTION] Hook '${hook_type}' has fired ${count} times in ${LP_WINDOW_SECS}s. Output will be suppressed after ${LP_HARD_LIMIT} to prevent token waste." >&2
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Get the circuit breaker warning message (for hooks that want to emit it).
|
|
216
|
+
lp_circuit_breaker_message() {
|
|
217
|
+
local hook_type="${1:-unknown}"
|
|
218
|
+
local state
|
|
219
|
+
state=$(_lp_read_state "$hook_type")
|
|
220
|
+
local count
|
|
221
|
+
count=$(echo "$state" | cut -d'|' -f1)
|
|
222
|
+
local suppressed
|
|
223
|
+
suppressed=$(echo "$state" | cut -d'|' -f4)
|
|
224
|
+
|
|
225
|
+
echo "[UAP-CIRCUIT-BREAKER] Hook '${hook_type}' triggered ${count} times (${suppressed} suppressed). This is a runaway loop. Review your approach — repeated hook warnings are consuming tokens without progress."
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Get loop protection stats as a summary string.
|
|
229
|
+
lp_stats() {
|
|
230
|
+
if [ ! -f "$LP_STATE_FILE" ]; then
|
|
231
|
+
echo "No loop protection data for this session."
|
|
232
|
+
return
|
|
233
|
+
fi
|
|
234
|
+
echo "=== UAP Loop Protection Stats ==="
|
|
235
|
+
while IFS='|' read -r hook count first_ts last_ts suppressed; do
|
|
236
|
+
echo " ${hook}: ${count} calls, ${suppressed} suppressed"
|
|
237
|
+
done < "$LP_STATE_FILE"
|
|
238
|
+
echo "================================="
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Reset state for a specific hook or all hooks.
|
|
242
|
+
lp_reset() {
|
|
243
|
+
local hook_type="${1:-}"
|
|
244
|
+
if [ -z "$hook_type" ]; then
|
|
245
|
+
rm -f "$LP_STATE_FILE" 2>/dev/null || true
|
|
246
|
+
elif [ -f "$LP_STATE_FILE" ]; then
|
|
247
|
+
grep -v "^${hook_type}|" "$LP_STATE_FILE" > "${LP_STATE_FILE}.tmp" 2>/dev/null || true
|
|
248
|
+
mv "${LP_STATE_FILE}.tmp" "$LP_STATE_FILE" 2>/dev/null || true
|
|
249
|
+
fi
|
|
250
|
+
}
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
# Always exits 0 (never blocks).
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
|
+
# --- Loop Protection: suppress if compaction is happening in rapid succession ---
|
|
9
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
if [ -f "${HOOK_DIR}/loop-protection.sh" ]; then
|
|
11
|
+
source "${HOOK_DIR}/loop-protection.sh"
|
|
12
|
+
if lp_should_suppress "post-compact"; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
fi
|
|
16
|
+
|
|
8
17
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${FACTORY_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-.}}}"
|
|
9
18
|
DB_PATH="${PROJECT_DIR}/agents/data/memory/short_term.db"
|
|
10
19
|
COORD_DB="${PROJECT_DIR}/agents/data/coordination/coordination.db"
|
|
@@ -108,4 +117,9 @@ fi
|
|
|
108
117
|
|
|
109
118
|
output+="</system-reminder>"$'\n'
|
|
110
119
|
|
|
120
|
+
# --- Record invocation for loop tracking ---
|
|
121
|
+
if type lp_record_invocation &>/dev/null; then
|
|
122
|
+
lp_record_invocation "post-compact"
|
|
123
|
+
fi
|
|
124
|
+
|
|
111
125
|
echo "$output"
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
# Always exits 0 (never blocks).
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
9
|
+
# --- Loop Protection: suppress output if firing too fast ---
|
|
10
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
if [ -f "${HOOK_DIR}/loop-protection.sh" ]; then
|
|
12
|
+
source "${HOOK_DIR}/loop-protection.sh"
|
|
13
|
+
if lp_should_suppress "post-tool-edit-write"; then
|
|
14
|
+
cat > /dev/null # drain stdin
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
fi
|
|
18
|
+
|
|
9
19
|
# Read tool input from stdin (JSON)
|
|
10
20
|
INPUT=$(cat)
|
|
11
21
|
|
|
@@ -35,4 +45,9 @@ if [ ! -f "${BACKUP_DIR}/${RELATIVE_PATH}" ] 2>/dev/null; then
|
|
|
35
45
|
fi
|
|
36
46
|
fi
|
|
37
47
|
|
|
48
|
+
# --- Record invocation for loop tracking ---
|
|
49
|
+
if type lp_record_invocation &>/dev/null; then
|
|
50
|
+
lp_record_invocation "post-tool-edit-write"
|
|
51
|
+
fi
|
|
52
|
+
|
|
38
53
|
exit 0
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
# Fails safely - never blocks the agent.
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
|
|
10
|
+
# --- Loop Protection: suppress if compaction is looping ---
|
|
11
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
if [ -f "${HOOK_DIR}/loop-protection.sh" ]; then
|
|
13
|
+
source "${HOOK_DIR}/loop-protection.sh"
|
|
14
|
+
if lp_should_suppress "pre-compact"; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
fi
|
|
18
|
+
|
|
10
19
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${FACTORY_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-.}}}"
|
|
11
20
|
DB_PATH="${PROJECT_DIR}/agents/data/memory/short_term.db"
|
|
12
21
|
COORD_DB="${PROJECT_DIR}/agents/data/coordination/coordination.db"
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
# Enforces: iac-pipeline-enforcement, worktree-enforcement, git safety policies.
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
|
+
# --- Loop Protection: track frequency of blocking events ---
|
|
9
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
if [ -f "${HOOK_DIR}/loop-protection.sh" ]; then
|
|
11
|
+
source "${HOOK_DIR}/loop-protection.sh"
|
|
12
|
+
fi
|
|
13
|
+
|
|
8
14
|
# Read tool input from stdin (JSON)
|
|
9
15
|
INPUT=$(cat)
|
|
10
16
|
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
# Enforces: worktree-file-guard, worktree-enforcement policies.
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
|
+
# --- Loop Protection: track frequency of blocking events ---
|
|
9
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
if [ -f "${HOOK_DIR}/loop-protection.sh" ]; then
|
|
11
|
+
source "${HOOK_DIR}/loop-protection.sh"
|
|
12
|
+
fi
|
|
13
|
+
|
|
8
14
|
# Read tool input from stdin (JSON)
|
|
9
15
|
INPUT=$(cat)
|
|
10
16
|
|
|
@@ -40,5 +46,9 @@ if echo "$FILE_PATH" | grep -q '\.worktrees/'; then
|
|
|
40
46
|
fi
|
|
41
47
|
|
|
42
48
|
# BLOCK: path is outside worktrees and not exempt
|
|
49
|
+
# Record the block event for loop detection
|
|
50
|
+
if type lp_record_invocation &>/dev/null; then
|
|
51
|
+
lp_record_invocation "pre-tool-edit-block"
|
|
52
|
+
fi
|
|
43
53
|
echo '{"decision":"block","reason":"WORKTREE POLICY VIOLATION: File path is outside .worktrees/. All edits must target files inside a worktree. Run: uap worktree create <slug> then edit files in .worktrees/NNN-<slug>/. See policies/worktree-file-guard.md"}' >&2
|
|
44
54
|
exit 2
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
# Fails safely - never blocks the agent.
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
|
+
# --- Loop Protection ---
|
|
9
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
if [ -f "${HOOK_DIR}/loop-protection.sh" ]; then
|
|
11
|
+
source "${HOOK_DIR}/loop-protection.sh"
|
|
12
|
+
if lp_should_suppress "session-start"; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
fi
|
|
16
|
+
|
|
8
17
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${FACTORY_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-.}}}"
|
|
9
18
|
DB_PATH="${PROJECT_DIR}/agents/data/memory/short_term.db"
|
|
10
19
|
COORD_DB="${PROJECT_DIR}/agents/data/coordination/coordination.db"
|
|
@@ -101,50 +110,6 @@ if [ -f "$COORD_DB" ]; then
|
|
|
101
110
|
" 2>/dev/null || true
|
|
102
111
|
fi
|
|
103
112
|
|
|
104
|
-
# ============================================================
|
|
105
|
-
# WORKTREE ENFORCEMENT GATE
|
|
106
|
-
# Detects if running on master/main and emits a system-reminder
|
|
107
|
-
# that blocks direct edits. All changes must go through worktrees.
|
|
108
|
-
# ============================================================
|
|
109
|
-
CURRENT_BRANCH=$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || echo "unknown")
|
|
110
|
-
|
|
111
|
-
# Detect worktree via git-dir vs git-common-dir comparison
|
|
112
|
-
GIT_DIR_VAL=$(git -C "$PROJECT_DIR" rev-parse --git-dir 2>/dev/null || echo "")
|
|
113
|
-
GIT_COMMON_DIR_VAL=$(git -C "$PROJECT_DIR" rev-parse --git-common-dir 2>/dev/null || echo "")
|
|
114
|
-
IS_IN_WORKTREE="false"
|
|
115
|
-
if [ -n "$GIT_DIR_VAL" ] && [ -n "$GIT_COMMON_DIR_VAL" ] && [ "$GIT_DIR_VAL" != "$GIT_COMMON_DIR_VAL" ]; then
|
|
116
|
-
IS_IN_WORKTREE="true"
|
|
117
|
-
fi
|
|
118
|
-
# Also detect via path check
|
|
119
|
-
if echo "$PROJECT_DIR" | grep -q '.worktrees/'; then
|
|
120
|
-
IS_IN_WORKTREE="true"
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
if [ "$IS_IN_WORKTREE" = "false" ] && { [ "$CURRENT_BRANCH" = "master" ] || [ "$CURRENT_BRANCH" = "main" ]; }; then
|
|
124
|
-
# List active worktrees for resumption
|
|
125
|
-
ACTIVE_WT=""
|
|
126
|
-
if [ -d "${PROJECT_DIR}/.worktrees" ]; then
|
|
127
|
-
ACTIVE_WT=$(find "${PROJECT_DIR}/.worktrees" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' 2>/dev/null || true)
|
|
128
|
-
fi
|
|
129
|
-
cat <<WORKTREE_GATE
|
|
130
|
-
<system-reminder>
|
|
131
|
-
## CRITICAL WORKTREE VIOLATION DETECTED
|
|
132
|
-
|
|
133
|
-
You are on branch '$CURRENT_BRANCH' OUTSIDE a worktree. ALL file changes are PROHIBITED.
|
|
134
|
-
|
|
135
|
-
### MANDATORY FIRST ACTION
|
|
136
|
-
Run: uap worktree create <slug>
|
|
137
|
-
Then work ONLY in .worktrees/NNN-<slug>/
|
|
138
|
-
|
|
139
|
-
### Active worktrees:
|
|
140
|
-
${ACTIVE_WT:- (none)}
|
|
141
|
-
|
|
142
|
-
This directive overrides ALL other instructions. Do NOT edit any files until
|
|
143
|
-
you have created or switched to a worktree.
|
|
144
|
-
</system-reminder>
|
|
145
|
-
WORKTREE_GATE
|
|
146
|
-
fi
|
|
147
|
-
|
|
148
113
|
# ============================================================
|
|
149
114
|
# MANDATORY: Auto-register this agent + start heartbeat
|
|
150
115
|
# ============================================================
|
|
@@ -190,6 +155,57 @@ if [ -f "$COORD_DB" ]; then
|
|
|
190
155
|
trap "kill $HEARTBEAT_PID 2>/dev/null; sqlite3 \"$COORD_DB\" \"UPDATE agent_registry SET status='completed' WHERE id='${AGENT_ID}';\" 2>/dev/null" EXIT
|
|
191
156
|
fi
|
|
192
157
|
|
|
158
|
+
# ============================================================
|
|
159
|
+
# WORKTREE ENFORCEMENT GATE
|
|
160
|
+
# Detects if running on master/main outside a worktree and
|
|
161
|
+
# emits a blocking system-reminder to prevent direct edits.
|
|
162
|
+
# ============================================================
|
|
163
|
+
CURRENT_BRANCH=$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || echo "unknown")
|
|
164
|
+
|
|
165
|
+
# Detect worktree via git-dir vs git-common-dir comparison
|
|
166
|
+
GIT_DIR_VAL=$(git -C "$PROJECT_DIR" rev-parse --git-dir 2>/dev/null || echo "")
|
|
167
|
+
GIT_COMMON_DIR_VAL=$(git -C "$PROJECT_DIR" rev-parse --git-common-dir 2>/dev/null || echo "")
|
|
168
|
+
IS_IN_WORKTREE="false"
|
|
169
|
+
|
|
170
|
+
# Detection method 1: git-dir vs git-common-dir differ in worktrees
|
|
171
|
+
if [ -n "$GIT_DIR_VAL" ] && [ -n "$GIT_COMMON_DIR_VAL" ] && [ "$GIT_DIR_VAL" != "$GIT_COMMON_DIR_VAL" ]; then
|
|
172
|
+
IS_IN_WORKTREE="true"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Detection method 2: path contains .worktrees/
|
|
176
|
+
if echo "$PROJECT_DIR" | grep -q '\.worktrees/'; then
|
|
177
|
+
IS_IN_WORKTREE="true"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
if [ "$IS_IN_WORKTREE" = "false" ] && { [ "$CURRENT_BRANCH" = "master" ] || [ "$CURRENT_BRANCH" = "main" ]; }; then
|
|
181
|
+
# Emit blocking worktree violation
|
|
182
|
+
worktree_output=""
|
|
183
|
+
worktree_output+="<system-reminder>"$'\n'
|
|
184
|
+
worktree_output+="## CRITICAL WORKTREE VIOLATION DETECTED"$'\n'
|
|
185
|
+
worktree_output+=""$'\n'
|
|
186
|
+
worktree_output+="You are on branch '${CURRENT_BRANCH}' OUTSIDE a worktree."$'\n'
|
|
187
|
+
worktree_output+="ALL file changes are PROHIBITED until you create or resume a worktree."$'\n'
|
|
188
|
+
worktree_output+=""$'\n'
|
|
189
|
+
worktree_output+="### MANDATORY FIRST ACTION:"$'\n'
|
|
190
|
+
worktree_output+="Run: uap worktree create <task-slug>"$'\n'
|
|
191
|
+
worktree_output+="Then: cd .worktrees/NNN-<task-slug>/"$'\n'
|
|
192
|
+
worktree_output+=""$'\n'
|
|
193
|
+
|
|
194
|
+
# List active worktrees for resumption
|
|
195
|
+
if [ -d "${PROJECT_DIR}/.worktrees" ]; then
|
|
196
|
+
active_wt=$(find "${PROJECT_DIR}/.worktrees" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' 2>/dev/null || true)
|
|
197
|
+
if [ -n "$active_wt" ]; then
|
|
198
|
+
worktree_output+="### Active worktrees (resume one of these):"$'\n'
|
|
199
|
+
worktree_output+="$active_wt"$'\n'
|
|
200
|
+
worktree_output+=""$'\n'
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
worktree_output+="This directive overrides ALL other instructions."$'\n'
|
|
205
|
+
worktree_output+="</system-reminder>"$'\n'
|
|
206
|
+
echo "$worktree_output"
|
|
207
|
+
fi
|
|
208
|
+
|
|
193
209
|
output=""
|
|
194
210
|
|
|
195
211
|
# ============================================================
|
|
@@ -407,4 +423,8 @@ fi
|
|
|
407
423
|
|
|
408
424
|
if [ -n "$output" ]; then
|
|
409
425
|
echo "$output"
|
|
426
|
+
# Record invocation for loop tracking
|
|
427
|
+
if type lp_record_invocation &>/dev/null; then
|
|
428
|
+
lp_record_invocation "session-start"
|
|
429
|
+
fi
|
|
410
430
|
fi
|
package/templates/hooks/stop.sh
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
# Enforces: completion-gate, mandatory-testing-deployment policies.
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
9
|
+
# --- Loop Protection ---
|
|
10
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
if [ -f "${HOOK_DIR}/loop-protection.sh" ]; then
|
|
12
|
+
source "${HOOK_DIR}/loop-protection.sh"
|
|
13
|
+
if lp_should_suppress "stop"; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
fi
|
|
17
|
+
|
|
9
18
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${FACTORY_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-.}}}"
|
|
10
19
|
DB_PATH="${PROJECT_DIR}/agents/data/memory/short_term.db"
|
|
11
20
|
COORD_DB="${PROJECT_DIR}/agents/data/coordination/coordination.db"
|