@zachjxyz/moxie 0.2.4 → 0.3.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/lib/agents.sh CHANGED
@@ -1,12 +1,146 @@
1
1
  #!/usr/bin/env bash
2
- # moxie/lib/agents.sh — Agent dispatch and rotation
2
+ # moxie/lib/agents.sh — Agent registry, dispatch, rotation, and health checks
3
3
  # Compatible with Bash 3.2 (macOS default) — no associative arrays.
4
4
 
5
+ # ---- Known agent registry ----
6
+ # To add a new agent: append one entry to each array at the same index.
7
+ # The command template is what gets written to config.toml. The prompt is
8
+ # appended as the final argument by dispatch_agent().
9
+
10
+ KNOWN_AGENT_NAMES=(claude codex qwen aider goose amp cline roo)
11
+ KNOWN_AGENT_LABELS=("Claude Code" "Codex" "Qwen Code" "Aider" "Goose" "Amp" "Cline" "Roo Code")
12
+ KNOWN_AGENT_BINARIES=(claude codex qwen aider goose amp cline roo)
13
+ KNOWN_AGENT_CMDS=(
14
+ 'claude --dangerously-skip-permissions --effort max --output-format json -p'
15
+ 'codex exec -c model_reasoning_effort="xhigh" --dangerously-bypass-approvals-and-sandbox -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true'
16
+ 'qwen --yolo -o json'
17
+ 'aider --yes-always -m'
18
+ 'goose run --output-format json -t'
19
+ 'amp --dangerously-allow-all --stream-json -x'
20
+ 'cline -y --json'
21
+ 'roo --output-format json'
22
+ )
23
+
24
+ # ---- Known gateway models (Vercel AI Gateway) ----
25
+ # To add a new gateway model: append one entry to each array at the same index.
26
+
27
+ KNOWN_GATEWAY_NAMES=(claude-gw gpt-gw qwen-gw glm-gw kimi-gw)
28
+ KNOWN_GATEWAY_MODELS=(
29
+ "anthropic/claude-opus-4-6"
30
+ "openai/gpt-5.4"
31
+ "qwen/qwen3.6-plus"
32
+ "zai/glm-5.1"
33
+ "moonshotai/kimi-k2.5"
34
+ )
35
+ KNOWN_GATEWAY_LABELS=(
36
+ "Claude Opus 4.6"
37
+ "GPT-5.4"
38
+ "Qwen 3.6 Plus"
39
+ "GLM 5.1"
40
+ "Kimi 2.5"
41
+ )
42
+
43
+ # ---- Gateway agent helpers ----
44
+
45
+ # Returns 0 if the agent is a gateway agent (command starts with "gateway:")
46
+ _is_gateway_agent() {
47
+ local name="$1"
48
+ local cmd
49
+ cmd=$(_agent_cmd "$name")
50
+ case "$cmd" in
51
+ gateway:*) return 0 ;;
52
+ *) return 1 ;;
53
+ esac
54
+ }
55
+
56
+ # Extracts the model string from a gateway agent's stored command
57
+ _agent_model() {
58
+ local name="$1"
59
+ local cmd
60
+ cmd=$(_agent_cmd "$name")
61
+ echo "${cmd#gateway:}"
62
+ }
63
+
64
+ # ---- Detect available agents on PATH ----
65
+
66
+ detect_available_agents() {
67
+ AVAILABLE_AGENT_INDICES=()
68
+ AVAILABLE_AGENT_COUNT=0
69
+
70
+ for i in "${!KNOWN_AGENT_BINARIES[@]}"; do
71
+ if command -v "${KNOWN_AGENT_BINARIES[$i]}" &>/dev/null; then
72
+ AVAILABLE_AGENT_INDICES+=("$i")
73
+ fi
74
+ done
75
+ AVAILABLE_AGENT_COUNT=${#AVAILABLE_AGENT_INDICES[@]}
76
+ }
77
+
78
+ # ---- Check minimum agent count (from config) ----
79
+
80
+ check_minimum_agents() {
81
+ if [ "${AGENT_COUNT:-0}" -eq 0 ]; then
82
+ load_agents
83
+ fi
84
+
85
+ local healthy=0
86
+ for i in "${!AGENT_NAMES[@]}"; do
87
+ local name="${AGENT_NAMES[$i]}"
88
+ if _is_gateway_agent "$name"; then
89
+ # Gateway agent: needs node + stored key
90
+ if command -v node &>/dev/null && gateway_has_key "vercel-ai-gateway"; then
91
+ healthy=$((healthy + 1))
92
+ fi
93
+ else
94
+ local cmd
95
+ cmd=$(_agent_cmd "$name")
96
+ local binary
97
+ binary=$(echo "$cmd" | awk '{print $1}')
98
+ if command -v "$binary" &>/dev/null; then
99
+ healthy=$((healthy + 1))
100
+ fi
101
+ fi
102
+ done
103
+
104
+ if [ "$healthy" -lt 2 ]; then
105
+ echo "" >&2
106
+ echo "ERROR: moxie requires at least 2 healthy agents for cross-model verification." >&2
107
+ echo "Single-agent mode is not supported." >&2
108
+ echo "" >&2
109
+ echo "Configured agents:" >&2
110
+ for i in "${!AGENT_NAMES[@]}"; do
111
+ local name="${AGENT_NAMES[$i]}"
112
+ if _is_gateway_agent "$name"; then
113
+ local model
114
+ model=$(_agent_model "$name")
115
+ if command -v node &>/dev/null && gateway_has_key "vercel-ai-gateway"; then
116
+ echo " [OK] $name — $model (AI Gateway)" >&2
117
+ else
118
+ echo " [!!] $name — $model (gateway key missing or node not found)" >&2
119
+ fi
120
+ else
121
+ local cmd
122
+ cmd=$(_agent_cmd "$name")
123
+ local binary
124
+ binary=$(echo "$cmd" | awk '{print $1}')
125
+ if command -v "$binary" &>/dev/null; then
126
+ echo " [OK] $name" >&2
127
+ else
128
+ echo " [!!] $name — '$binary' not found on PATH" >&2
129
+ fi
130
+ fi
131
+ done
132
+ echo "" >&2
133
+ echo "Run 'moxie doctor' for details, or 'moxie init' to reconfigure." >&2
134
+ return 1
135
+ fi
136
+ return 0
137
+ }
138
+
5
139
  # Global arrays populated by load_agents():
6
140
  # AGENT_NAMES=("codex" "claude" "qwen")
7
- # AGENT_CMDS_0="codex exec ..." (indexed by position in AGENT_NAMES)
8
- # AGENT_CMDS_1="claude ..."
9
- # AGENT_CMDS_2="qwen ..."
141
+ # AGENT_CMD_0="codex exec ..." (indexed by position in AGENT_NAMES)
142
+ # AGENT_CMD_1="claude ..."
143
+ # AGENT_CMD_2="qwen ..."
10
144
 
11
145
  # ---- Load agents from config ----
12
146
 
@@ -20,30 +154,40 @@ load_agents() {
20
154
  local current_agent=""
21
155
  local current_cmd=""
22
156
  local current_order="99"
157
+ local current_type=""
158
+ local current_model=""
159
+
160
+ _commit_agent() {
161
+ if [ -z "$current_agent" ]; then return; fi
162
+ # CLI agent: has command. Gateway agent: type=gateway with model.
163
+ if [ -n "$current_cmd" ]; then
164
+ AGENT_NAMES+=("$current_agent")
165
+ agent_cmds+=("$current_cmd")
166
+ agent_orders+=("$current_order")
167
+ elif [ "$current_type" = "gateway" ] && [ -n "$current_model" ]; then
168
+ AGENT_NAMES+=("$current_agent")
169
+ agent_cmds+=("gateway:$current_model")
170
+ agent_orders+=("$current_order")
171
+ fi
172
+ }
23
173
 
24
174
  while IFS= read -r line; do
25
175
  [[ "$line" =~ ^[[:space:]]*# ]] && continue
26
176
  [[ -z "${line// /}" ]] && continue
27
177
 
28
178
  if [[ "$line" =~ ^\[agents\.([a-zA-Z0-9_-]+)\] ]]; then
29
- if [ -n "$current_agent" ] && [ -n "$current_cmd" ]; then
30
- AGENT_NAMES+=("$current_agent")
31
- agent_cmds+=("$current_cmd")
32
- agent_orders+=("$current_order")
33
- fi
179
+ _commit_agent
34
180
  current_agent="${BASH_REMATCH[1]}"
35
181
  current_cmd=""
36
182
  current_order="99"
183
+ current_type=""
184
+ current_model=""
37
185
  continue
38
186
  fi
39
187
 
40
188
  # New non-agent section
41
189
  if [[ "$line" =~ ^\[ ]] && ! [[ "$line" =~ ^\[agents\. ]]; then
42
- if [ -n "$current_agent" ] && [ -n "$current_cmd" ]; then
43
- AGENT_NAMES+=("$current_agent")
44
- agent_cmds+=("$current_cmd")
45
- agent_orders+=("$current_order")
46
- fi
190
+ _commit_agent
47
191
  current_agent=""
48
192
  continue
49
193
  fi
@@ -59,15 +203,21 @@ load_agents() {
59
203
  if [[ "$line" =~ ^[[:space:]]*order[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
60
204
  current_order="${BASH_REMATCH[1]}"
61
205
  fi
206
+ if [[ "$line" =~ ^[[:space:]]*type[[:space:]]*=[[:space:]]*(.*) ]]; then
207
+ current_type="${BASH_REMATCH[1]}"
208
+ current_type="${current_type#\"}"
209
+ current_type="${current_type%\"}"
210
+ fi
211
+ if [[ "$line" =~ ^[[:space:]]*model[[:space:]]*=[[:space:]]*(.*) ]]; then
212
+ current_model="${BASH_REMATCH[1]}"
213
+ current_model="${current_model#\"}"
214
+ current_model="${current_model%\"}"
215
+ fi
62
216
  fi
63
217
  done < "$MOXIE_CONFIG"
64
218
 
65
219
  # Save last agent
66
- if [ -n "$current_agent" ] && [ -n "$current_cmd" ]; then
67
- AGENT_NAMES+=("$current_agent")
68
- agent_cmds+=("$current_cmd")
69
- agent_orders+=("$current_order")
70
- fi
220
+ _commit_agent
71
221
 
72
222
  # Sort by order using a temp file
73
223
  local sorted
@@ -110,6 +260,7 @@ dispatch_agent() {
110
260
  local agent="$1"
111
261
  local prompt_file="$2"
112
262
  local timeout_secs="$3"
263
+ local phase="${4:-unknown}"
113
264
  local cmd
114
265
  cmd=$(_agent_cmd "$agent")
115
266
 
@@ -118,7 +269,34 @@ dispatch_agent() {
118
269
  return 1
119
270
  fi
120
271
 
121
- # Isolate: strip plugin root vars so hooks from other CLIs can't fire
272
+ # Gateway agent: dispatch via Node.js script
273
+ if _is_gateway_agent "$agent"; then
274
+ local model endpoint api_key
275
+ model=$(_agent_model "$agent")
276
+ endpoint=$(toml_get "$MOXIE_CONFIG" "gateway.endpoint" "https://ai-gateway.vercel.sh")
277
+ api_key=$(gateway_get_key "vercel-ai-gateway") || {
278
+ echo "ERROR: Gateway API key not found. Run 'moxie init' to configure." >&2
279
+ return 1
280
+ }
281
+
282
+ env -u CLAUDE_PLUGIN_ROOT -u CODEX_PLUGIN_ROOT -u CURSOR_PLUGIN_ROOT \
283
+ GATEWAY_API_KEY="$api_key" \
284
+ GATEWAY_ENDPOINT="$endpoint" \
285
+ GATEWAY_MODEL="$model" \
286
+ GATEWAY_PHASE="$phase" \
287
+ GATEWAY_AGENT="$agent" \
288
+ timeout "$timeout_secs" \
289
+ node "$MOXIE_ROOT/lib/gateway-agent.mjs" "$(cat "$prompt_file")" || {
290
+ local rc=$?
291
+ if [ $rc -eq 124 ]; then
292
+ echo "[TIMEOUT] $agent exceeded ${timeout_secs}s — skipping"
293
+ fi
294
+ return $rc
295
+ }
296
+ return 0
297
+ fi
298
+
299
+ # CLI agent: dispatch via shell command
122
300
  env -u CLAUDE_PLUGIN_ROOT \
123
301
  -u CODEX_PLUGIN_ROOT \
124
302
  -u CURSOR_PLUGIN_ROOT \
@@ -145,6 +323,109 @@ next_agent() {
145
323
  echo "${AGENT_NAMES[0]}"
146
324
  }
147
325
 
326
+ # ---- Agent health tracking ----
327
+ # Tracks consecutive failures per agent. An agent is degraded after
328
+ # AGENT_DEGRADE_THRESHOLD consecutive non-productive turns (timeout,
329
+ # empty output, or no useful work done).
330
+
331
+ AGENT_DEGRADE_THRESHOLD=2
332
+
333
+ # Parallel arrays indexed by AGENT_NAMES position.
334
+ # Initialized by _init_agent_health() after load_agents.
335
+ AGENT_FAIL_STREAK=()
336
+ AGENT_DEGRADED=()
337
+
338
+ _init_agent_health() {
339
+ AGENT_FAIL_STREAK=()
340
+ AGENT_DEGRADED=()
341
+ for (( i = 0; i < AGENT_COUNT; i++ )); do
342
+ AGENT_FAIL_STREAK+=(0)
343
+ AGENT_DEGRADED+=(0)
344
+ done
345
+ }
346
+
347
+ # Returns 0 if the agent is degraded, 1 otherwise.
348
+ is_agent_degraded() {
349
+ local name="$1"
350
+ for i in "${!AGENT_NAMES[@]}"; do
351
+ if [ "${AGENT_NAMES[$i]}" = "$name" ]; then
352
+ [ "${AGENT_DEGRADED[$i]}" = "1" ]
353
+ return
354
+ fi
355
+ done
356
+ return 1
357
+ }
358
+
359
+ # Record a productive or non-productive turn for an agent.
360
+ # Usage: _record_turn_health <agent_name> <logfile>
361
+ _record_turn_health() {
362
+ local name="$1"
363
+ local logfile="$2"
364
+
365
+ local idx=-1
366
+ for i in "${!AGENT_NAMES[@]}"; do
367
+ [ "${AGENT_NAMES[$i]}" = "$name" ] && idx=$i && break
368
+ done
369
+ [ "$idx" = "-1" ] && return
370
+
371
+ # Already degraded — don't re-evaluate
372
+ [ "${AGENT_DEGRADED[$idx]}" = "1" ] && return
373
+
374
+ # A productive turn has a log file > 1KB with actual agent output
375
+ # (not just the prompt echo or timeout message)
376
+ local log_size=0
377
+ if [ -f "$logfile" ]; then
378
+ log_size=$(wc -c < "$logfile" | tr -d ' ')
379
+ fi
380
+
381
+ # Check for known failure signatures in the log
382
+ local is_failure=0
383
+ if [ "$log_size" -lt 1024 ]; then
384
+ is_failure=1
385
+ elif grep -qE '^\[TIMEOUT\]' "$logfile" 2>/dev/null; then
386
+ is_failure=1
387
+ fi
388
+
389
+ if [ "$is_failure" = "1" ]; then
390
+ AGENT_FAIL_STREAK[$idx]=$(( ${AGENT_FAIL_STREAK[$idx]} + 1 ))
391
+ local streak=${AGENT_FAIL_STREAK[$idx]}
392
+
393
+ if [ "$streak" -ge "$AGENT_DEGRADE_THRESHOLD" ]; then
394
+ AGENT_DEGRADED[$idx]=1
395
+ echo " [DEGRADED] $name failed $streak consecutive turns — removing from rotation"
396
+ echo " (possible quota exhaustion, auth failure, or CLI crash)"
397
+
398
+ # Count remaining healthy agents
399
+ local healthy=0
400
+ for j in "${!AGENT_DEGRADED[@]}"; do
401
+ [ "${AGENT_DEGRADED[$j]}" = "0" ] && healthy=$((healthy + 1))
402
+ done
403
+
404
+ if [ "$healthy" -lt 2 ]; then
405
+ echo ""
406
+ echo " FATAL: Only $healthy healthy agent(s) remaining."
407
+ echo " moxie requires at least 2 agents for cross-model verification."
408
+ echo " Pipeline cannot continue. Stopping."
409
+ return 2
410
+ fi
411
+ else
412
+ echo " [WARN] $name produced no useful output (turn $streak/$AGENT_DEGRADE_THRESHOLD before degraded)"
413
+ fi
414
+ else
415
+ # Productive turn — reset streak
416
+ AGENT_FAIL_STREAK[$idx]=0
417
+ fi
418
+ }
419
+
420
+ # Count healthy (non-degraded) agents.
421
+ count_healthy_agents() {
422
+ local healthy=0
423
+ for i in "${!AGENT_DEGRADED[@]}"; do
424
+ [ "${AGENT_DEGRADED[$i]}" = "0" ] && healthy=$((healthy + 1))
425
+ done
426
+ echo "$healthy"
427
+ }
428
+
148
429
  # ---- Logging ----
149
430
 
150
431
  dispatch_logged() {
@@ -168,12 +449,15 @@ dispatch_logged() {
168
449
  return 0
169
450
  fi
170
451
 
171
- dispatch_agent "$agent" "$prompt_file" "$timeout_secs" 2>&1 | tee "$logfile" || true
452
+ dispatch_agent "$agent" "$prompt_file" "$timeout_secs" "$phase" 2>&1 | tee "$logfile" || true
172
453
 
173
454
  local tokens
174
455
  tokens=$(extract_tokens "$logfile" "$agent")
175
456
  record_tokens "$csv_file" "$turn" "$agent" "$ts" "$tokens" "$phase"
176
457
  echo " Tokens: $tokens"
458
+
459
+ # Track agent health based on output
460
+ _record_turn_health "$agent" "$logfile" || return $?
177
461
  }
178
462
 
179
463
  # ---- List agents ----
@@ -210,13 +494,18 @@ cmd_doctor() {
210
494
  # ---- Dependencies ----
211
495
  echo "Dependencies:"
212
496
  _check_dep "python3" "Required for ledger parsing and token tracking" || all_ok=0
213
- _check_dep "caffeinate" "Keeps machine awake during runs (macOS)" || true # non-fatal
497
+ # Sleep inhibitor: platform-specific, non-fatal
498
+ if [ "$MOXIE_PLATFORM" = "darwin" ]; then
499
+ _check_dep "caffeinate" "Keeps machine awake during runs (macOS)" || true
500
+ elif [ "$MOXIE_PLATFORM" = "linux" ]; then
501
+ _check_dep "systemd-inhibit" "Keeps machine awake during runs (Linux)" || true
502
+ fi
214
503
  if command -v timeout &>/dev/null; then
215
504
  echo " [OK] timeout"
216
505
  elif command -v gtimeout &>/dev/null; then
217
506
  echo " [OK] coreutils (gtimeout aliased as timeout)"
218
507
  else
219
- echo " [!!] coreutils — not found. Install: brew install coreutils"
508
+ echo " [!!] coreutils — not found. Install: $(coreutils_install_hint)"
220
509
  echo " Provides timeout protection for agent turns (gtimeout)."
221
510
  all_ok=0
222
511
  fi
@@ -225,37 +514,78 @@ cmd_doctor() {
225
514
  # ---- Agents ----
226
515
  if [ "$has_project" = "1" ]; then
227
516
  echo "Agents (from .moxie/config.toml):"
517
+ local healthy_count=0
228
518
  for i in "${!AGENT_NAMES[@]}"; do
229
519
  local name="${AGENT_NAMES[$i]}"
230
- local cmd
231
- cmd=$(_agent_cmd "$name")
232
520
 
233
- # Extract the base binary from the command
234
- local binary
235
- binary=$(echo "$cmd" | awk '{print $1}')
236
-
237
- if ! command -v "$binary" &>/dev/null; then
238
- echo " [!!] $name — '$binary' not found on PATH"
239
- all_ok=0
240
- continue
241
- fi
242
-
243
- # Try --version to verify it's functional
244
- local version
245
- version=$("$binary" --version 2>&1 | head -1)
246
- if [ $? -eq 0 ] && [ -n "$version" ]; then
247
- echo " [OK] $name $version"
521
+ if _is_gateway_agent "$name"; then
522
+ # Gateway agent
523
+ local model
524
+ model=$(_agent_model "$name")
525
+ local gw_ok=1
526
+
527
+ if ! command -v node &>/dev/null; then
528
+ echo " [!!] $name — $model (node not found on PATH)"
529
+ all_ok=0
530
+ gw_ok=0
531
+ fi
532
+
533
+ if [ "$gw_ok" = "1" ] && ! gateway_has_key "vercel-ai-gateway"; then
534
+ echo " [!!] $name $model (AI Gateway key not configured)"
535
+ echo " Run 'moxie init' to set up your gateway key."
536
+ all_ok=0
537
+ gw_ok=0
538
+ fi
539
+
540
+ if [ "$gw_ok" = "1" ]; then
541
+ echo " [OK] $name — $model (AI Gateway, key stored)"
542
+ healthy_count=$((healthy_count + 1))
543
+ fi
248
544
  else
249
- echo " [??] $name — '$binary' found but --version failed"
250
- echo " May not be authenticated. Try running: $binary --version"
545
+ # CLI agent
546
+ local cmd
547
+ cmd=$(_agent_cmd "$name")
548
+ local binary
549
+ binary=$(echo "$cmd" | awk '{print $1}')
550
+
551
+ if ! command -v "$binary" &>/dev/null; then
552
+ echo " [!!] $name — '$binary' not found on PATH"
553
+ all_ok=0
554
+ continue
555
+ fi
556
+
557
+ healthy_count=$((healthy_count + 1))
558
+
559
+ local version
560
+ version=$("$binary" --version 2>&1 | head -1)
561
+ if [ $? -eq 0 ] && [ -n "$version" ]; then
562
+ echo " [OK] $name — $version"
563
+ else
564
+ echo " [??] $name — '$binary' found but --version failed"
565
+ echo " May not be authenticated. Try running: $binary --version"
566
+ fi
251
567
  fi
252
568
  done
569
+
570
+ if [ "$healthy_count" -lt 2 ]; then
571
+ echo ""
572
+ echo " [FAIL] Only $healthy_count healthy agent(s). moxie requires at least 2"
573
+ echo " for cross-model verification. Single-agent mode is not supported."
574
+ all_ok=0
575
+ fi
253
576
  else
254
577
  echo "Agents:"
255
- echo " (no .moxie/ project — checking common agents)"
256
- _check_agent_binary "codex" || all_ok=0
257
- _check_agent_binary "claude" || all_ok=0
258
- _check_agent_binary "qwen" || all_ok=0
578
+ echo " (no .moxie/ project — scanning for known agents)"
579
+ detect_available_agents
580
+ for i in "${!KNOWN_AGENT_NAMES[@]}"; do
581
+ _check_agent_binary "${KNOWN_AGENT_BINARIES[$i]}" || true
582
+ done
583
+ if [ "$AVAILABLE_AGENT_COUNT" -lt 2 ]; then
584
+ echo ""
585
+ echo " [FAIL] Only $AVAILABLE_AGENT_COUNT agent(s) found. moxie requires at least 2"
586
+ echo " for cross-model verification. Single-agent mode is not supported."
587
+ all_ok=0
588
+ fi
259
589
  fi
260
590
  echo ""
261
591
 
@@ -295,8 +625,10 @@ cmd_doctor() {
295
625
  echo ""
296
626
  if [ "$all_ok" = "1" ]; then
297
627
  echo "All checks passed. Ready to run."
628
+ return 0
298
629
  else
299
630
  echo "Some checks failed. Fix the issues above before running."
631
+ return 1
300
632
  fi
301
633
  }
302
634