@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/README.md +280 -0
- package/bin/moxie +4 -2
- package/lib/agents.sh +378 -46
- package/lib/gateway-agent.mjs +549 -0
- package/lib/gateway-cost.mjs +78 -0
- package/lib/gateway-keys.sh +118 -0
- package/lib/phases.sh +569 -75
- package/lib/platform.sh +78 -0
- package/lib/tokens.sh +121 -20
- package/package.json +9 -3
package/lib/phases.sh
CHANGED
|
@@ -52,13 +52,13 @@ _discover_context_docs() {
|
|
|
52
52
|
echo "$rp"
|
|
53
53
|
done < "$tmpfile" | sort -u > "$dedup_file"
|
|
54
54
|
|
|
55
|
-
# Sort by mtime (most recent first)
|
|
55
|
+
# Sort by mtime (most recent first)
|
|
56
56
|
local sorted_file
|
|
57
57
|
sorted_file=$(mktemp)
|
|
58
58
|
|
|
59
59
|
while IFS= read -r rp; do
|
|
60
60
|
local mtime
|
|
61
|
-
mtime=$(
|
|
61
|
+
mtime=$(stat_mtime "$rp")
|
|
62
62
|
echo "$mtime $rp"
|
|
63
63
|
done < "$dedup_file" | sort -rn | head -10 | while IFS= read -r line; do
|
|
64
64
|
echo "${line#* }"
|
|
@@ -97,7 +97,7 @@ _select_context_docs() {
|
|
|
97
97
|
else
|
|
98
98
|
sz="${sz} B"
|
|
99
99
|
fi
|
|
100
|
-
mdate=$(
|
|
100
|
+
mdate=$(stat_date "$f")
|
|
101
101
|
meta+=("${sz}, ${mdate}")
|
|
102
102
|
local len=${#f}
|
|
103
103
|
[ "$len" -gt "$max_label_len" ] && max_label_len=$len
|
|
@@ -209,6 +209,272 @@ _select_context_docs() {
|
|
|
209
209
|
done
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
# ---- Agent selection TUI ----
|
|
213
|
+
# Populates SELECTED_AGENT_INDICES (CLI) and SELECTED_GATEWAY_INDICES (gateway).
|
|
214
|
+
# Shows CLI agents found on PATH + gateway models. Requires minimum 2 total.
|
|
215
|
+
|
|
216
|
+
_select_agents() {
|
|
217
|
+
SELECTED_AGENT_INDICES=()
|
|
218
|
+
SELECTED_GATEWAY_INDICES=()
|
|
219
|
+
|
|
220
|
+
# Build unified item list: CLI agents, separator, gateway models
|
|
221
|
+
local item_labels=()
|
|
222
|
+
local item_meta=()
|
|
223
|
+
local item_types=() # "cli", "separator", "gateway"
|
|
224
|
+
local item_sources=() # index into KNOWN_AGENT_* or KNOWN_GATEWAY_*
|
|
225
|
+
local max_label_len=0
|
|
226
|
+
|
|
227
|
+
# CLI agents header
|
|
228
|
+
item_labels+=("--- Detected CLI agents ---")
|
|
229
|
+
item_meta+=("")
|
|
230
|
+
item_types+=("separator")
|
|
231
|
+
item_sources+=("-1")
|
|
232
|
+
|
|
233
|
+
# CLI agents on PATH
|
|
234
|
+
for idx in "${AVAILABLE_AGENT_INDICES[@]}"; do
|
|
235
|
+
local label="${KNOWN_AGENT_LABELS[$idx]}"
|
|
236
|
+
local binary="${KNOWN_AGENT_BINARIES[$idx]}"
|
|
237
|
+
item_labels+=("$label")
|
|
238
|
+
local ver
|
|
239
|
+
ver=$("$binary" --version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+[0-9.]*' | head -1) || ver=""
|
|
240
|
+
[ -z "$ver" ] && ver="installed"
|
|
241
|
+
item_meta+=("$ver")
|
|
242
|
+
item_types+=("cli")
|
|
243
|
+
item_sources+=("$idx")
|
|
244
|
+
local len=${#name}
|
|
245
|
+
[ "$len" -gt "$max_label_len" ] && max_label_len=$len
|
|
246
|
+
done
|
|
247
|
+
|
|
248
|
+
# Separator
|
|
249
|
+
item_labels+=("--- Vercel AI Gateway ---")
|
|
250
|
+
item_meta+=("")
|
|
251
|
+
item_types+=("separator")
|
|
252
|
+
item_sources+=("-1")
|
|
253
|
+
|
|
254
|
+
# Gateway models (always shown)
|
|
255
|
+
for idx in "${!KNOWN_GATEWAY_NAMES[@]}"; do
|
|
256
|
+
local label="${KNOWN_GATEWAY_LABELS[$idx]}"
|
|
257
|
+
item_labels+=("$label")
|
|
258
|
+
item_meta+=("${KNOWN_GATEWAY_MODELS[$idx]}")
|
|
259
|
+
item_types+=("gateway")
|
|
260
|
+
item_sources+=("$idx")
|
|
261
|
+
local len=${#label}
|
|
262
|
+
[ "$len" -gt "$max_label_len" ] && max_label_len=$len
|
|
263
|
+
done
|
|
264
|
+
|
|
265
|
+
local count=${#item_labels[@]}
|
|
266
|
+
|
|
267
|
+
# State: pre-select CLI agents, gateway unselected
|
|
268
|
+
local cursor=0
|
|
269
|
+
local selected=()
|
|
270
|
+
for (( i = 0; i < count; i++ )); do
|
|
271
|
+
if [ "${item_types[$i]}" = "cli" ]; then
|
|
272
|
+
selected+=(1)
|
|
273
|
+
else
|
|
274
|
+
selected+=(0)
|
|
275
|
+
fi
|
|
276
|
+
done
|
|
277
|
+
|
|
278
|
+
# Skip separators for initial cursor position
|
|
279
|
+
while [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" = "separator" ]; do
|
|
280
|
+
cursor=$((cursor + 1))
|
|
281
|
+
done
|
|
282
|
+
|
|
283
|
+
local sep=""
|
|
284
|
+
for (( i = 0; i < max_label_len + 40; i++ )); do sep="${sep}-"; done
|
|
285
|
+
|
|
286
|
+
# submit row is at index $count
|
|
287
|
+
local jump_back=$(( count + 3 ))
|
|
288
|
+
|
|
289
|
+
_agent_render() {
|
|
290
|
+
local sel_count=0
|
|
291
|
+
for (( i = 0; i < count; i++ )); do
|
|
292
|
+
[ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
|
|
293
|
+
done
|
|
294
|
+
|
|
295
|
+
for (( i = 0; i < count; i++ )); do
|
|
296
|
+
if [ "${item_types[$i]}" = "separator" ]; then
|
|
297
|
+
printf "\\r\\033[K \\033[2m%s\\033[0m\\n" "${item_labels[$i]}" >&2
|
|
298
|
+
continue
|
|
299
|
+
fi
|
|
300
|
+
local marker=" "
|
|
301
|
+
[ "$cursor" -eq "$i" ] && marker="> "
|
|
302
|
+
local check="[ ]"
|
|
303
|
+
[ "${selected[$i]}" = "1" ] && check="[x]"
|
|
304
|
+
if [ -n "${item_meta[$i]}" ]; then
|
|
305
|
+
printf "\\r\\033[K %s%s %-${max_label_len}s (%s)\\n" "$marker" "$check" "${item_labels[$i]}" "${item_meta[$i]}" >&2
|
|
306
|
+
else
|
|
307
|
+
printf "\\r\\033[K %s%s %s\\n" "$marker" "$check" "${item_labels[$i]}" >&2
|
|
308
|
+
fi
|
|
309
|
+
done
|
|
310
|
+
|
|
311
|
+
printf "\\r\\033[K %s\\n" "$sep" >&2
|
|
312
|
+
|
|
313
|
+
local submit_marker=" "
|
|
314
|
+
[ "$cursor" -eq "$count" ] && submit_marker="> "
|
|
315
|
+
if [ "$sel_count" -ge 2 ]; then
|
|
316
|
+
printf "\\r\\033[K %s%s\\n" "$submit_marker" "Submit ($sel_count selected)" >&2
|
|
317
|
+
else
|
|
318
|
+
printf "\\r\\033[K %s\\033[2m%s\\033[0m\\n" "$submit_marker" "Submit (need at least 2)" >&2
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space/enter toggle · enter submit (min 2)\\033[0m" >&2
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
printf "Select agents (at least 2). CLI agents pre-selected, gateway models available below:\\n\\n" >&2
|
|
325
|
+
printf "\\033[?25l" >&2
|
|
326
|
+
_agent_render
|
|
327
|
+
|
|
328
|
+
local old_stty
|
|
329
|
+
old_stty=$(stty -g < /dev/tty 2>/dev/null)
|
|
330
|
+
_agent_cleanup() {
|
|
331
|
+
printf "\\033[?25h" >&2
|
|
332
|
+
stty "$old_stty" < /dev/tty 2>/dev/null
|
|
333
|
+
}
|
|
334
|
+
trap '_agent_cleanup' INT TERM
|
|
335
|
+
stty -echo -icanon min 1 < /dev/tty 2>/dev/null
|
|
336
|
+
|
|
337
|
+
while true; do
|
|
338
|
+
local key
|
|
339
|
+
key=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || break
|
|
340
|
+
|
|
341
|
+
if [ "$key" = $'\033' ]; then
|
|
342
|
+
local bracket dir
|
|
343
|
+
bracket=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
|
|
344
|
+
dir=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
|
|
345
|
+
if [ "$bracket" = "[" ]; then
|
|
346
|
+
case "$dir" in
|
|
347
|
+
A) # Up: skip separators
|
|
348
|
+
local new_cursor=$((cursor - 1))
|
|
349
|
+
while [ "$new_cursor" -ge 0 ] && [ "${item_types[$new_cursor]}" = "separator" ]; do
|
|
350
|
+
new_cursor=$((new_cursor - 1))
|
|
351
|
+
done
|
|
352
|
+
[ "$new_cursor" -ge 0 ] && cursor=$new_cursor
|
|
353
|
+
;;
|
|
354
|
+
B) # Down: skip separators
|
|
355
|
+
local new_cursor=$((cursor + 1))
|
|
356
|
+
while [ "$new_cursor" -lt "$count" ] && [ "${item_types[$new_cursor]}" = "separator" ]; do
|
|
357
|
+
new_cursor=$((new_cursor + 1))
|
|
358
|
+
done
|
|
359
|
+
[ "$new_cursor" -le "$count" ] && cursor=$new_cursor
|
|
360
|
+
;;
|
|
361
|
+
esac
|
|
362
|
+
fi
|
|
363
|
+
elif [ "$key" = " " ]; then
|
|
364
|
+
if [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" != "separator" ]; then
|
|
365
|
+
[ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
|
|
366
|
+
fi
|
|
367
|
+
elif [ "$key" = "" ]; then
|
|
368
|
+
if [ "$cursor" -eq "$count" ]; then
|
|
369
|
+
local sel_count=0
|
|
370
|
+
for (( i = 0; i < count; i++ )); do
|
|
371
|
+
[ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
|
|
372
|
+
done
|
|
373
|
+
[ "$sel_count" -ge 2 ] && break
|
|
374
|
+
elif [ "${item_types[$cursor]}" != "separator" ]; then
|
|
375
|
+
[ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
|
|
376
|
+
fi
|
|
377
|
+
elif [ "$key" = "q" ]; then
|
|
378
|
+
for (( i = 0; i < count; i++ )); do selected[$i]=0; done
|
|
379
|
+
break
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
printf "\\033[%dA" "$jump_back" >&2
|
|
383
|
+
_agent_render
|
|
384
|
+
done
|
|
385
|
+
|
|
386
|
+
_agent_cleanup
|
|
387
|
+
trap - INT TERM
|
|
388
|
+
printf "\\n\\n" >&2
|
|
389
|
+
|
|
390
|
+
# Collect selections by type
|
|
391
|
+
local has_gateway=0
|
|
392
|
+
for (( i = 0; i < count; i++ )); do
|
|
393
|
+
[ "${selected[$i]}" != "1" ] && continue
|
|
394
|
+
case "${item_types[$i]}" in
|
|
395
|
+
cli) SELECTED_AGENT_INDICES+=("${item_sources[$i]}") ;;
|
|
396
|
+
gateway)
|
|
397
|
+
SELECTED_GATEWAY_INDICES+=("${item_sources[$i]}")
|
|
398
|
+
has_gateway=1
|
|
399
|
+
;;
|
|
400
|
+
esac
|
|
401
|
+
done
|
|
402
|
+
|
|
403
|
+
# If gateway models selected, ensure API key is stored
|
|
404
|
+
if [ "$has_gateway" = "1" ] && ! gateway_has_key "vercel-ai-gateway"; then
|
|
405
|
+
printf "Gateway models selected. Setting up API key...\\n\\n" >&2
|
|
406
|
+
gateway_store_key "vercel-ai-gateway" || {
|
|
407
|
+
echo "ERROR: Failed to store gateway key. Gateway models will not work." >&2
|
|
408
|
+
SELECTED_GATEWAY_INDICES=()
|
|
409
|
+
}
|
|
410
|
+
fi
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# ---- Write config.toml from selected agents ----
|
|
414
|
+
|
|
415
|
+
_write_config_toml() {
|
|
416
|
+
local config_file="$1"
|
|
417
|
+
cat > "$config_file" <<'HEADER'
|
|
418
|
+
# moxie configuration
|
|
419
|
+
# Generated by: moxie init
|
|
420
|
+
|
|
421
|
+
[spec]
|
|
422
|
+
path = "spec.md"
|
|
423
|
+
|
|
424
|
+
HEADER
|
|
425
|
+
|
|
426
|
+
# Gateway section (if any gateway models selected)
|
|
427
|
+
if [ ${#SELECTED_GATEWAY_INDICES[@]} -gt 0 ]; then
|
|
428
|
+
cat >> "$config_file" <<'GATEWAY'
|
|
429
|
+
[gateway]
|
|
430
|
+
endpoint = "https://ai-gateway.vercel.sh"
|
|
431
|
+
|
|
432
|
+
GATEWAY
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
cat >> "$config_file" <<'AGENTHEAD'
|
|
436
|
+
# Agent definitions — order is the default rotation sequence.
|
|
437
|
+
# Rotation is randomized per phase at runtime, so order only
|
|
438
|
+
# determines fallback ordering.
|
|
439
|
+
AGENTHEAD
|
|
440
|
+
|
|
441
|
+
local order=1
|
|
442
|
+
|
|
443
|
+
# CLI agents
|
|
444
|
+
for idx in "${SELECTED_AGENT_INDICES[@]}"; do
|
|
445
|
+
local name="${KNOWN_AGENT_NAMES[$idx]}"
|
|
446
|
+
local cmd="${KNOWN_AGENT_CMDS[$idx]}"
|
|
447
|
+
cat >> "$config_file" <<AGENT
|
|
448
|
+
[agents.${name}]
|
|
449
|
+
command = '${cmd}'
|
|
450
|
+
order = ${order}
|
|
451
|
+
|
|
452
|
+
AGENT
|
|
453
|
+
order=$((order + 1))
|
|
454
|
+
done
|
|
455
|
+
|
|
456
|
+
# Gateway agents
|
|
457
|
+
for idx in "${SELECTED_GATEWAY_INDICES[@]}"; do
|
|
458
|
+
local name="${KNOWN_GATEWAY_NAMES[$idx]}"
|
|
459
|
+
local model="${KNOWN_GATEWAY_MODELS[$idx]}"
|
|
460
|
+
cat >> "$config_file" <<GWAGENT
|
|
461
|
+
[agents.${name}]
|
|
462
|
+
type = "gateway"
|
|
463
|
+
model = "${model}"
|
|
464
|
+
order = ${order}
|
|
465
|
+
|
|
466
|
+
GWAGENT
|
|
467
|
+
order=$((order + 1))
|
|
468
|
+
done
|
|
469
|
+
|
|
470
|
+
cat >> "$config_file" <<'SETTINGS'
|
|
471
|
+
[settings]
|
|
472
|
+
max_rounds = 15
|
|
473
|
+
max_rounds_build = 50
|
|
474
|
+
turn_timeout = 900
|
|
475
|
+
SETTINGS
|
|
476
|
+
}
|
|
477
|
+
|
|
212
478
|
_copy_context_docs() {
|
|
213
479
|
if [ ${#CONTEXT_SELECTED[@]} -eq 0 ]; then
|
|
214
480
|
return
|
|
@@ -273,7 +539,44 @@ cmd_init() {
|
|
|
273
539
|
echo "WARNING: $MOXIE_DIR already exists. Reinitializing." >&2
|
|
274
540
|
fi
|
|
275
541
|
|
|
276
|
-
#
|
|
542
|
+
# ---- Detect and select agents ----
|
|
543
|
+
detect_available_agents
|
|
544
|
+
|
|
545
|
+
if [ "$AVAILABLE_AGENT_COUNT" -lt 2 ]; then
|
|
546
|
+
echo "ERROR: moxie requires at least 2 agent CLIs installed." >&2
|
|
547
|
+
echo "" >&2
|
|
548
|
+
echo "Found $AVAILABLE_AGENT_COUNT agent(s) on PATH. Install at least 2 of:" >&2
|
|
549
|
+
for i in "${!KNOWN_AGENT_NAMES[@]}"; do
|
|
550
|
+
local binary="${KNOWN_AGENT_BINARIES[$i]}"
|
|
551
|
+
if command -v "$binary" &>/dev/null; then
|
|
552
|
+
echo " [OK] ${KNOWN_AGENT_NAMES[$i]}" >&2
|
|
553
|
+
else
|
|
554
|
+
echo " [ ] ${KNOWN_AGENT_NAMES[$i]} (not found)" >&2
|
|
555
|
+
fi
|
|
556
|
+
done
|
|
557
|
+
echo "" >&2
|
|
558
|
+
echo "moxie requires at least 2 agents for cross-model verification." >&2
|
|
559
|
+
echo "Single-agent mode is not supported." >&2
|
|
560
|
+
exit 1
|
|
561
|
+
fi
|
|
562
|
+
|
|
563
|
+
if [ -t 0 ]; then
|
|
564
|
+
_select_agents
|
|
565
|
+
else
|
|
566
|
+
# Non-interactive: auto-select all available CLI agents
|
|
567
|
+
SELECTED_AGENT_INDICES=("${AVAILABLE_AGENT_INDICES[@]}")
|
|
568
|
+
SELECTED_GATEWAY_INDICES=()
|
|
569
|
+
fi
|
|
570
|
+
|
|
571
|
+
local total_selected=$(( ${#SELECTED_AGENT_INDICES[@]} + ${#SELECTED_GATEWAY_INDICES[@]} ))
|
|
572
|
+
if [ "$total_selected" -lt 2 ]; then
|
|
573
|
+
echo "ERROR: At least 2 agents must be selected." >&2
|
|
574
|
+
echo "moxie requires at least 2 agents for cross-model verification." >&2
|
|
575
|
+
echo "Single-agent mode is not supported." >&2
|
|
576
|
+
exit 1
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
# ---- Spec handling ----
|
|
277
580
|
if [ -z "$spec_path" ]; then
|
|
278
581
|
generate_rfc=1
|
|
279
582
|
echo "Initializing moxie project (RFC will be generated by agents)..."
|
|
@@ -285,6 +588,15 @@ cmd_init() {
|
|
|
285
588
|
echo "Initializing moxie project..."
|
|
286
589
|
echo " Spec: $spec_path"
|
|
287
590
|
fi
|
|
591
|
+
|
|
592
|
+
# List selected agents
|
|
593
|
+
echo " Agents:"
|
|
594
|
+
for idx in "${SELECTED_AGENT_INDICES[@]}"; do
|
|
595
|
+
echo " - ${KNOWN_AGENT_LABELS[$idx]} (CLI)"
|
|
596
|
+
done
|
|
597
|
+
for idx in "${SELECTED_GATEWAY_INDICES[@]}"; do
|
|
598
|
+
echo " - ${KNOWN_GATEWAY_LABELS[$idx]} (${KNOWN_GATEWAY_MODELS[$idx]}, AI Gateway)"
|
|
599
|
+
done
|
|
288
600
|
echo ""
|
|
289
601
|
|
|
290
602
|
# Create directory structure
|
|
@@ -307,35 +619,8 @@ SEED
|
|
|
307
619
|
cp "$spec_path" "$MOXIE_DIR/spec.md"
|
|
308
620
|
fi
|
|
309
621
|
|
|
310
|
-
# Generate config
|
|
311
|
-
|
|
312
|
-
# moxie configuration
|
|
313
|
-
# Generated by: moxie init
|
|
314
|
-
|
|
315
|
-
[spec]
|
|
316
|
-
path = "spec.md"
|
|
317
|
-
|
|
318
|
-
# Agent definitions — order is the default rotation sequence.
|
|
319
|
-
# Rotation is randomized per phase at runtime, so order only
|
|
320
|
-
# determines fallback ordering.
|
|
321
|
-
# command is passed the prompt as the final argument.
|
|
322
|
-
[agents.codex]
|
|
323
|
-
command = 'codex exec -c model_reasoning_effort="xhigh" --dangerously-bypass-approvals-and-sandbox -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true'
|
|
324
|
-
order = 1
|
|
325
|
-
|
|
326
|
-
[agents.claude]
|
|
327
|
-
command = "claude --dangerously-skip-permissions --effort max -p"
|
|
328
|
-
order = 2
|
|
329
|
-
|
|
330
|
-
[agents.qwen]
|
|
331
|
-
command = "qwen --yolo"
|
|
332
|
-
order = 3
|
|
333
|
-
|
|
334
|
-
[settings]
|
|
335
|
-
max_rounds = 15
|
|
336
|
-
max_rounds_build = 50
|
|
337
|
-
turn_timeout = 900
|
|
338
|
-
TOML
|
|
622
|
+
# Generate config from selected agents
|
|
623
|
+
_write_config_toml "$MOXIE_CONFIG"
|
|
339
624
|
|
|
340
625
|
# Generate prompt templates from bundled templates
|
|
341
626
|
for phase in "${PHASES[@]}"; do
|
|
@@ -383,8 +668,8 @@ TOML
|
|
|
383
668
|
if [ ! -d "$MOXIE_DIR/context" ] || [ -z "$(ls -A "$MOXIE_DIR/context" 2>/dev/null)" ]; then
|
|
384
669
|
echo " Tip: add context docs to .moxie/context/ (roadmaps, PRDs, etc.)"
|
|
385
670
|
fi
|
|
386
|
-
echo " 3. Run: moxie start (background
|
|
387
|
-
echo " Or: moxie run (foreground
|
|
671
|
+
echo " 3. Run: moxie start (background)"
|
|
672
|
+
echo " Or: moxie run (foreground)"
|
|
388
673
|
}
|
|
389
674
|
|
|
390
675
|
_init_ledger() {
|
|
@@ -467,6 +752,39 @@ sys.exit(0)
|
|
|
467
752
|
" 2>/dev/null
|
|
468
753
|
}
|
|
469
754
|
|
|
755
|
+
# Like _all_agents_reached but excludes degraded agents.
|
|
756
|
+
# Quorum requires all HEALTHY agents to have reached: true.
|
|
757
|
+
_healthy_agents_reached() {
|
|
758
|
+
local ledger="$1"
|
|
759
|
+
|
|
760
|
+
# Build space-separated list of healthy agent names
|
|
761
|
+
local healthy_names=""
|
|
762
|
+
for i in "${!AGENT_NAMES[@]}"; do
|
|
763
|
+
if [ "${AGENT_DEGRADED[$i]}" = "0" ]; then
|
|
764
|
+
healthy_names="${healthy_names}${AGENT_NAMES[$i]} "
|
|
765
|
+
fi
|
|
766
|
+
done
|
|
767
|
+
healthy_names="${healthy_names% }" # trim trailing space
|
|
768
|
+
|
|
769
|
+
local healthy_count
|
|
770
|
+
healthy_count=$(count_healthy_agents)
|
|
771
|
+
|
|
772
|
+
python3 -c "
|
|
773
|
+
import json, sys
|
|
774
|
+
with open('$ledger') as f:
|
|
775
|
+
d = json.load(f)
|
|
776
|
+
agents = d.get('agents', {})
|
|
777
|
+
required = set('$healthy_names'.split())
|
|
778
|
+
if len(required) < 2:
|
|
779
|
+
sys.exit(1)
|
|
780
|
+
for name in required:
|
|
781
|
+
info = agents.get(name, {})
|
|
782
|
+
if not info.get('reached', False):
|
|
783
|
+
sys.exit(1)
|
|
784
|
+
sys.exit(0)
|
|
785
|
+
" 2>/dev/null
|
|
786
|
+
}
|
|
787
|
+
|
|
470
788
|
# ---- Phase completion detection (for resume) ----
|
|
471
789
|
|
|
472
790
|
_phase_is_complete() {
|
|
@@ -496,6 +814,7 @@ _shuffle_agents() {
|
|
|
496
814
|
cmd_run() {
|
|
497
815
|
require_moxie_project
|
|
498
816
|
load_agents
|
|
817
|
+
check_minimum_agents || exit 1
|
|
499
818
|
|
|
500
819
|
local target_phase=""
|
|
501
820
|
local dry_run="${DRY_RUN:-0}"
|
|
@@ -527,19 +846,34 @@ cmd_run() {
|
|
|
527
846
|
echo $$ > "$MOXIE_DIR/moxie.pid"
|
|
528
847
|
trap 'rm -f "$MOXIE_DIR/moxie.pid"' EXIT
|
|
529
848
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
849
|
+
if [ "$MOXIE_PLATFORM" = "darwin" ]; then
|
|
850
|
+
caffeinate -d -i -s bash -c "
|
|
851
|
+
set -euo pipefail; cd '$(pwd)'
|
|
852
|
+
export DRY_RUN=0 MOXIE_ROOT='$MOXIE_ROOT' MOXIE_LIB='$MOXIE_LIB'
|
|
853
|
+
source '$MOXIE_LIB/platform.sh'; source '$MOXIE_LIB/core.sh'
|
|
854
|
+
source '$MOXIE_LIB/agents.sh'; source '$MOXIE_LIB/phases.sh'
|
|
855
|
+
source '$MOXIE_LIB/tokens.sh'
|
|
856
|
+
load_agents; _run_pipeline ${run_phases[*]}
|
|
857
|
+
"
|
|
858
|
+
elif [ "$MOXIE_PLATFORM" = "linux" ] && command -v systemd-inhibit &>/dev/null; then
|
|
859
|
+
systemd-inhibit --what=idle:sleep --who=moxie --why="moxie pipeline" bash -c "
|
|
860
|
+
set -euo pipefail; cd '$(pwd)'
|
|
861
|
+
export DRY_RUN=0 MOXIE_ROOT='$MOXIE_ROOT' MOXIE_LIB='$MOXIE_LIB'
|
|
862
|
+
source '$MOXIE_LIB/platform.sh'; source '$MOXIE_LIB/core.sh'
|
|
863
|
+
source '$MOXIE_LIB/agents.sh'; source '$MOXIE_LIB/phases.sh'
|
|
864
|
+
source '$MOXIE_LIB/tokens.sh'
|
|
865
|
+
load_agents; _run_pipeline ${run_phases[*]}
|
|
866
|
+
"
|
|
867
|
+
else
|
|
868
|
+
bash -c "
|
|
869
|
+
set -euo pipefail; cd '$(pwd)'
|
|
870
|
+
export DRY_RUN=0 MOXIE_ROOT='$MOXIE_ROOT' MOXIE_LIB='$MOXIE_LIB'
|
|
871
|
+
source '$MOXIE_LIB/platform.sh'; source '$MOXIE_LIB/core.sh'
|
|
872
|
+
source '$MOXIE_LIB/agents.sh'; source '$MOXIE_LIB/phases.sh'
|
|
873
|
+
source '$MOXIE_LIB/tokens.sh'
|
|
874
|
+
load_agents; _run_pipeline ${run_phases[*]}
|
|
875
|
+
"
|
|
876
|
+
fi
|
|
543
877
|
fi
|
|
544
878
|
}
|
|
545
879
|
|
|
@@ -547,6 +881,8 @@ cmd_run() {
|
|
|
547
881
|
|
|
548
882
|
cmd_start() {
|
|
549
883
|
require_moxie_project
|
|
884
|
+
load_agents
|
|
885
|
+
check_minimum_agents || exit 1
|
|
550
886
|
|
|
551
887
|
local target_phase=""
|
|
552
888
|
while [[ $# -gt 0 ]]; do
|
|
@@ -582,28 +918,30 @@ cmd_start() {
|
|
|
582
918
|
echo " Stop: moxie stop"
|
|
583
919
|
echo ""
|
|
584
920
|
|
|
585
|
-
# Launch in background
|
|
586
|
-
|
|
587
|
-
set -euo pipefail
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
export MOXIE_LIB='$MOXIE_LIB'
|
|
592
|
-
source '$MOXIE_LIB/core.sh'
|
|
593
|
-
source '$MOXIE_LIB/agents.sh'
|
|
594
|
-
source '$MOXIE_LIB/phases.sh'
|
|
921
|
+
# Launch in background with platform-appropriate sleep inhibitor
|
|
922
|
+
local _bg_script="
|
|
923
|
+
set -euo pipefail; cd '$(pwd)'
|
|
924
|
+
export DRY_RUN=0 MOXIE_ROOT='$MOXIE_ROOT' MOXIE_LIB='$MOXIE_LIB'
|
|
925
|
+
source '$MOXIE_LIB/platform.sh'; source '$MOXIE_LIB/core.sh'
|
|
926
|
+
source '$MOXIE_LIB/agents.sh'; source '$MOXIE_LIB/phases.sh'
|
|
595
927
|
source '$MOXIE_LIB/tokens.sh'
|
|
596
928
|
load_agents
|
|
597
|
-
|
|
598
|
-
# Determine phases
|
|
599
929
|
if [ -n '$target_phase' ]; then
|
|
600
930
|
phases=('$target_phase')
|
|
601
931
|
else
|
|
602
932
|
phases=(\${PHASES[@]})
|
|
603
933
|
fi
|
|
604
|
-
|
|
605
934
|
_run_pipeline \${phases[*]}
|
|
606
|
-
"
|
|
935
|
+
"
|
|
936
|
+
|
|
937
|
+
if [ "$MOXIE_PLATFORM" = "darwin" ]; then
|
|
938
|
+
nohup caffeinate -d -i -s bash -c "$_bg_script" > "$log_file" 2>&1 &
|
|
939
|
+
elif [ "$MOXIE_PLATFORM" = "linux" ] && command -v systemd-inhibit &>/dev/null; then
|
|
940
|
+
nohup systemd-inhibit --what=idle:sleep --who=moxie --why="moxie pipeline" \
|
|
941
|
+
bash -c "$_bg_script" > "$log_file" 2>&1 &
|
|
942
|
+
else
|
|
943
|
+
nohup bash -c "$_bg_script" > "$log_file" 2>&1 &
|
|
944
|
+
fi
|
|
607
945
|
|
|
608
946
|
local bg_pid=$!
|
|
609
947
|
echo "$bg_pid" > "$MOXIE_DIR/moxie.pid"
|
|
@@ -667,7 +1005,11 @@ _print_run_banner() {
|
|
|
667
1005
|
echo ""
|
|
668
1006
|
echo "Started: $(date)"
|
|
669
1007
|
if [ "${DRY_RUN:-0}" = "0" ]; then
|
|
670
|
-
|
|
1008
|
+
local _inhibitor
|
|
1009
|
+
_inhibitor=$(sleep_inhibit_name)
|
|
1010
|
+
if [ "$_inhibitor" != "(none)" ]; then
|
|
1011
|
+
echo "Machine will stay awake ($_inhibitor)."
|
|
1012
|
+
fi
|
|
671
1013
|
else
|
|
672
1014
|
echo "Mode: DRY RUN"
|
|
673
1015
|
fi
|
|
@@ -759,6 +1101,90 @@ print(f' {grand:,} tokens')
|
|
|
759
1101
|
" 2>/dev/null || echo " (unknown)"
|
|
760
1102
|
}
|
|
761
1103
|
|
|
1104
|
+
# ---- Convergence helpers ----
|
|
1105
|
+
|
|
1106
|
+
# Returns a stable fingerprint of the current findings in a ledger.
|
|
1107
|
+
# Extracts all INCOMPLETE/INACCURATE section labels, sorts them, and hashes.
|
|
1108
|
+
# If findings are identical across rotations, the hash won't change.
|
|
1109
|
+
_findings_fingerprint() {
|
|
1110
|
+
local ledger="$1"
|
|
1111
|
+
python3 -c "
|
|
1112
|
+
import json, hashlib
|
|
1113
|
+
with open('$ledger') as f:
|
|
1114
|
+
d = json.load(f)
|
|
1115
|
+
# Collect all finding descriptions from agent outputs
|
|
1116
|
+
findings = set()
|
|
1117
|
+
for name, info in d.get('agents', {}).items():
|
|
1118
|
+
out = info.get('output_file', '')
|
|
1119
|
+
note = info.get('notes', '')
|
|
1120
|
+
# Use notes field as findings summary (agents write their review status here)
|
|
1121
|
+
if note:
|
|
1122
|
+
findings.add(note[:200])
|
|
1123
|
+
# Also check the top-level findings array if present
|
|
1124
|
+
for f in d.get('findings', []):
|
|
1125
|
+
if isinstance(f, str):
|
|
1126
|
+
findings.add(f[:200])
|
|
1127
|
+
elif isinstance(f, dict):
|
|
1128
|
+
findings.add(str(f.get('section', '')) + ':' + str(f.get('status', '')))
|
|
1129
|
+
# Stable fingerprint: sorted findings hashed
|
|
1130
|
+
key = '|'.join(sorted(findings))
|
|
1131
|
+
print(hashlib.md5(key.encode()).hexdigest() if key else '')
|
|
1132
|
+
" 2>/dev/null
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
# Force-accept the best available draft as FINAL for a phase.
|
|
1136
|
+
# Picks the most recent .md file (by filename timestamp) from any agent.
|
|
1137
|
+
_force_accept_best_draft() {
|
|
1138
|
+
local phase_dir="$1"
|
|
1139
|
+
local final_prefix="$2"
|
|
1140
|
+
local ledger="$3"
|
|
1141
|
+
|
|
1142
|
+
# Find the most recent agent draft (not a FINAL file)
|
|
1143
|
+
local best=""
|
|
1144
|
+
for f in "$phase_dir"/*.md; do
|
|
1145
|
+
[ ! -f "$f" ] && continue
|
|
1146
|
+
case "$(basename "$f")" in
|
|
1147
|
+
${final_prefix}-*) continue ;; # skip existing FINAL files
|
|
1148
|
+
esac
|
|
1149
|
+
# Pick the one with the latest filename (timestamps sort lexically)
|
|
1150
|
+
if [ -z "$best" ] || [[ "$(basename "$f")" > "$(basename "$best")" ]]; then
|
|
1151
|
+
best="$f"
|
|
1152
|
+
fi
|
|
1153
|
+
done
|
|
1154
|
+
|
|
1155
|
+
if [ -z "$best" ]; then
|
|
1156
|
+
echo " No draft files found to accept." >&2
|
|
1157
|
+
return 1
|
|
1158
|
+
fi
|
|
1159
|
+
|
|
1160
|
+
local ts
|
|
1161
|
+
ts=$(date +"%m%d%y-%H%M%S")
|
|
1162
|
+
local final_file="$phase_dir/${final_prefix}-${ts}.md"
|
|
1163
|
+
|
|
1164
|
+
cp "$best" "$final_file"
|
|
1165
|
+
|
|
1166
|
+
# Also copy to spec.md if this is the RFC phase
|
|
1167
|
+
if [ "$final_prefix" = "RFC-FINAL" ]; then
|
|
1168
|
+
cp "$final_file" "$MOXIE_DIR/spec.md"
|
|
1169
|
+
fi
|
|
1170
|
+
|
|
1171
|
+
# Mark all healthy agents as reached in the ledger
|
|
1172
|
+
python3 -c "
|
|
1173
|
+
import json
|
|
1174
|
+
with open('$ledger') as f:
|
|
1175
|
+
d = json.load(f)
|
|
1176
|
+
for name in d.get('agents', {}):
|
|
1177
|
+
d['agents'][name]['reached'] = True
|
|
1178
|
+
d['status'] = '$(echo "$final_prefix" | tr '[:upper:]' '[:lower:]' | tr '-' '_')'
|
|
1179
|
+
with open('$ledger', 'w') as f:
|
|
1180
|
+
json.dump(d, f, indent=2)
|
|
1181
|
+
" 2>/dev/null
|
|
1182
|
+
|
|
1183
|
+
echo " Accepted: $(basename "$best") → $(basename "$final_file")"
|
|
1184
|
+
echo ""
|
|
1185
|
+
_show_quorum "$ledger"
|
|
1186
|
+
}
|
|
1187
|
+
|
|
762
1188
|
# ---- Phase execution ----
|
|
763
1189
|
|
|
764
1190
|
_run_phase() {
|
|
@@ -778,11 +1204,19 @@ _run_phase() {
|
|
|
778
1204
|
|
|
779
1205
|
# Randomize rotation order for this phase
|
|
780
1206
|
_shuffle_agents
|
|
1207
|
+
_init_agent_health
|
|
781
1208
|
echo "Rotation order for $(phase_label "$phase"): ${AGENT_NAMES[*]}"
|
|
782
1209
|
echo ""
|
|
783
1210
|
|
|
784
1211
|
banner "moxie: $(phase_label "$phase") phase" "$max_rounds" "$turn_timeout" "${DRY_RUN:-0}"
|
|
785
1212
|
|
|
1213
|
+
# Convergence detection: track findings fingerprints across rounds.
|
|
1214
|
+
# If the same findings repeat for STALE_THRESHOLD consecutive full rotations
|
|
1215
|
+
# (each agent gets a turn), force-accept the best draft.
|
|
1216
|
+
local STALE_THRESHOLD=3
|
|
1217
|
+
local stale_count=0
|
|
1218
|
+
local prev_fingerprint=""
|
|
1219
|
+
|
|
786
1220
|
# Show initial ledger
|
|
787
1221
|
echo "Quorum status:"
|
|
788
1222
|
_show_quorum "$ledger"
|
|
@@ -791,27 +1225,43 @@ _run_phase() {
|
|
|
791
1225
|
local current="${AGENT_NAMES[0]}"
|
|
792
1226
|
|
|
793
1227
|
for round in $(seq 1 "$max_rounds"); do
|
|
1228
|
+
# Skip degraded agents
|
|
1229
|
+
local skipped=0
|
|
1230
|
+
while is_agent_degraded "$current"; do
|
|
1231
|
+
current=$(next_agent "$current")
|
|
1232
|
+
skipped=$((skipped + 1))
|
|
1233
|
+
# Safety: don't infinite-loop if all agents are degraded
|
|
1234
|
+
if [ "$skipped" -ge "$AGENT_COUNT" ]; then
|
|
1235
|
+
echo ""
|
|
1236
|
+
echo "FATAL: All agents are degraded. Pipeline cannot continue."
|
|
1237
|
+
_show_quorum "$ledger"
|
|
1238
|
+
return 1
|
|
1239
|
+
fi
|
|
1240
|
+
done
|
|
1241
|
+
|
|
794
1242
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
795
1243
|
echo " Turn $round/$max_rounds — $current"
|
|
796
1244
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
797
1245
|
|
|
798
|
-
# Check if FINAL already exists
|
|
1246
|
+
# Check if FINAL already exists AND healthy agents have all reached quorum
|
|
799
1247
|
if ls "$phase_dir"/${final_prefix}-*.md &>/dev/null; then
|
|
800
|
-
if
|
|
1248
|
+
if _healthy_agents_reached "$ledger"; then
|
|
801
1249
|
local final
|
|
802
1250
|
final=$(ls -1 "$phase_dir"/${final_prefix}-*.md | tail -1)
|
|
1251
|
+
local healthy
|
|
1252
|
+
healthy=$(count_healthy_agents)
|
|
803
1253
|
echo ""
|
|
804
1254
|
echo "╔════════════════════════════════════════════════════════════╗"
|
|
805
|
-
printf "║ %-58s║\n" "$(phase_label "$phase") phase complete —
|
|
1255
|
+
printf "║ %-58s║\n" "$(phase_label "$phase") phase complete — quorum reached"
|
|
806
1256
|
printf "║ Final: %-52s║\n" "$(basename "$final")"
|
|
807
|
-
printf "║
|
|
1257
|
+
printf "║ %d/%d agents signed off ║\n" "$healthy" "$AGENT_COUNT"
|
|
808
1258
|
echo "╚════════════════════════════════════════════════════════════╝"
|
|
809
1259
|
echo ""
|
|
810
1260
|
show_phase_tokens "$csv"
|
|
811
1261
|
return 0
|
|
812
1262
|
else
|
|
813
|
-
# FINAL file exists but not all agents reached — keep going
|
|
814
|
-
echo " FINAL file exists but not all agents have signed off yet."
|
|
1263
|
+
# FINAL file exists but not all healthy agents reached — keep going
|
|
1264
|
+
echo " FINAL file exists but not all healthy agents have signed off yet."
|
|
815
1265
|
echo ""
|
|
816
1266
|
fi
|
|
817
1267
|
fi
|
|
@@ -825,39 +1275,83 @@ _run_phase() {
|
|
|
825
1275
|
|
|
826
1276
|
# Run
|
|
827
1277
|
dispatch_logged "$current" "$prompt_file" "$turn_timeout" "$log_dir" "$round" "$csv" "$phase"
|
|
1278
|
+
local dispatch_rc=$?
|
|
828
1279
|
rm -f "$prompt_file"
|
|
829
1280
|
|
|
1281
|
+
# If _record_turn_health returned 2 (fatal: < 2 agents remaining), stop
|
|
1282
|
+
if [ "$dispatch_rc" = "2" ] || [ "$(count_healthy_agents)" -lt 2 ]; then
|
|
1283
|
+
echo ""
|
|
1284
|
+
echo "FATAL: Fewer than 2 healthy agents remaining. Pipeline cannot continue."
|
|
1285
|
+
_show_quorum "$ledger"
|
|
1286
|
+
return 1
|
|
1287
|
+
fi
|
|
1288
|
+
|
|
830
1289
|
echo ""
|
|
831
1290
|
echo "Quorum after $current's turn:"
|
|
832
1291
|
_show_quorum "$ledger"
|
|
833
1292
|
|
|
834
|
-
# Check
|
|
835
|
-
if
|
|
1293
|
+
# Check quorum among healthy agents after this turn
|
|
1294
|
+
if _healthy_agents_reached "$ledger"; then
|
|
836
1295
|
# Verify FINAL file was actually written by the agent
|
|
837
1296
|
if ls "$phase_dir"/${final_prefix}-*.md &>/dev/null; then
|
|
838
1297
|
local final
|
|
839
1298
|
final=$(ls -1 "$phase_dir"/${final_prefix}-*.md | tail -1)
|
|
1299
|
+
local healthy
|
|
1300
|
+
healthy=$(count_healthy_agents)
|
|
840
1301
|
echo ""
|
|
841
1302
|
echo "╔════════════════════════════════════════════════════════════╗"
|
|
842
|
-
printf "║ %-58s║\n" "$(phase_label "$phase") phase complete —
|
|
1303
|
+
printf "║ %-58s║\n" "$(phase_label "$phase") phase complete — quorum reached"
|
|
843
1304
|
printf "║ Final: %-52s║\n" "$(basename "$final")"
|
|
844
|
-
printf "║
|
|
1305
|
+
printf "║ %d/%d agents signed off ║\n" "$healthy" "$AGENT_COUNT"
|
|
845
1306
|
echo "╚════════════════════════════════════════════════════════════╝"
|
|
846
1307
|
echo ""
|
|
847
1308
|
show_phase_tokens "$csv"
|
|
848
1309
|
return 0
|
|
849
1310
|
else
|
|
850
|
-
echo " All agents reached quorum but FINAL file not yet written."
|
|
1311
|
+
echo " All healthy agents reached quorum but FINAL file not yet written."
|
|
851
1312
|
echo " Next agent will write it."
|
|
852
1313
|
fi
|
|
853
1314
|
fi
|
|
854
1315
|
|
|
1316
|
+
# ---- Convergence detection ----
|
|
1317
|
+
# After each full rotation (every AGENT_COUNT turns), fingerprint the
|
|
1318
|
+
# findings. If findings stabilize for STALE_THRESHOLD rotations, the
|
|
1319
|
+
# agents are in a perfectionist loop — force-accept the best draft.
|
|
1320
|
+
if [ $(( round % $(count_healthy_agents) )) -eq 0 ]; then
|
|
1321
|
+
local fingerprint
|
|
1322
|
+
fingerprint=$(_findings_fingerprint "$ledger")
|
|
1323
|
+
|
|
1324
|
+
if [ "$fingerprint" = "$prev_fingerprint" ] && [ -n "$fingerprint" ]; then
|
|
1325
|
+
stale_count=$((stale_count + 1))
|
|
1326
|
+
echo " [convergence] Findings unchanged for $stale_count rotation(s) ($STALE_THRESHOLD triggers force-accept)"
|
|
1327
|
+
else
|
|
1328
|
+
stale_count=0
|
|
1329
|
+
prev_fingerprint="$fingerprint"
|
|
1330
|
+
fi
|
|
1331
|
+
|
|
1332
|
+
if [ "$stale_count" -ge "$STALE_THRESHOLD" ]; then
|
|
1333
|
+
echo ""
|
|
1334
|
+
echo "╔════════════════════════════════════════════════════════════╗"
|
|
1335
|
+
printf "║ %-58s║\n" "$(phase_label "$phase") — force-accepting (convergence stall)"
|
|
1336
|
+
echo "║ Findings stabilized for $STALE_THRESHOLD rotations. ║"
|
|
1337
|
+
echo "║ Accepting best available draft as FINAL. ║"
|
|
1338
|
+
echo "╚════════════════════════════════════════════════════════════╝"
|
|
1339
|
+
echo ""
|
|
1340
|
+
_force_accept_best_draft "$phase_dir" "$final_prefix" "$ledger"
|
|
1341
|
+
show_phase_tokens "$csv"
|
|
1342
|
+
return 0
|
|
1343
|
+
fi
|
|
1344
|
+
fi
|
|
1345
|
+
|
|
855
1346
|
echo ""
|
|
856
1347
|
current=$(next_agent "$current")
|
|
857
1348
|
done
|
|
858
1349
|
|
|
1350
|
+
# If max rounds exhausted, force-accept rather than failing
|
|
859
1351
|
echo ""
|
|
860
|
-
echo "WARNING: $(phase_label "$phase") phase reached max rounds ($max_rounds) without
|
|
1352
|
+
echo "WARNING: $(phase_label "$phase") phase reached max rounds ($max_rounds) without quorum."
|
|
1353
|
+
echo "Force-accepting best available draft."
|
|
1354
|
+
_force_accept_best_draft "$phase_dir" "$final_prefix" "$ledger"
|
|
861
1355
|
_show_quorum "$ledger"
|
|
862
1356
|
echo ""
|
|
863
1357
|
show_phase_tokens "$csv"
|