create-merlin-brain 2.3.3 → 2.4.0
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/files/loop/lib/agents.sh +603 -0
- package/files/loop/lib/boot.sh +453 -0
- package/files/loop/lib/discuss.sh +224 -0
- package/files/loop/lib/modes.sh +294 -0
- package/files/loop/lib/session-end.sh +248 -0
- package/files/loop/lib/sights.sh +725 -0
- package/files/loop/lib/timeout.sh +207 -0
- package/files/loop/lib/tui.sh +388 -0
- package/files/loop/merlin-loop.sh +311 -16
- package/files/loop/prompts/PROMPT_DISCUSS.md +102 -0
- package/files/loop/prompts/PROMPT_build.md +152 -2
- package/package.json +1 -1
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Loop - Sights Integration
|
|
4
|
+
# Fetches recent changes, trajectory, and records commits
|
|
5
|
+
# Part of the Sights Memory Layer for Merlin Pro
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
# Configuration
|
|
10
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
11
|
+
|
|
12
|
+
SIGHTS_API_URL="${MERLIN_API_URL:-https://auth.merlin.build}"
|
|
13
|
+
RECENT_CHANGES_LIMIT="${MERLIN_RECENT_LIMIT:-5}"
|
|
14
|
+
|
|
15
|
+
# Cache for current session
|
|
16
|
+
CACHED_RECENT_CHANGES=""
|
|
17
|
+
CACHED_TRAJECTORY=""
|
|
18
|
+
|
|
19
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
# API Key Management
|
|
21
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
get_api_key() {
|
|
24
|
+
# Check environment variable first
|
|
25
|
+
if [ -n "$MERLIN_API_KEY" ]; then
|
|
26
|
+
echo "$MERLIN_API_KEY"
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Check ~/.merlin/config.json
|
|
31
|
+
local config_file="$HOME/.merlin/config.json"
|
|
32
|
+
if [ -f "$config_file" ] && command -v jq &> /dev/null; then
|
|
33
|
+
local key
|
|
34
|
+
key=$(jq -r '.apiKey // empty' "$config_file")
|
|
35
|
+
if [ -n "$key" ]; then
|
|
36
|
+
echo "$key"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Not found
|
|
42
|
+
echo ""
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
46
|
+
# Repo ID Management
|
|
47
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
# Get current repo ID (stored in state or detected)
|
|
50
|
+
get_current_repo_id() {
|
|
51
|
+
# Check state file first
|
|
52
|
+
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
|
53
|
+
local repo_id
|
|
54
|
+
repo_id=$(jq -r '.sights_repo_id // empty' "$STATE_FILE")
|
|
55
|
+
if [ -n "$repo_id" ]; then
|
|
56
|
+
echo "$repo_id"
|
|
57
|
+
return
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Check ~/.merlin/config.json for selected repo
|
|
62
|
+
local config_file="$HOME/.merlin/config.json"
|
|
63
|
+
if [ -f "$config_file" ] && command -v jq &> /dev/null; then
|
|
64
|
+
local repo_id
|
|
65
|
+
repo_id=$(jq -r '.selectedRepo.id // empty' "$config_file")
|
|
66
|
+
if [ -n "$repo_id" ]; then
|
|
67
|
+
echo "$repo_id"
|
|
68
|
+
return
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Not found
|
|
73
|
+
echo ""
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Set repo ID in state
|
|
77
|
+
set_current_repo_id() {
|
|
78
|
+
local repo_id="$1"
|
|
79
|
+
|
|
80
|
+
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
|
81
|
+
local tmp
|
|
82
|
+
tmp=$(mktemp)
|
|
83
|
+
jq --arg id "$repo_id" '.sights_repo_id = $id' "$STATE_FILE" > "$tmp"
|
|
84
|
+
mv "$tmp" "$STATE_FILE"
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
89
|
+
# Recent Changes
|
|
90
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
91
|
+
|
|
92
|
+
# Fetch recent changes from Sights
|
|
93
|
+
fetch_recent_changes() {
|
|
94
|
+
local api_key
|
|
95
|
+
api_key=$(get_api_key)
|
|
96
|
+
|
|
97
|
+
if [ -z "$api_key" ]; then
|
|
98
|
+
echo "# Recent Changes: Not available (no API key)"
|
|
99
|
+
return 1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
local repo_id
|
|
103
|
+
repo_id=$(get_current_repo_id)
|
|
104
|
+
|
|
105
|
+
if [ -z "$repo_id" ]; then
|
|
106
|
+
echo "# Recent Changes: Not available (no repo selected)"
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
local response
|
|
111
|
+
response=$(curl -sf "${SIGHTS_API_URL}/api/agent/${repo_id}/activity/recent?limit=${RECENT_CHANGES_LIMIT}" \
|
|
112
|
+
-H "Authorization: Bearer $api_key" 2>/dev/null)
|
|
113
|
+
|
|
114
|
+
if [ -z "$response" ]; then
|
|
115
|
+
echo "# Recent Changes: Could not fetch"
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
CACHED_RECENT_CHANGES="$response"
|
|
120
|
+
|
|
121
|
+
# Format for prompt injection
|
|
122
|
+
format_recent_changes "$response"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Format recent changes for prompt injection
|
|
126
|
+
format_recent_changes() {
|
|
127
|
+
local json="$1"
|
|
128
|
+
|
|
129
|
+
if ! command -v jq &> /dev/null; then
|
|
130
|
+
echo "# Recent Changes: Install jq for formatted output"
|
|
131
|
+
return
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
local trajectory
|
|
135
|
+
trajectory=$(echo "$json" | jq -r '.trajectory // "Not detected"')
|
|
136
|
+
|
|
137
|
+
local active_areas
|
|
138
|
+
active_areas=$(echo "$json" | jq -r '.activeAreas // [] | join(", ")')
|
|
139
|
+
|
|
140
|
+
echo "## Recent Changes (from Sights)"
|
|
141
|
+
echo ""
|
|
142
|
+
echo "**Trajectory:** $trajectory"
|
|
143
|
+
echo "**Active Areas:** ${active_areas:-None}"
|
|
144
|
+
echo ""
|
|
145
|
+
echo "### Recent Commits"
|
|
146
|
+
|
|
147
|
+
echo "$json" | jq -r '.commits[]? | "- **\(.hash[0:7])**: \(.message) (\(.filesChanged | length) files)"'
|
|
148
|
+
|
|
149
|
+
echo ""
|
|
150
|
+
echo "### Hot Files (frequently modified)"
|
|
151
|
+
echo "$json" | jq -r '.hotFiles[]? | "- **\(.path)**: \(.changeCount) changes"'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
155
|
+
# Trajectory
|
|
156
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
157
|
+
|
|
158
|
+
# Get trajectory only
|
|
159
|
+
fetch_trajectory() {
|
|
160
|
+
local api_key
|
|
161
|
+
api_key=$(get_api_key)
|
|
162
|
+
|
|
163
|
+
if [ -z "$api_key" ]; then
|
|
164
|
+
return 1
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
local repo_id
|
|
168
|
+
repo_id=$(get_current_repo_id)
|
|
169
|
+
|
|
170
|
+
if [ -z "$repo_id" ]; then
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
local response
|
|
175
|
+
response=$(curl -sf "${SIGHTS_API_URL}/api/agent/${repo_id}/activity/trajectory" \
|
|
176
|
+
-H "Authorization: Bearer $api_key" 2>/dev/null)
|
|
177
|
+
|
|
178
|
+
if [ -z "$response" ]; then
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
CACHED_TRAJECTORY=$(echo "$response" | jq -r '.trajectory // "Not detected"')
|
|
183
|
+
echo "$CACHED_TRAJECTORY"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Get cached trajectory (doesn't fetch)
|
|
187
|
+
get_cached_trajectory() {
|
|
188
|
+
echo "$CACHED_TRAJECTORY"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
192
|
+
# Record Commits
|
|
193
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
194
|
+
|
|
195
|
+
# Record a commit to Sights
|
|
196
|
+
record_commit_to_sights() {
|
|
197
|
+
local hash="$1"
|
|
198
|
+
local message="$2"
|
|
199
|
+
local files_changed="$3" # JSON array string
|
|
200
|
+
local author="${4:-claude}"
|
|
201
|
+
local author_type="${5:-loop-agent}"
|
|
202
|
+
local task_id="$6"
|
|
203
|
+
|
|
204
|
+
local api_key
|
|
205
|
+
api_key=$(get_api_key)
|
|
206
|
+
|
|
207
|
+
if [ -z "$api_key" ]; then
|
|
208
|
+
return 1
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
local repo_id
|
|
212
|
+
repo_id=$(get_current_repo_id)
|
|
213
|
+
|
|
214
|
+
if [ -z "$repo_id" ]; then
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
local body
|
|
219
|
+
body=$(jq -n \
|
|
220
|
+
--arg hash "$hash" \
|
|
221
|
+
--arg message "$message" \
|
|
222
|
+
--argjson filesChanged "$files_changed" \
|
|
223
|
+
--arg author "$author" \
|
|
224
|
+
--arg authorType "$author_type" \
|
|
225
|
+
--arg taskId "$task_id" \
|
|
226
|
+
'{hash: $hash, message: $message, filesChanged: $filesChanged, author: $author, authorType: $authorType, taskId: $taskId}')
|
|
227
|
+
|
|
228
|
+
curl -sf -X POST "${SIGHTS_API_URL}/api/agent/${repo_id}/activity/commit" \
|
|
229
|
+
-H "Authorization: Bearer $api_key" \
|
|
230
|
+
-H "Content-Type: application/json" \
|
|
231
|
+
-d "$body" > /dev/null 2>&1
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Record the last git commit to Sights
|
|
235
|
+
record_last_commit() {
|
|
236
|
+
local task_id="${1:-}"
|
|
237
|
+
local author="${2:-claude}"
|
|
238
|
+
local author_type="${3:-loop-agent}"
|
|
239
|
+
|
|
240
|
+
# Get last commit info
|
|
241
|
+
local last_commit
|
|
242
|
+
last_commit=$(git log -1 --pretty=format:"%H|%s" 2>/dev/null || echo "")
|
|
243
|
+
|
|
244
|
+
if [ -z "$last_commit" ]; then
|
|
245
|
+
return 1
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
local hash message
|
|
249
|
+
hash=$(echo "$last_commit" | cut -d'|' -f1)
|
|
250
|
+
message=$(echo "$last_commit" | cut -d'|' -f2-)
|
|
251
|
+
|
|
252
|
+
# Get files changed
|
|
253
|
+
local files_json
|
|
254
|
+
files_json=$(git diff-tree --no-commit-id --name-only -r "$hash" 2>/dev/null | jq -R -s 'split("\n") | map(select(. != ""))' || echo "[]")
|
|
255
|
+
|
|
256
|
+
# Record to Sights
|
|
257
|
+
record_commit_to_sights "$hash" "$message" "$files_json" "$author" "$author_type" "$task_id"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
261
|
+
# Sights Status Check
|
|
262
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
263
|
+
|
|
264
|
+
# Check if Sights is connected and ready
|
|
265
|
+
check_sights_connection() {
|
|
266
|
+
local api_key
|
|
267
|
+
api_key=$(get_api_key)
|
|
268
|
+
|
|
269
|
+
if [ -z "$api_key" ]; then
|
|
270
|
+
echo "not_configured"
|
|
271
|
+
return 1
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
local repo_id
|
|
275
|
+
repo_id=$(get_current_repo_id)
|
|
276
|
+
|
|
277
|
+
if [ -z "$repo_id" ]; then
|
|
278
|
+
echo "no_repo"
|
|
279
|
+
return 1
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Try to fetch trajectory as a health check
|
|
283
|
+
local response
|
|
284
|
+
response=$(curl -sf "${SIGHTS_API_URL}/api/agent/${repo_id}/activity/trajectory" \
|
|
285
|
+
-H "Authorization: Bearer $api_key" 2>/dev/null)
|
|
286
|
+
|
|
287
|
+
if [ -z "$response" ]; then
|
|
288
|
+
echo "unreachable"
|
|
289
|
+
return 1
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
echo "connected"
|
|
293
|
+
return 0
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Print Sights status for loop startup
|
|
297
|
+
print_sights_status() {
|
|
298
|
+
local status
|
|
299
|
+
status=$(check_sights_connection)
|
|
300
|
+
|
|
301
|
+
case "$status" in
|
|
302
|
+
"connected")
|
|
303
|
+
echo -e "${GREEN}✓ Sights: Connected${RESET}"
|
|
304
|
+
;;
|
|
305
|
+
"not_configured")
|
|
306
|
+
echo -e "${YELLOW}⚠ Sights: No API key configured${RESET}"
|
|
307
|
+
;;
|
|
308
|
+
"no_repo")
|
|
309
|
+
echo -e "${YELLOW}⚠ Sights: No repository selected${RESET}"
|
|
310
|
+
;;
|
|
311
|
+
"unreachable")
|
|
312
|
+
echo -e "${RED}✗ Sights: Could not connect${RESET}"
|
|
313
|
+
;;
|
|
314
|
+
*)
|
|
315
|
+
echo -e "${YELLOW}⚠ Sights: Unknown status${RESET}"
|
|
316
|
+
;;
|
|
317
|
+
esac
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
321
|
+
# Cloud Checkpoints (v3.0)
|
|
322
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
323
|
+
|
|
324
|
+
# Fetch latest checkpoint from cloud
|
|
325
|
+
fetch_checkpoint() {
|
|
326
|
+
local api_key
|
|
327
|
+
api_key=$(get_api_key)
|
|
328
|
+
|
|
329
|
+
if [ -z "$api_key" ]; then
|
|
330
|
+
return 1
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
local repo_id
|
|
334
|
+
repo_id=$(get_current_repo_id)
|
|
335
|
+
|
|
336
|
+
if [ -z "$repo_id" ]; then
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
local response
|
|
341
|
+
response=$(curl -sf "${SIGHTS_API_URL}/api/agent/${repo_id}/checkpoints/latest" \
|
|
342
|
+
-H "Authorization: Bearer $api_key" 2>/dev/null)
|
|
343
|
+
|
|
344
|
+
echo "$response"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Format checkpoint for display
|
|
348
|
+
format_checkpoint() {
|
|
349
|
+
local json="$1"
|
|
350
|
+
|
|
351
|
+
if ! command -v jq &> /dev/null; then
|
|
352
|
+
echo "Install jq for checkpoint display"
|
|
353
|
+
return
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
local checkpoint
|
|
357
|
+
checkpoint=$(echo "$json" | jq -r '.checkpoint // empty')
|
|
358
|
+
|
|
359
|
+
if [ -z "$checkpoint" ] || [ "$checkpoint" == "null" ]; then
|
|
360
|
+
echo "No checkpoint found"
|
|
361
|
+
return
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
echo "## Checkpoint"
|
|
365
|
+
echo ""
|
|
366
|
+
echo "**Created By:** $(echo "$checkpoint" | jq -r '.createdBy // "Unknown"') ($(echo "$checkpoint" | jq -r '.createdByType // "unknown"'))"
|
|
367
|
+
echo "**Created At:** $(echo "$checkpoint" | jq -r '.createdAt // "Unknown"')"
|
|
368
|
+
|
|
369
|
+
local current_phase
|
|
370
|
+
current_phase=$(echo "$checkpoint" | jq -r '.currentPhase // empty')
|
|
371
|
+
if [ -n "$current_phase" ]; then
|
|
372
|
+
echo "**Phase:** $current_phase"
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
local current_task
|
|
376
|
+
current_task=$(echo "$checkpoint" | jq -r '.currentTask // empty')
|
|
377
|
+
if [ -n "$current_task" ] && [ "$current_task" != "null" ]; then
|
|
378
|
+
echo "**Task:** $current_task"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
local summary
|
|
382
|
+
summary=$(echo "$checkpoint" | jq -r '.summary // empty')
|
|
383
|
+
if [ -n "$summary" ]; then
|
|
384
|
+
echo ""
|
|
385
|
+
echo "**Summary:** $summary"
|
|
386
|
+
fi
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Save checkpoint to cloud
|
|
390
|
+
save_checkpoint() {
|
|
391
|
+
local summary="$1"
|
|
392
|
+
local current_phase="${2:-}"
|
|
393
|
+
local current_task="${3:-}"
|
|
394
|
+
local working_files="${4:-[]}" # JSON array
|
|
395
|
+
local created_by="${5:-loop-agent}"
|
|
396
|
+
|
|
397
|
+
local api_key
|
|
398
|
+
api_key=$(get_api_key)
|
|
399
|
+
|
|
400
|
+
if [ -z "$api_key" ]; then
|
|
401
|
+
return 1
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
local repo_id
|
|
405
|
+
repo_id=$(get_current_repo_id)
|
|
406
|
+
|
|
407
|
+
if [ -z "$repo_id" ]; then
|
|
408
|
+
return 1
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
local body
|
|
412
|
+
body=$(jq -n \
|
|
413
|
+
--arg summary "$summary" \
|
|
414
|
+
--arg phase "$current_phase" \
|
|
415
|
+
--arg task "$current_task" \
|
|
416
|
+
--argjson files "$working_files" \
|
|
417
|
+
--arg by "$created_by" \
|
|
418
|
+
'{
|
|
419
|
+
createdBy: $by,
|
|
420
|
+
createdByType: "loop-agent",
|
|
421
|
+
currentPhase: (if $phase != "" then $phase else null end),
|
|
422
|
+
currentTask: (if $task != "" then ($task | tonumber) else null end),
|
|
423
|
+
summary: $summary,
|
|
424
|
+
workingFiles: $files
|
|
425
|
+
}')
|
|
426
|
+
|
|
427
|
+
curl -sf -X POST "${SIGHTS_API_URL}/api/agent/${repo_id}/checkpoints" \
|
|
428
|
+
-H "Authorization: Bearer $api_key" \
|
|
429
|
+
-H "Content-Type: application/json" \
|
|
430
|
+
-d "$body" > /dev/null 2>&1
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
434
|
+
# Decisions
|
|
435
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
436
|
+
|
|
437
|
+
# Record a decision
|
|
438
|
+
record_decision() {
|
|
439
|
+
local description="$1"
|
|
440
|
+
local reasoning="${2:-}"
|
|
441
|
+
local task_id="${3:-}"
|
|
442
|
+
local made_by="${4:-loop-agent}"
|
|
443
|
+
|
|
444
|
+
local api_key
|
|
445
|
+
api_key=$(get_api_key)
|
|
446
|
+
|
|
447
|
+
if [ -z "$api_key" ]; then
|
|
448
|
+
return 1
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
local repo_id
|
|
452
|
+
repo_id=$(get_current_repo_id)
|
|
453
|
+
|
|
454
|
+
if [ -z "$repo_id" ]; then
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
local body
|
|
459
|
+
body=$(jq -n \
|
|
460
|
+
--arg desc "$description" \
|
|
461
|
+
--arg reason "$reasoning" \
|
|
462
|
+
--arg task "$task_id" \
|
|
463
|
+
--arg by "$made_by" \
|
|
464
|
+
'{
|
|
465
|
+
description: $desc,
|
|
466
|
+
reasoning: (if $reason != "" then $reason else null end),
|
|
467
|
+
taskId: (if $task != "" then $task else null end),
|
|
468
|
+
madeBy: $by,
|
|
469
|
+
madeByType: "loop-agent"
|
|
470
|
+
}')
|
|
471
|
+
|
|
472
|
+
curl -sf -X POST "${SIGHTS_API_URL}/api/agent/${repo_id}/decisions" \
|
|
473
|
+
-H "Authorization: Bearer $api_key" \
|
|
474
|
+
-H "Content-Type: application/json" \
|
|
475
|
+
-d "$body" > /dev/null 2>&1
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
# Get recent decisions
|
|
479
|
+
get_decisions() {
|
|
480
|
+
local limit="${1:-5}"
|
|
481
|
+
|
|
482
|
+
local api_key
|
|
483
|
+
api_key=$(get_api_key)
|
|
484
|
+
|
|
485
|
+
if [ -z "$api_key" ]; then
|
|
486
|
+
return 1
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
local repo_id
|
|
490
|
+
repo_id=$(get_current_repo_id)
|
|
491
|
+
|
|
492
|
+
if [ -z "$repo_id" ]; then
|
|
493
|
+
return 1
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
curl -sf "${SIGHTS_API_URL}/api/agent/${repo_id}/decisions/recent?limit=${limit}" \
|
|
497
|
+
-H "Authorization: Bearer $api_key" 2>/dev/null
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
501
|
+
# Worker Coordination
|
|
502
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
503
|
+
|
|
504
|
+
# Worker session ID for this loop instance
|
|
505
|
+
WORKER_SESSION_ID="${LOOP_SESSION_ID:-loop-$$-$(date +%s)}"
|
|
506
|
+
HEARTBEAT_PID=""
|
|
507
|
+
|
|
508
|
+
# Register worker session
|
|
509
|
+
register_worker() {
|
|
510
|
+
local current_task="${1:-}"
|
|
511
|
+
|
|
512
|
+
local api_key
|
|
513
|
+
api_key=$(get_api_key)
|
|
514
|
+
|
|
515
|
+
if [ -z "$api_key" ]; then
|
|
516
|
+
return 1
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
local repo_id
|
|
520
|
+
repo_id=$(get_current_repo_id)
|
|
521
|
+
|
|
522
|
+
if [ -z "$repo_id" ]; then
|
|
523
|
+
return 1
|
|
524
|
+
fi
|
|
525
|
+
|
|
526
|
+
local body
|
|
527
|
+
body=$(jq -n \
|
|
528
|
+
--arg agent "merlin-loop" \
|
|
529
|
+
--arg session "$WORKER_SESSION_ID" \
|
|
530
|
+
--arg task "$current_task" \
|
|
531
|
+
'{
|
|
532
|
+
agentId: $agent,
|
|
533
|
+
sessionId: $session,
|
|
534
|
+
currentTask: (if $task != "" then $task else null end)
|
|
535
|
+
}')
|
|
536
|
+
|
|
537
|
+
curl -sf -X POST "${SIGHTS_API_URL}/api/agent/${repo_id}/workers/heartbeat" \
|
|
538
|
+
-H "Authorization: Bearer $api_key" \
|
|
539
|
+
-H "Content-Type: application/json" \
|
|
540
|
+
-d "$body" > /dev/null 2>&1
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# Send heartbeat
|
|
544
|
+
worker_heartbeat() {
|
|
545
|
+
local current_task="${1:-}"
|
|
546
|
+
register_worker "$current_task"
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# Start background heartbeat process
|
|
550
|
+
start_heartbeat() {
|
|
551
|
+
local interval="${1:-30}"
|
|
552
|
+
|
|
553
|
+
# Kill existing heartbeat if running
|
|
554
|
+
stop_heartbeat
|
|
555
|
+
|
|
556
|
+
# Start background process
|
|
557
|
+
(
|
|
558
|
+
while true; do
|
|
559
|
+
worker_heartbeat
|
|
560
|
+
sleep "$interval"
|
|
561
|
+
done
|
|
562
|
+
) &
|
|
563
|
+
|
|
564
|
+
HEARTBEAT_PID=$!
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Stop background heartbeat
|
|
568
|
+
stop_heartbeat() {
|
|
569
|
+
if [ -n "$HEARTBEAT_PID" ] && kill -0 "$HEARTBEAT_PID" 2>/dev/null; then
|
|
570
|
+
kill "$HEARTBEAT_PID" 2>/dev/null
|
|
571
|
+
HEARTBEAT_PID=""
|
|
572
|
+
fi
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# Get active workers
|
|
576
|
+
get_active_workers() {
|
|
577
|
+
local api_key
|
|
578
|
+
api_key=$(get_api_key)
|
|
579
|
+
|
|
580
|
+
if [ -z "$api_key" ]; then
|
|
581
|
+
return 1
|
|
582
|
+
fi
|
|
583
|
+
|
|
584
|
+
local repo_id
|
|
585
|
+
repo_id=$(get_current_repo_id)
|
|
586
|
+
|
|
587
|
+
if [ -z "$repo_id" ]; then
|
|
588
|
+
return 1
|
|
589
|
+
fi
|
|
590
|
+
|
|
591
|
+
curl -sf "${SIGHTS_API_URL}/api/agent/${repo_id}/workers/active" \
|
|
592
|
+
-H "Authorization: Bearer $api_key" 2>/dev/null
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Claim a task
|
|
596
|
+
claim_task() {
|
|
597
|
+
local task_id="$1"
|
|
598
|
+
|
|
599
|
+
local api_key
|
|
600
|
+
api_key=$(get_api_key)
|
|
601
|
+
|
|
602
|
+
if [ -z "$api_key" ]; then
|
|
603
|
+
return 1
|
|
604
|
+
fi
|
|
605
|
+
|
|
606
|
+
local repo_id
|
|
607
|
+
repo_id=$(get_current_repo_id)
|
|
608
|
+
|
|
609
|
+
if [ -z "$repo_id" ]; then
|
|
610
|
+
return 1
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
local body
|
|
614
|
+
body=$(jq -n \
|
|
615
|
+
--arg agent "merlin-loop" \
|
|
616
|
+
--arg session "$WORKER_SESSION_ID" \
|
|
617
|
+
'{
|
|
618
|
+
agentId: $agent,
|
|
619
|
+
sessionId: $session
|
|
620
|
+
}')
|
|
621
|
+
|
|
622
|
+
local response
|
|
623
|
+
response=$(curl -sf -X POST "${SIGHTS_API_URL}/api/agent/${repo_id}/tasks/${task_id}/claim" \
|
|
624
|
+
-H "Authorization: Bearer $api_key" \
|
|
625
|
+
-H "Content-Type: application/json" \
|
|
626
|
+
-d "$body" 2>/dev/null)
|
|
627
|
+
|
|
628
|
+
local success
|
|
629
|
+
success=$(echo "$response" | jq -r '.success // false')
|
|
630
|
+
|
|
631
|
+
if [ "$success" == "true" ]; then
|
|
632
|
+
return 0
|
|
633
|
+
else
|
|
634
|
+
return 1
|
|
635
|
+
fi
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
# Release a task
|
|
639
|
+
release_task() {
|
|
640
|
+
local task_id="$1"
|
|
641
|
+
|
|
642
|
+
local api_key
|
|
643
|
+
api_key=$(get_api_key)
|
|
644
|
+
|
|
645
|
+
if [ -z "$api_key" ]; then
|
|
646
|
+
return 1
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
local repo_id
|
|
650
|
+
repo_id=$(get_current_repo_id)
|
|
651
|
+
|
|
652
|
+
if [ -z "$repo_id" ]; then
|
|
653
|
+
return 1
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
curl -sf -X DELETE "${SIGHTS_API_URL}/api/agent/${repo_id}/tasks/${task_id}/claim" \
|
|
657
|
+
-H "Authorization: Bearer $api_key" > /dev/null 2>&1
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
661
|
+
# Sights Refresh
|
|
662
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
663
|
+
|
|
664
|
+
# Trigger Sights re-analysis when context is stale
|
|
665
|
+
# Usage: trigger_sights_refresh "reason"
|
|
666
|
+
trigger_sights_refresh() {
|
|
667
|
+
local reason="${1:-Manual refresh}"
|
|
668
|
+
|
|
669
|
+
local api_key
|
|
670
|
+
api_key=$(get_api_key)
|
|
671
|
+
|
|
672
|
+
if [ -z "$api_key" ]; then
|
|
673
|
+
echo "no-auth"
|
|
674
|
+
return 1
|
|
675
|
+
fi
|
|
676
|
+
|
|
677
|
+
local repo_id
|
|
678
|
+
repo_id=$(get_current_repo_id)
|
|
679
|
+
|
|
680
|
+
if [ -z "$repo_id" ]; then
|
|
681
|
+
echo "no-repo"
|
|
682
|
+
return 1
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
local response
|
|
686
|
+
response=$(curl -sf -X POST "${SIGHTS_API_URL}/api/repos/${repo_id}/sync" \
|
|
687
|
+
-H "Authorization: Bearer $api_key" \
|
|
688
|
+
-H "Content-Type: application/json" 2>/dev/null || echo "")
|
|
689
|
+
|
|
690
|
+
if [ -n "$response" ]; then
|
|
691
|
+
local success
|
|
692
|
+
success=$(echo "$response" | jq -r '.success // false' 2>/dev/null || echo "false")
|
|
693
|
+
|
|
694
|
+
if [ "$success" == "true" ]; then
|
|
695
|
+
echo "queued"
|
|
696
|
+
return 0
|
|
697
|
+
fi
|
|
698
|
+
fi
|
|
699
|
+
|
|
700
|
+
echo "failed"
|
|
701
|
+
return 1
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
705
|
+
# Initialization
|
|
706
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
707
|
+
|
|
708
|
+
# Initialize Sights for this loop session
|
|
709
|
+
init_sights() {
|
|
710
|
+
# Check connection
|
|
711
|
+
local status
|
|
712
|
+
status=$(check_sights_connection)
|
|
713
|
+
|
|
714
|
+
if [ "$status" != "connected" ]; then
|
|
715
|
+
return 1
|
|
716
|
+
fi
|
|
717
|
+
|
|
718
|
+
# Pre-fetch trajectory
|
|
719
|
+
fetch_trajectory > /dev/null 2>&1
|
|
720
|
+
|
|
721
|
+
# Register this worker
|
|
722
|
+
register_worker > /dev/null 2>&1
|
|
723
|
+
|
|
724
|
+
return 0
|
|
725
|
+
}
|