@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.
Files changed (43) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/benchmarks/speculative-autotune.d.ts +46 -0
  3. package/dist/benchmarks/speculative-autotune.d.ts.map +1 -0
  4. package/dist/benchmarks/speculative-autotune.js +145 -0
  5. package/dist/benchmarks/speculative-autotune.js.map +1 -0
  6. package/dist/benchmarks/token-throughput.d.ts +46 -46
  7. package/dist/bin/cli.js +2 -0
  8. package/dist/bin/cli.js.map +1 -1
  9. package/dist/bin/llama-server-optimize.js +176 -0
  10. package/dist/bin/llama-server-optimize.js.map +1 -1
  11. package/dist/bin/policy.js +0 -0
  12. package/dist/cli/hooks.js +1 -0
  13. package/dist/cli/hooks.js.map +1 -1
  14. package/dist/cli/init.d.ts +1 -0
  15. package/dist/cli/init.d.ts.map +1 -1
  16. package/dist/cli/init.js +18 -0
  17. package/dist/cli/init.js.map +1 -1
  18. package/dist/cli/setup.d.ts +1 -0
  19. package/dist/cli/setup.d.ts.map +1 -1
  20. package/dist/cli/setup.js +1 -0
  21. package/dist/cli/setup.js.map +1 -1
  22. package/dist/cli/systemd-services.d.ts +12 -0
  23. package/dist/cli/systemd-services.d.ts.map +1 -0
  24. package/dist/cli/systemd-services.js +179 -0
  25. package/dist/cli/systemd-services.js.map +1 -0
  26. package/dist/models/types.d.ts +12 -12
  27. package/dist/policies/schemas/policy.d.ts +12 -12
  28. package/dist/types/config.d.ts +24 -24
  29. package/docs/deployment/QWEN35_LLAMA_CPP.md +49 -0
  30. package/docs/deployment/UAP_LLAMA_ANTHROPIC_PROXY_BOOTSTRAP.md +279 -0
  31. package/package.json +1 -1
  32. package/templates/hooks/loop-protection.sh +250 -0
  33. package/templates/hooks/post-compact.sh +14 -0
  34. package/templates/hooks/post-tool-use-edit-write.sh +15 -0
  35. package/templates/hooks/pre-compact.sh +9 -0
  36. package/templates/hooks/pre-tool-use-bash.sh +6 -0
  37. package/templates/hooks/pre-tool-use-edit-write.sh +10 -0
  38. package/templates/hooks/session-start.sh +64 -44
  39. package/templates/hooks/stop.sh +9 -0
  40. package/tools/agents/scripts/anthropic_proxy.py +716 -166
  41. package/tools/agents/tests/test_anthropic_proxy_streaming.py +51 -0
  42. package/tools/agents/scripts/__pycache__/anthropic_proxy.cpython-313.pyc +0 -0
  43. 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
@@ -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"