loki-mode 5.59.0 → 6.0.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +34 -9
- package/autonomy/issue-providers.sh +423 -0
- package/autonomy/loki +859 -19
- package/autonomy/run.sh +320 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +7 -5
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/providers/claude.sh +52 -2
- package/providers/codex.sh +39 -4
- package/providers/gemini.sh +44 -3
package/autonomy/loki
CHANGED
|
@@ -382,11 +382,14 @@ show_help() {
|
|
|
382
382
|
echo "Usage: loki <command> [options]"
|
|
383
383
|
echo ""
|
|
384
384
|
echo "Commands:"
|
|
385
|
+
echo " run <issue> Issue-driven engineering (v6.0.0) - GitHub/GitLab/Jira/Azure DevOps"
|
|
385
386
|
echo " start [PRD] Start Loki Mode (optionally with PRD file)"
|
|
386
387
|
echo " quick \"task\" Quick single-task mode (lightweight, 3 iterations max)"
|
|
387
388
|
echo " demo Run interactive demo (~60s simulated session)"
|
|
388
389
|
echo " init Build a PRD interactively or from templates"
|
|
389
|
-
echo " issue <url|num>
|
|
390
|
+
echo " issue <url|num> [DEPRECATED] Use 'loki run' instead"
|
|
391
|
+
echo " watch [sec] Live TUI session monitor (v6.0.0)"
|
|
392
|
+
echo " export <format> Export session data: json|markdown|csv|timeline (v6.0.0)"
|
|
390
393
|
echo " stop Stop execution immediately"
|
|
391
394
|
echo " cleanup Kill orphaned processes from crashed sessions"
|
|
392
395
|
echo " pause Pause after current session"
|
|
@@ -402,7 +405,7 @@ show_help() {
|
|
|
402
405
|
echo " voice [cmd] Voice input for PRD creation (status|listen|dictate|speak|start)"
|
|
403
406
|
echo " import Import GitHub issues as tasks"
|
|
404
407
|
echo " github [cmd] GitHub integration (sync|export|pr|status)"
|
|
405
|
-
echo " config [cmd] Manage configuration (show|init|edit|path)"
|
|
408
|
+
echo " config [cmd] Manage configuration (show|init|edit|path|set|get)"
|
|
406
409
|
echo " completions [bash|zsh] Output shell completion scripts"
|
|
407
410
|
echo " memory [cmd] Cross-project learnings (list|show|search|stats)"
|
|
408
411
|
echo " compound [cmd] Knowledge compounding (list|show|search|run|stats)"
|
|
@@ -436,14 +439,18 @@ show_help() {
|
|
|
436
439
|
echo " --compliance PRESET Enable compliance mode (default|healthcare|fintech|government)"
|
|
437
440
|
echo " --budget USD Set cost budget limit (display in dashboard/status)"
|
|
438
441
|
echo ""
|
|
439
|
-
echo "Options for '
|
|
440
|
-
echo " --
|
|
441
|
-
echo " --
|
|
442
|
-
echo " --
|
|
443
|
-
echo " --
|
|
444
|
-
echo " --
|
|
442
|
+
echo "Options for 'run' (v6.0.0):"
|
|
443
|
+
echo " --dry-run Preview generated PRD without starting"
|
|
444
|
+
echo " --no-start Generate PRD but don't start execution"
|
|
445
|
+
echo " --output FILE Save PRD to custom path"
|
|
446
|
+
echo " --provider NAME AI provider: claude (default), codex, gemini"
|
|
447
|
+
echo " --parallel Enable parallel mode with git worktrees"
|
|
448
|
+
echo " --budget USD Set cost budget limit"
|
|
445
449
|
echo ""
|
|
446
450
|
echo "Examples:"
|
|
451
|
+
echo " loki run 123 # GitHub issue from current repo"
|
|
452
|
+
echo " loki run PROJ-456 # Jira issue"
|
|
453
|
+
echo " loki run owner/repo#789 # GitHub with specific repo"
|
|
447
454
|
echo " loki demo # Run 60-second interactive demo"
|
|
448
455
|
echo " loki init # Build a PRD interactively"
|
|
449
456
|
echo " loki init -t saas-starter # Start from a template"
|
|
@@ -451,13 +458,11 @@ show_help() {
|
|
|
451
458
|
echo " loki start ./prd.md # Start with PRD file"
|
|
452
459
|
echo " loki start --bg # Start in background"
|
|
453
460
|
echo " loki start --parallel # Start in parallel mode"
|
|
454
|
-
echo " loki
|
|
461
|
+
echo " loki watch # Live session monitor"
|
|
462
|
+
echo " loki export json # Export session data"
|
|
463
|
+
echo " loki config set maxTier sonnet # Cap model cost"
|
|
455
464
|
echo " loki status # Check current status"
|
|
456
|
-
echo " loki issue 123 # Generate PRD from issue #123"
|
|
457
|
-
echo " loki issue 123 --start # Generate PRD and start Loki Mode"
|
|
458
|
-
echo " loki issue https://github.com/owner/repo/issues/123 # From URL"
|
|
459
465
|
echo " loki remote # Start remote session (phone/browser)"
|
|
460
|
-
echo " loki remote ./prd.md # Remote session with PRD context"
|
|
461
466
|
echo ""
|
|
462
467
|
echo "Environment Variables:"
|
|
463
468
|
echo " See: $RUN_SH (header comments)"
|
|
@@ -2267,6 +2272,10 @@ cmd_issue_parse() {
|
|
|
2267
2272
|
;;
|
|
2268
2273
|
--output=*)
|
|
2269
2274
|
output_file="${1#*=}"
|
|
2275
|
+
if [[ "$output_file" == *".."* ]]; then
|
|
2276
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
2277
|
+
exit 1
|
|
2278
|
+
fi
|
|
2270
2279
|
shift
|
|
2271
2280
|
;;
|
|
2272
2281
|
--quiet|-q)
|
|
@@ -2417,8 +2426,240 @@ cmd_issue_view() {
|
|
|
2417
2426
|
echo -e " ${CYAN}loki issue $issue_ref --start${NC} # Generate PRD and start"
|
|
2418
2427
|
}
|
|
2419
2428
|
|
|
2420
|
-
|
|
2429
|
+
#===============================================================================
|
|
2430
|
+
# loki run - Issue-driven engineering (v6.0.0)
|
|
2431
|
+
# Primary entry point for issue-to-implementation workflow.
|
|
2432
|
+
# Supports GitHub, GitLab, Jira, and Azure DevOps issues.
|
|
2433
|
+
#===============================================================================
|
|
2434
|
+
|
|
2435
|
+
cmd_run() {
|
|
2436
|
+
require_jq
|
|
2437
|
+
|
|
2438
|
+
local issue_ref=""
|
|
2439
|
+
local dry_run=false
|
|
2440
|
+
local output_file=""
|
|
2441
|
+
local start_args=()
|
|
2442
|
+
local no_start=false
|
|
2443
|
+
local provider_override=""
|
|
2444
|
+
|
|
2445
|
+
# Parse arguments
|
|
2446
|
+
while [[ $# -gt 0 ]]; do
|
|
2447
|
+
case "$1" in
|
|
2448
|
+
--help|-h)
|
|
2449
|
+
echo -e "${BOLD}loki run${NC} - Issue-driven engineering (v6.0.0)"
|
|
2450
|
+
echo ""
|
|
2451
|
+
echo "Usage: loki run <issue-ref> [options]"
|
|
2452
|
+
echo " loki run <url-or-key> [options]"
|
|
2453
|
+
echo ""
|
|
2454
|
+
echo "Takes an issue from any supported tracker, generates a PRD,"
|
|
2455
|
+
echo "and starts Loki Mode to implement it autonomously."
|
|
2456
|
+
echo ""
|
|
2457
|
+
echo "Issue Reference Formats:"
|
|
2458
|
+
echo " GitHub: 123, #123, owner/repo#123, https://github.com/..."
|
|
2459
|
+
echo " GitLab: https://gitlab.com/owner/repo/-/issues/42"
|
|
2460
|
+
echo " Jira: PROJ-123, https://org.atlassian.net/browse/PROJ-123"
|
|
2461
|
+
echo " Azure DevOps: https://dev.azure.com/org/project/_workitems/edit/456"
|
|
2462
|
+
echo ""
|
|
2463
|
+
echo "Options:"
|
|
2464
|
+
echo " --dry-run Preview generated PRD without starting"
|
|
2465
|
+
echo " --no-start Generate PRD but don't start execution"
|
|
2466
|
+
echo " --output FILE Save PRD to custom path"
|
|
2467
|
+
echo " --provider NAME AI provider: claude (default), codex, gemini"
|
|
2468
|
+
echo " --parallel Enable parallel mode with git worktrees"
|
|
2469
|
+
echo " --bg, --background Run in background mode"
|
|
2470
|
+
echo " --simple Force simple complexity tier"
|
|
2471
|
+
echo " --complex Force complex complexity tier"
|
|
2472
|
+
echo " --no-dashboard Disable web dashboard"
|
|
2473
|
+
echo " --sandbox Run in Docker sandbox"
|
|
2474
|
+
echo " --budget USD Set cost budget limit"
|
|
2475
|
+
echo ""
|
|
2476
|
+
echo "Environment Variables:"
|
|
2477
|
+
echo " JIRA_API_TOKEN Jira API token (for Jira issues)"
|
|
2478
|
+
echo " JIRA_URL Jira base URL (for Jira issues)"
|
|
2479
|
+
echo " JIRA_EMAIL Jira user email (for Jira Cloud auth)"
|
|
2480
|
+
echo ""
|
|
2481
|
+
echo "Examples:"
|
|
2482
|
+
echo " loki run 123 # GitHub issue from current repo"
|
|
2483
|
+
echo " loki run owner/repo#456 # GitHub issue from specific repo"
|
|
2484
|
+
echo " loki run PROJ-789 # Jira issue"
|
|
2485
|
+
echo " loki run https://gitlab.com/o/r/-/issues/42 # GitLab issue"
|
|
2486
|
+
echo " loki run 123 --dry-run # Preview PRD"
|
|
2487
|
+
echo " loki run 123 --parallel --provider codex # Parallel mode with Codex"
|
|
2488
|
+
exit 0
|
|
2489
|
+
;;
|
|
2490
|
+
--dry-run)
|
|
2491
|
+
dry_run=true
|
|
2492
|
+
shift
|
|
2493
|
+
;;
|
|
2494
|
+
--no-start)
|
|
2495
|
+
no_start=true
|
|
2496
|
+
shift
|
|
2497
|
+
;;
|
|
2498
|
+
--output)
|
|
2499
|
+
if [[ -n "${2:-}" ]]; then
|
|
2500
|
+
output_file="$2"
|
|
2501
|
+
if [[ "$output_file" == *".."* ]]; then
|
|
2502
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
2503
|
+
exit 1
|
|
2504
|
+
fi
|
|
2505
|
+
shift 2
|
|
2506
|
+
else
|
|
2507
|
+
echo -e "${RED}--output requires a file path${NC}"
|
|
2508
|
+
exit 1
|
|
2509
|
+
fi
|
|
2510
|
+
;;
|
|
2511
|
+
--output=*)
|
|
2512
|
+
output_file="${1#*=}"
|
|
2513
|
+
if [[ "$output_file" == *".."* ]]; then
|
|
2514
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
2515
|
+
exit 1
|
|
2516
|
+
fi
|
|
2517
|
+
shift
|
|
2518
|
+
;;
|
|
2519
|
+
--provider)
|
|
2520
|
+
if [[ -n "${2:-}" ]]; then
|
|
2521
|
+
provider_override="$2"
|
|
2522
|
+
start_args+=("--provider" "$2")
|
|
2523
|
+
shift 2
|
|
2524
|
+
else
|
|
2525
|
+
echo -e "${RED}--provider requires a value${NC}"
|
|
2526
|
+
exit 1
|
|
2527
|
+
fi
|
|
2528
|
+
;;
|
|
2529
|
+
--provider=*)
|
|
2530
|
+
provider_override="${1#*=}"
|
|
2531
|
+
start_args+=("--provider" "$provider_override")
|
|
2532
|
+
shift
|
|
2533
|
+
;;
|
|
2534
|
+
--parallel|--bg|--background|--simple|--complex|--no-dashboard|--sandbox)
|
|
2535
|
+
start_args+=("$1")
|
|
2536
|
+
shift
|
|
2537
|
+
;;
|
|
2538
|
+
--budget)
|
|
2539
|
+
if [[ -n "${2:-}" ]]; then
|
|
2540
|
+
start_args+=("--budget" "$2")
|
|
2541
|
+
shift 2
|
|
2542
|
+
else
|
|
2543
|
+
echo -e "${RED}--budget requires a USD amount${NC}"
|
|
2544
|
+
exit 1
|
|
2545
|
+
fi
|
|
2546
|
+
;;
|
|
2547
|
+
--budget=*)
|
|
2548
|
+
start_args+=("--budget" "${1#*=}")
|
|
2549
|
+
shift
|
|
2550
|
+
;;
|
|
2551
|
+
-*)
|
|
2552
|
+
echo -e "${RED}Unknown option: $1${NC}"
|
|
2553
|
+
echo "Run 'loki run --help' for usage."
|
|
2554
|
+
exit 1
|
|
2555
|
+
;;
|
|
2556
|
+
*)
|
|
2557
|
+
if [[ -z "$issue_ref" ]]; then
|
|
2558
|
+
issue_ref="$1"
|
|
2559
|
+
fi
|
|
2560
|
+
shift
|
|
2561
|
+
;;
|
|
2562
|
+
esac
|
|
2563
|
+
done
|
|
2564
|
+
|
|
2565
|
+
if [[ -z "$issue_ref" ]]; then
|
|
2566
|
+
echo -e "${RED}Error: Issue reference required${NC}"
|
|
2567
|
+
echo ""
|
|
2568
|
+
echo "Usage: loki run <issue-ref> [options]"
|
|
2569
|
+
echo ""
|
|
2570
|
+
echo "Examples:"
|
|
2571
|
+
echo " loki run 123 # GitHub issue"
|
|
2572
|
+
echo " loki run PROJ-456 # Jira issue"
|
|
2573
|
+
echo " loki run owner/repo#789 # GitHub with specific repo"
|
|
2574
|
+
echo ""
|
|
2575
|
+
echo "Run 'loki run --help' for full usage."
|
|
2576
|
+
exit 1
|
|
2577
|
+
fi
|
|
2578
|
+
|
|
2579
|
+
# Source issue provider abstraction
|
|
2580
|
+
local issue_providers_script="$SKILL_DIR/autonomy/issue-providers.sh"
|
|
2581
|
+
if [[ ! -f "$issue_providers_script" ]]; then
|
|
2582
|
+
echo -e "${RED}Error: issue-providers.sh not found at $issue_providers_script${NC}"
|
|
2583
|
+
exit 1
|
|
2584
|
+
fi
|
|
2585
|
+
source "$issue_providers_script"
|
|
2586
|
+
|
|
2587
|
+
# Detect provider
|
|
2588
|
+
local issue_provider
|
|
2589
|
+
issue_provider=$(detect_issue_provider "$issue_ref")
|
|
2590
|
+
echo -e "${CYAN}Issue provider:${NC} ${issue_provider}"
|
|
2591
|
+
|
|
2592
|
+
# Fetch the issue
|
|
2593
|
+
echo -e "${CYAN}Fetching issue...${NC}"
|
|
2594
|
+
local issue_json
|
|
2595
|
+
issue_json=$(fetch_issue "$issue_ref") || {
|
|
2596
|
+
echo -e "${RED}Failed to fetch issue${NC}"
|
|
2597
|
+
exit 1
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
# Extract fields for display
|
|
2601
|
+
local title number url
|
|
2602
|
+
title=$(echo "$issue_json" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('title',''))")
|
|
2603
|
+
number=$(echo "$issue_json" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('number',''))")
|
|
2604
|
+
url=$(echo "$issue_json" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('url',''))")
|
|
2605
|
+
|
|
2606
|
+
echo -e "${GREEN}Issue #$number:${NC} $title"
|
|
2607
|
+
if [[ -n "$url" ]]; then
|
|
2608
|
+
echo -e "${DIM}$url${NC}"
|
|
2609
|
+
fi
|
|
2610
|
+
echo ""
|
|
2611
|
+
|
|
2612
|
+
# Generate PRD
|
|
2613
|
+
local prd_content
|
|
2614
|
+
prd_content=$(echo "$issue_json" | generate_prd_from_issue)
|
|
2615
|
+
|
|
2616
|
+
# Handle dry-run
|
|
2617
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
2618
|
+
echo -e "${BOLD}Generated PRD Preview:${NC}"
|
|
2619
|
+
echo "------------------------------------------------------------------------"
|
|
2620
|
+
echo "$prd_content"
|
|
2621
|
+
echo "------------------------------------------------------------------------"
|
|
2622
|
+
echo ""
|
|
2623
|
+
echo -e "${DIM}(dry-run mode - PRD not saved)${NC}"
|
|
2624
|
+
exit 0
|
|
2625
|
+
fi
|
|
2626
|
+
|
|
2627
|
+
# Determine output file
|
|
2628
|
+
if [[ -z "$output_file" ]]; then
|
|
2629
|
+
mkdir -p "$LOKI_DIR"
|
|
2630
|
+
# Use provider-specific naming
|
|
2631
|
+
case "$issue_provider" in
|
|
2632
|
+
github|gitlab) output_file="$LOKI_DIR/prd-issue-$number.md" ;;
|
|
2633
|
+
jira) output_file="$LOKI_DIR/prd-$number.md" ;;
|
|
2634
|
+
azure_devops) output_file="$LOKI_DIR/prd-ado-$number.md" ;;
|
|
2635
|
+
*) output_file="$LOKI_DIR/prd-issue-$number.md" ;;
|
|
2636
|
+
esac
|
|
2637
|
+
fi
|
|
2638
|
+
|
|
2639
|
+
# Write PRD file
|
|
2640
|
+
echo "$prd_content" > "$output_file"
|
|
2641
|
+
echo -e "${GREEN}PRD generated:${NC} $output_file"
|
|
2642
|
+
|
|
2643
|
+
# Start Loki Mode unless --no-start
|
|
2644
|
+
if [[ "$no_start" == "true" ]]; then
|
|
2645
|
+
echo ""
|
|
2646
|
+
echo "Next steps:"
|
|
2647
|
+
echo -e " ${CYAN}loki start $output_file${NC} # Start with generated PRD"
|
|
2648
|
+
echo -e " ${CYAN}cat $output_file${NC} # View PRD"
|
|
2649
|
+
else
|
|
2650
|
+
echo ""
|
|
2651
|
+
echo -e "${GREEN}Starting Loki Mode with generated PRD...${NC}"
|
|
2652
|
+
cmd_start "$output_file" ${start_args[@]+"${start_args[@]}"}
|
|
2653
|
+
fi
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
# Generate PRD from GitHub issue (DEPRECATED in v6.0.0 - use 'loki run' instead)
|
|
2421
2657
|
cmd_issue() {
|
|
2658
|
+
# v6.0.0: Show deprecation notice
|
|
2659
|
+
echo -e "${YELLOW}[DEPRECATED] 'loki issue' is deprecated in v6.0.0. Use 'loki run' instead.${NC}" >&2
|
|
2660
|
+
echo -e "${YELLOW} 'loki run' supports GitHub, GitLab, Jira, and Azure DevOps issues.${NC}" >&2
|
|
2661
|
+
echo "" >&2
|
|
2662
|
+
|
|
2422
2663
|
require_jq
|
|
2423
2664
|
|
|
2424
2665
|
local issue_ref=""
|
|
@@ -2551,6 +2792,10 @@ cmd_issue() {
|
|
|
2551
2792
|
--output)
|
|
2552
2793
|
if [[ -n "${2:-}" ]]; then
|
|
2553
2794
|
output_file="$2"
|
|
2795
|
+
if [[ "$output_file" == *".."* ]]; then
|
|
2796
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
2797
|
+
exit 1
|
|
2798
|
+
fi
|
|
2554
2799
|
shift 2
|
|
2555
2800
|
else
|
|
2556
2801
|
echo -e "${RED}--output requires a file path${NC}"
|
|
@@ -2559,6 +2804,10 @@ cmd_issue() {
|
|
|
2559
2804
|
;;
|
|
2560
2805
|
--output=*)
|
|
2561
2806
|
output_file="${1#*=}"
|
|
2807
|
+
if [[ "$output_file" == *".."* ]]; then
|
|
2808
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
2809
|
+
exit 1
|
|
2810
|
+
fi
|
|
2562
2811
|
shift
|
|
2563
2812
|
;;
|
|
2564
2813
|
# Pass through options to start command
|
|
@@ -2743,8 +2992,407 @@ EOF
|
|
|
2743
2992
|
}
|
|
2744
2993
|
|
|
2745
2994
|
# Show configuration
|
|
2995
|
+
#===============================================================================
|
|
2996
|
+
# loki watch - Live TUI session monitor (v6.0.0)
|
|
2997
|
+
#===============================================================================
|
|
2998
|
+
|
|
2999
|
+
cmd_watch() {
|
|
3000
|
+
local interval="${1:-2}"
|
|
3001
|
+
|
|
3002
|
+
# Handle --help
|
|
3003
|
+
if [[ "$interval" == "--help" || "$interval" == "-h" ]]; then
|
|
3004
|
+
# fall through to help block
|
|
3005
|
+
:
|
|
3006
|
+
elif ! [[ "$interval" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
|
3007
|
+
echo -e "${RED}Invalid interval: $interval (expected positive number)${NC}"
|
|
3008
|
+
return 1
|
|
3009
|
+
elif [[ "$interval" =~ ^0+(\.0+)?$ ]]; then
|
|
3010
|
+
echo -e "${RED}Invalid interval: $interval (must be greater than 0)${NC}"
|
|
3011
|
+
return 1
|
|
3012
|
+
fi
|
|
3013
|
+
if [[ "$interval" == "--help" || "$interval" == "-h" ]]; then
|
|
3014
|
+
echo -e "${BOLD}loki watch${NC} - Live session monitor (v6.0.0)"
|
|
3015
|
+
echo ""
|
|
3016
|
+
echo "Usage: loki watch [interval]"
|
|
3017
|
+
echo ""
|
|
3018
|
+
echo "Options:"
|
|
3019
|
+
echo " interval Refresh interval in seconds (default: 2)"
|
|
3020
|
+
echo ""
|
|
3021
|
+
echo "Displays:"
|
|
3022
|
+
echo " - Current iteration and RARV phase"
|
|
3023
|
+
echo " - Provider and model tier"
|
|
3024
|
+
echo " - Task queue status"
|
|
3025
|
+
echo " - Recent log lines"
|
|
3026
|
+
echo " - Cost tracking"
|
|
3027
|
+
echo ""
|
|
3028
|
+
echo "Press Ctrl+C to exit."
|
|
3029
|
+
exit 0
|
|
3030
|
+
fi
|
|
3031
|
+
|
|
3032
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
3033
|
+
echo -e "${RED}No active session found.${NC}"
|
|
3034
|
+
echo "Start a session with: loki start <prd>"
|
|
3035
|
+
exit 1
|
|
3036
|
+
fi
|
|
3037
|
+
|
|
3038
|
+
echo -e "${BOLD}Loki Mode Watch${NC} (refresh: ${interval}s, Ctrl+C to exit)"
|
|
3039
|
+
echo ""
|
|
3040
|
+
|
|
3041
|
+
trap 'echo ""; echo "Watch stopped."; return 0' INT TERM
|
|
3042
|
+
|
|
3043
|
+
while true; do
|
|
3044
|
+
clear
|
|
3045
|
+
echo -e "${BOLD}Loki Mode Watch${NC} $(date '+%H:%M:%S') (refresh: ${interval}s)"
|
|
3046
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
3047
|
+
|
|
3048
|
+
# Session status
|
|
3049
|
+
if [ -f "$LOKI_DIR/PAUSE" ]; then
|
|
3050
|
+
echo -e "Status: ${YELLOW}PAUSED${NC}"
|
|
3051
|
+
elif [ -f "$LOKI_DIR/STOP" ]; then
|
|
3052
|
+
echo -e "Status: ${RED}STOPPED${NC}"
|
|
3053
|
+
elif [ -f "$LOKI_DIR/loki.pid" ] && kill -0 "$(cat "$LOKI_DIR/loki.pid" 2>/dev/null)" 2>/dev/null; then
|
|
3054
|
+
echo -e "Status: ${GREEN}RUNNING${NC}"
|
|
3055
|
+
else
|
|
3056
|
+
echo -e "Status: ${DIM}IDLE${NC}"
|
|
3057
|
+
fi
|
|
3058
|
+
|
|
3059
|
+
# Iteration and phase
|
|
3060
|
+
if [ -f "$LOKI_DIR/state/iteration" ]; then
|
|
3061
|
+
local iter
|
|
3062
|
+
iter=$(cat "$LOKI_DIR/state/iteration" 2>/dev/null || echo "?")
|
|
3063
|
+
echo -e "Iteration: $iter"
|
|
3064
|
+
fi
|
|
3065
|
+
|
|
3066
|
+
# Provider info
|
|
3067
|
+
local prov="${LOKI_PROVIDER:-claude}"
|
|
3068
|
+
if [ -f "$LOKI_DIR/state/provider" ]; then
|
|
3069
|
+
prov=$(cat "$LOKI_DIR/state/provider" 2>/dev/null || echo "$prov")
|
|
3070
|
+
fi
|
|
3071
|
+
echo -e "Provider: $prov"
|
|
3072
|
+
|
|
3073
|
+
# Queue status
|
|
3074
|
+
echo ""
|
|
3075
|
+
echo -e "${CYAN}Task Queue:${NC}"
|
|
3076
|
+
for queue in pending in-progress completed failed; do
|
|
3077
|
+
local queue_file="$LOKI_DIR/queue/${queue}.json"
|
|
3078
|
+
if [ -f "$queue_file" ]; then
|
|
3079
|
+
local count
|
|
3080
|
+
count=$(_QUEUE_FILE="$queue_file" python3 -c "import json, os; print(len(json.load(open(os.environ['_QUEUE_FILE']))))" 2>/dev/null || echo "0")
|
|
3081
|
+
echo " $queue: $count"
|
|
3082
|
+
fi
|
|
3083
|
+
done
|
|
3084
|
+
|
|
3085
|
+
# Cost tracking
|
|
3086
|
+
if [ -f "$LOKI_DIR/state/cost-tracker.json" ]; then
|
|
3087
|
+
echo ""
|
|
3088
|
+
echo -e "${CYAN}Cost:${NC}"
|
|
3089
|
+
_LOKI_COST_FILE="$LOKI_DIR/state/cost-tracker.json" python3 -c "
|
|
3090
|
+
import json, os
|
|
3091
|
+
with open(os.environ['_LOKI_COST_FILE']) as f:
|
|
3092
|
+
data = json.load(f)
|
|
3093
|
+
total = data.get('total_cost_usd', 0)
|
|
3094
|
+
budget = data.get('budget_limit', 0)
|
|
3095
|
+
print(f' Total: \${total:.4f}')
|
|
3096
|
+
if budget > 0:
|
|
3097
|
+
print(f' Budget: \${budget:.2f} ({total/budget*100:.1f}% used)')
|
|
3098
|
+
" 2>/dev/null || true
|
|
3099
|
+
fi
|
|
3100
|
+
|
|
3101
|
+
# Recent log
|
|
3102
|
+
echo ""
|
|
3103
|
+
echo -e "${CYAN}Recent Activity:${NC}"
|
|
3104
|
+
local log_file="$LOKI_DIR/logs/loki-run.log"
|
|
3105
|
+
if [ -f "$log_file" ]; then
|
|
3106
|
+
tail -5 "$log_file" 2>/dev/null | sed 's/^/ /'
|
|
3107
|
+
else
|
|
3108
|
+
echo " (no log file)"
|
|
3109
|
+
fi
|
|
3110
|
+
|
|
3111
|
+
echo ""
|
|
3112
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
3113
|
+
|
|
3114
|
+
sleep "$interval"
|
|
3115
|
+
done
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
#===============================================================================
|
|
3119
|
+
# loki export - Export session data (v6.0.0)
|
|
3120
|
+
#===============================================================================
|
|
3121
|
+
|
|
3122
|
+
cmd_export() {
|
|
3123
|
+
local format="${1:---help}"
|
|
3124
|
+
local output_path="${2:-}"
|
|
3125
|
+
|
|
3126
|
+
case "$format" in
|
|
3127
|
+
--help|-h)
|
|
3128
|
+
echo -e "${BOLD}loki export${NC} - Export session data (v6.0.0)"
|
|
3129
|
+
echo ""
|
|
3130
|
+
echo "Usage: loki export <format> [output-path]"
|
|
3131
|
+
echo ""
|
|
3132
|
+
echo "Formats:"
|
|
3133
|
+
echo " json Full session state as JSON"
|
|
3134
|
+
echo " markdown Human-readable session summary"
|
|
3135
|
+
echo " csv Task queue as CSV"
|
|
3136
|
+
echo " timeline Iteration timeline as JSON"
|
|
3137
|
+
echo ""
|
|
3138
|
+
echo "Examples:"
|
|
3139
|
+
echo " loki export json # Print to stdout"
|
|
3140
|
+
echo " loki export json session-export.json # Save to file"
|
|
3141
|
+
echo " loki export markdown # Readable summary"
|
|
3142
|
+
echo " loki export csv tasks.csv # Task queue CSV"
|
|
3143
|
+
echo " loki export timeline # Iteration timeline"
|
|
3144
|
+
exit 0
|
|
3145
|
+
;;
|
|
3146
|
+
json)
|
|
3147
|
+
_export_json "$output_path"
|
|
3148
|
+
;;
|
|
3149
|
+
markdown|md)
|
|
3150
|
+
_export_markdown "$output_path"
|
|
3151
|
+
;;
|
|
3152
|
+
csv)
|
|
3153
|
+
_export_csv "$output_path"
|
|
3154
|
+
;;
|
|
3155
|
+
timeline)
|
|
3156
|
+
_export_timeline "$output_path"
|
|
3157
|
+
;;
|
|
3158
|
+
*)
|
|
3159
|
+
echo -e "${RED}Unknown format: $format${NC}"
|
|
3160
|
+
echo "Supported: json, markdown, csv, timeline"
|
|
3161
|
+
exit 1
|
|
3162
|
+
;;
|
|
3163
|
+
esac
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
_export_json() {
|
|
3167
|
+
local output="$1"
|
|
3168
|
+
|
|
3169
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
3170
|
+
echo -e "${RED}No active session found.${NC}"
|
|
3171
|
+
exit 1
|
|
3172
|
+
fi
|
|
3173
|
+
|
|
3174
|
+
local json_output
|
|
3175
|
+
json_output=$(python3 << 'EXPORT_JSON'
|
|
3176
|
+
import json, os, glob
|
|
3177
|
+
from datetime import datetime
|
|
3178
|
+
|
|
3179
|
+
loki_dir = os.environ.get("LOKI_DIR", ".loki")
|
|
3180
|
+
export = {
|
|
3181
|
+
"exported_at": datetime.utcnow().isoformat() + "Z",
|
|
3182
|
+
"version": "6.0.0",
|
|
3183
|
+
"session": {},
|
|
3184
|
+
"queue": {},
|
|
3185
|
+
"quality": {},
|
|
3186
|
+
"config": {}
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
# Session state
|
|
3190
|
+
for state_file in glob.glob(os.path.join(loki_dir, "state", "*")):
|
|
3191
|
+
name = os.path.basename(state_file)
|
|
3192
|
+
try:
|
|
3193
|
+
with open(state_file) as f:
|
|
3194
|
+
content = f.read().strip()
|
|
3195
|
+
try:
|
|
3196
|
+
export["session"][name] = json.loads(content)
|
|
3197
|
+
except json.JSONDecodeError:
|
|
3198
|
+
export["session"][name] = content
|
|
3199
|
+
except:
|
|
3200
|
+
pass
|
|
3201
|
+
|
|
3202
|
+
# Queue
|
|
3203
|
+
for queue in ["pending", "in-progress", "completed", "failed"]:
|
|
3204
|
+
qf = os.path.join(loki_dir, "queue", f"{queue}.json")
|
|
3205
|
+
if os.path.exists(qf):
|
|
3206
|
+
try:
|
|
3207
|
+
with open(qf) as f:
|
|
3208
|
+
export["queue"][queue] = json.load(f)
|
|
3209
|
+
except:
|
|
3210
|
+
pass
|
|
3211
|
+
|
|
3212
|
+
# Quality reviews (latest 5)
|
|
3213
|
+
reviews_dir = os.path.join(loki_dir, "quality", "reviews")
|
|
3214
|
+
if os.path.isdir(reviews_dir):
|
|
3215
|
+
reviews = sorted(os.listdir(reviews_dir), reverse=True)[:5]
|
|
3216
|
+
export["quality"]["recent_reviews"] = reviews
|
|
3217
|
+
|
|
3218
|
+
# Config
|
|
3219
|
+
cfg = os.path.join(loki_dir, "config", "settings.json")
|
|
3220
|
+
if os.path.exists(cfg):
|
|
3221
|
+
try:
|
|
3222
|
+
with open(cfg) as f:
|
|
3223
|
+
export["config"] = json.load(f)
|
|
3224
|
+
except:
|
|
3225
|
+
pass
|
|
3226
|
+
|
|
3227
|
+
print(json.dumps(export, indent=2, default=str))
|
|
3228
|
+
EXPORT_JSON
|
|
3229
|
+
)
|
|
3230
|
+
|
|
3231
|
+
if [ -n "$output" ]; then
|
|
3232
|
+
# Reject path traversal
|
|
3233
|
+
if [[ "$output" == *".."* ]]; then
|
|
3234
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
3235
|
+
return 1
|
|
3236
|
+
fi
|
|
3237
|
+
echo "$json_output" > "$output"
|
|
3238
|
+
echo -e "${GREEN}Exported to $output${NC}"
|
|
3239
|
+
else
|
|
3240
|
+
echo "$json_output"
|
|
3241
|
+
fi
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
_export_markdown() {
|
|
3245
|
+
local output="$1"
|
|
3246
|
+
|
|
3247
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
3248
|
+
echo -e "${RED}No active session found.${NC}"
|
|
3249
|
+
exit 1
|
|
3250
|
+
fi
|
|
3251
|
+
|
|
3252
|
+
local md_output
|
|
3253
|
+
md_output=$(cat << MDEOF
|
|
3254
|
+
# Loki Mode Session Export
|
|
3255
|
+
|
|
3256
|
+
**Exported:** $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
3257
|
+
**Directory:** $(pwd)
|
|
3258
|
+
|
|
3259
|
+
## Status
|
|
3260
|
+
|
|
3261
|
+
$(cat "$LOKI_DIR/STATUS.txt" 2>/dev/null || echo "No status file")
|
|
3262
|
+
|
|
3263
|
+
## Recent Commits
|
|
3264
|
+
|
|
3265
|
+
$(git log --oneline -10 2>/dev/null || echo "No git history")
|
|
3266
|
+
|
|
3267
|
+
## Task Queue Summary
|
|
3268
|
+
|
|
3269
|
+
$(for q in pending in-progress completed failed; do
|
|
3270
|
+
f="$LOKI_DIR/queue/${q}.json"
|
|
3271
|
+
if [ -f "$f" ]; then
|
|
3272
|
+
count=$(_QUEUE_FILE="$f" python3 -c "import json, os; print(len(json.load(open(os.environ['_QUEUE_FILE']))))" 2>/dev/null || echo "0")
|
|
3273
|
+
echo "- **$q:** $count tasks"
|
|
3274
|
+
fi
|
|
3275
|
+
done)
|
|
3276
|
+
MDEOF
|
|
3277
|
+
)
|
|
3278
|
+
|
|
3279
|
+
if [ -n "$output" ]; then
|
|
3280
|
+
if [[ "$output" == *".."* ]]; then
|
|
3281
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
3282
|
+
return 1
|
|
3283
|
+
fi
|
|
3284
|
+
echo "$md_output" > "$output"
|
|
3285
|
+
echo -e "${GREEN}Exported to $output${NC}"
|
|
3286
|
+
else
|
|
3287
|
+
echo "$md_output"
|
|
3288
|
+
fi
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
_export_csv() {
|
|
3292
|
+
local output="$1"
|
|
3293
|
+
|
|
3294
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
3295
|
+
echo -e "${RED}No active session found.${NC}"
|
|
3296
|
+
exit 1
|
|
3297
|
+
fi
|
|
3298
|
+
|
|
3299
|
+
local csv_output
|
|
3300
|
+
csv_output=$(python3 << 'EXPORT_CSV'
|
|
3301
|
+
import json, os, csv, io
|
|
3302
|
+
|
|
3303
|
+
loki_dir = os.environ.get("LOKI_DIR", ".loki")
|
|
3304
|
+
writer_buf = io.StringIO()
|
|
3305
|
+
writer = csv.writer(writer_buf)
|
|
3306
|
+
writer.writerow(["status", "id", "title", "created_at"])
|
|
3307
|
+
|
|
3308
|
+
for queue in ["pending", "in-progress", "completed", "failed"]:
|
|
3309
|
+
qf = os.path.join(loki_dir, "queue", f"{queue}.json")
|
|
3310
|
+
if os.path.exists(qf):
|
|
3311
|
+
try:
|
|
3312
|
+
with open(qf) as f:
|
|
3313
|
+
tasks = json.load(f)
|
|
3314
|
+
for t in tasks:
|
|
3315
|
+
writer.writerow([
|
|
3316
|
+
queue,
|
|
3317
|
+
t.get("id", ""),
|
|
3318
|
+
t.get("title", t.get("task", "")),
|
|
3319
|
+
t.get("created_at", "")
|
|
3320
|
+
])
|
|
3321
|
+
except:
|
|
3322
|
+
pass
|
|
3323
|
+
|
|
3324
|
+
print(writer_buf.getvalue())
|
|
3325
|
+
EXPORT_CSV
|
|
3326
|
+
)
|
|
3327
|
+
|
|
3328
|
+
if [ -n "$output" ]; then
|
|
3329
|
+
if [[ "$output" == *".."* ]]; then
|
|
3330
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
3331
|
+
return 1
|
|
3332
|
+
fi
|
|
3333
|
+
echo "$csv_output" > "$output"
|
|
3334
|
+
echo -e "${GREEN}Exported to $output${NC}"
|
|
3335
|
+
else
|
|
3336
|
+
echo "$csv_output"
|
|
3337
|
+
fi
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
_export_timeline() {
|
|
3341
|
+
local output="$1"
|
|
3342
|
+
|
|
3343
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
3344
|
+
echo -e "${RED}No active session found.${NC}"
|
|
3345
|
+
exit 1
|
|
3346
|
+
fi
|
|
3347
|
+
|
|
3348
|
+
local timeline_output
|
|
3349
|
+
timeline_output=$(python3 << 'EXPORT_TIMELINE'
|
|
3350
|
+
import json, os, glob
|
|
3351
|
+
|
|
3352
|
+
loki_dir = os.environ.get("LOKI_DIR", ".loki")
|
|
3353
|
+
timeline = []
|
|
3354
|
+
|
|
3355
|
+
# Collect iteration logs
|
|
3356
|
+
log_dir = os.path.join(loki_dir, "logs")
|
|
3357
|
+
if os.path.isdir(log_dir):
|
|
3358
|
+
for f in sorted(glob.glob(os.path.join(log_dir, "iteration-*.json"))):
|
|
3359
|
+
try:
|
|
3360
|
+
with open(f) as fh:
|
|
3361
|
+
timeline.append(json.load(fh))
|
|
3362
|
+
except:
|
|
3363
|
+
pass
|
|
3364
|
+
|
|
3365
|
+
# Collect council verdicts
|
|
3366
|
+
council_dir = os.path.join(loki_dir, "council")
|
|
3367
|
+
if os.path.isdir(council_dir):
|
|
3368
|
+
for f in sorted(glob.glob(os.path.join(council_dir, "votes", "iteration-*", "aggregate.json"))):
|
|
3369
|
+
try:
|
|
3370
|
+
with open(f) as fh:
|
|
3371
|
+
data = json.load(fh)
|
|
3372
|
+
data["_type"] = "council_vote"
|
|
3373
|
+
timeline.append(data)
|
|
3374
|
+
except:
|
|
3375
|
+
pass
|
|
3376
|
+
|
|
3377
|
+
print(json.dumps(timeline, indent=2, default=str))
|
|
3378
|
+
EXPORT_TIMELINE
|
|
3379
|
+
)
|
|
3380
|
+
|
|
3381
|
+
if [ -n "$output" ]; then
|
|
3382
|
+
if [[ "$output" == *".."* ]]; then
|
|
3383
|
+
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
3384
|
+
return 1
|
|
3385
|
+
fi
|
|
3386
|
+
echo "$timeline_output" > "$output"
|
|
3387
|
+
echo -e "${GREEN}Exported to $output${NC}"
|
|
3388
|
+
else
|
|
3389
|
+
echo "$timeline_output"
|
|
3390
|
+
fi
|
|
3391
|
+
}
|
|
3392
|
+
|
|
2746
3393
|
cmd_config() {
|
|
2747
3394
|
local subcommand="${1:-show}"
|
|
3395
|
+
shift 2>/dev/null || true
|
|
2748
3396
|
|
|
2749
3397
|
case "$subcommand" in
|
|
2750
3398
|
show)
|
|
@@ -2759,15 +3407,188 @@ cmd_config() {
|
|
|
2759
3407
|
path)
|
|
2760
3408
|
cmd_config_path
|
|
2761
3409
|
;;
|
|
3410
|
+
set)
|
|
3411
|
+
cmd_config_set "$@"
|
|
3412
|
+
;;
|
|
3413
|
+
get)
|
|
3414
|
+
cmd_config_get "$@"
|
|
3415
|
+
;;
|
|
2762
3416
|
*)
|
|
2763
|
-
echo -e "${YELLOW}Usage: loki config [show|init|edit|path]${NC}"
|
|
3417
|
+
echo -e "${YELLOW}Usage: loki config [show|init|edit|path|set|get]${NC}"
|
|
3418
|
+
echo ""
|
|
3419
|
+
echo " show Show current configuration (default)"
|
|
3420
|
+
echo " init Create a config file from template"
|
|
3421
|
+
echo " edit Open config file in editor"
|
|
3422
|
+
echo " path Show config file paths"
|
|
3423
|
+
echo " set KEY VALUE Set a configuration value"
|
|
3424
|
+
echo " get KEY Get a configuration value"
|
|
2764
3425
|
echo ""
|
|
2765
|
-
echo "
|
|
2766
|
-
echo "
|
|
2767
|
-
echo "
|
|
2768
|
-
echo "
|
|
3426
|
+
echo "Settable keys (v6.0.0):"
|
|
3427
|
+
echo " maxTier Cost ceiling: opus, sonnet, haiku (default: opus)"
|
|
3428
|
+
echo " model.planning Model for planning tier"
|
|
3429
|
+
echo " model.development Model for development tier"
|
|
3430
|
+
echo " model.fast Model for fast tier"
|
|
3431
|
+
echo " provider Default AI provider: claude, codex, gemini"
|
|
3432
|
+
echo " issue.provider Default issue provider: github, gitlab, jira, azure_devops"
|
|
3433
|
+
echo " blind_validation Blind validation mode: true, false (default: true)"
|
|
3434
|
+
echo " adversarial_testing Adversarial testing: true, false (default: true)"
|
|
3435
|
+
echo " spawn_timeout Provider spawn timeout in seconds (default: 120)"
|
|
3436
|
+
echo " spawn_retries Provider spawn retry count (default: 2)"
|
|
3437
|
+
echo " notify.slack Slack webhook URL"
|
|
3438
|
+
echo " notify.discord Discord webhook URL"
|
|
3439
|
+
echo " budget Cost budget limit in USD"
|
|
3440
|
+
;;
|
|
3441
|
+
esac
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
# v6.0.0: Set a configuration value
|
|
3445
|
+
cmd_config_set() {
|
|
3446
|
+
local key="${1:-}"
|
|
3447
|
+
local value="${2:-}"
|
|
3448
|
+
|
|
3449
|
+
if [[ -z "$key" || -z "$value" ]]; then
|
|
3450
|
+
echo -e "${RED}Usage: loki config set <key> <value>${NC}"
|
|
3451
|
+
echo "Run 'loki config' for list of settable keys."
|
|
3452
|
+
return 1
|
|
3453
|
+
fi
|
|
3454
|
+
|
|
3455
|
+
# Ensure config directory exists
|
|
3456
|
+
mkdir -p "$LOKI_DIR/config"
|
|
3457
|
+
local config_store="$LOKI_DIR/config/settings.json"
|
|
3458
|
+
|
|
3459
|
+
# Initialize if not exists
|
|
3460
|
+
if [ ! -f "$config_store" ]; then
|
|
3461
|
+
echo '{}' > "$config_store"
|
|
3462
|
+
fi
|
|
3463
|
+
|
|
3464
|
+
# Validate known keys and values
|
|
3465
|
+
case "$key" in
|
|
3466
|
+
maxTier)
|
|
3467
|
+
case "$value" in
|
|
3468
|
+
opus|sonnet|haiku) ;;
|
|
3469
|
+
*) echo -e "${RED}Invalid maxTier: $value (expected: opus, sonnet, haiku)${NC}"; return 1 ;;
|
|
3470
|
+
esac
|
|
3471
|
+
;;
|
|
3472
|
+
provider)
|
|
3473
|
+
case "$value" in
|
|
3474
|
+
claude|codex|gemini) ;;
|
|
3475
|
+
*) echo -e "${RED}Invalid provider: $value (expected: claude, codex, gemini)${NC}"; return 1 ;;
|
|
3476
|
+
esac
|
|
3477
|
+
;;
|
|
3478
|
+
issue.provider)
|
|
3479
|
+
case "$value" in
|
|
3480
|
+
github|gitlab|jira|azure_devops) ;;
|
|
3481
|
+
*) echo -e "${RED}Invalid issue.provider: $value${NC}"; return 1 ;;
|
|
3482
|
+
esac
|
|
3483
|
+
;;
|
|
3484
|
+
blind_validation|adversarial_testing)
|
|
3485
|
+
case "$value" in
|
|
3486
|
+
true|false) ;;
|
|
3487
|
+
*) echo -e "${RED}Invalid $key: $value (expected: true, false)${NC}"; return 1 ;;
|
|
3488
|
+
esac
|
|
3489
|
+
;;
|
|
3490
|
+
spawn_timeout|spawn_retries)
|
|
3491
|
+
if ! echo "$value" | grep -qE '^[0-9]+$'; then
|
|
3492
|
+
echo -e "${RED}Invalid $key: $value (expected: integer)${NC}"; return 1
|
|
3493
|
+
fi
|
|
3494
|
+
;;
|
|
3495
|
+
budget)
|
|
3496
|
+
if ! echo "$value" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
|
|
3497
|
+
echo -e "${RED}Invalid budget: $value (expected: numeric USD amount)${NC}"; return 1
|
|
3498
|
+
fi
|
|
3499
|
+
;;
|
|
3500
|
+
model.planning|model.development|model.fast)
|
|
3501
|
+
# Validate model names: alphanumeric, dots, hyphens, underscores only
|
|
3502
|
+
if ! echo "$value" | grep -qE '^[a-zA-Z0-9._-]+$'; then
|
|
3503
|
+
echo -e "${RED}Invalid model name: $value (only alphanumeric, dots, hyphens, underscores)${NC}"; return 1
|
|
3504
|
+
fi
|
|
3505
|
+
;;
|
|
3506
|
+
notify.slack|notify.discord)
|
|
3507
|
+
# Validate webhook URLs: must be https
|
|
3508
|
+
if ! echo "$value" | grep -qiE '^https://'; then
|
|
3509
|
+
echo -e "${RED}Invalid webhook URL: must start with https://${NC}"; return 1
|
|
3510
|
+
fi
|
|
3511
|
+
;;
|
|
3512
|
+
*)
|
|
3513
|
+
echo -e "${YELLOW}Warning: Unknown key '$key' - storing anyway${NC}"
|
|
2769
3514
|
;;
|
|
2770
3515
|
esac
|
|
3516
|
+
|
|
3517
|
+
# Update JSON config via python (use inline env vars to avoid leaking into environment)
|
|
3518
|
+
LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" LOKI_CFG_VALUE="$value" \
|
|
3519
|
+
python3 << 'SET_CONFIG'
|
|
3520
|
+
import json, os
|
|
3521
|
+
cfg_file = os.environ["LOKI_CFG_FILE"]
|
|
3522
|
+
key = os.environ["LOKI_CFG_KEY"]
|
|
3523
|
+
value = os.environ["LOKI_CFG_VALUE"]
|
|
3524
|
+
|
|
3525
|
+
with open(cfg_file) as f:
|
|
3526
|
+
config = json.load(f)
|
|
3527
|
+
|
|
3528
|
+
# Handle dotted keys (model.planning -> {"model": {"planning": value}})
|
|
3529
|
+
parts = key.split(".")
|
|
3530
|
+
current = config
|
|
3531
|
+
for part in parts[:-1]:
|
|
3532
|
+
if part not in current or not isinstance(current[part], dict):
|
|
3533
|
+
current[part] = {}
|
|
3534
|
+
current = current[part]
|
|
3535
|
+
current[parts[-1]] = value
|
|
3536
|
+
|
|
3537
|
+
with open(cfg_file, "w") as f:
|
|
3538
|
+
json.dump(config, f, indent=2)
|
|
3539
|
+
SET_CONFIG
|
|
3540
|
+
|
|
3541
|
+
echo -e "${GREEN}Set $key = $value${NC}"
|
|
3542
|
+
|
|
3543
|
+
# Also set env var for immediate effect in current session
|
|
3544
|
+
case "$key" in
|
|
3545
|
+
maxTier) export LOKI_MAX_TIER="$value" ;;
|
|
3546
|
+
provider) export LOKI_PROVIDER="$value" ;;
|
|
3547
|
+
blind_validation) export LOKI_BLIND_VALIDATION="$value" ;;
|
|
3548
|
+
adversarial_testing) export LOKI_ADVERSARIAL_TESTING="$value" ;;
|
|
3549
|
+
spawn_timeout) export LOKI_SPAWN_TIMEOUT="$value" ;;
|
|
3550
|
+
spawn_retries) export LOKI_SPAWN_RETRIES="$value" ;;
|
|
3551
|
+
budget) export LOKI_BUDGET_LIMIT="$value" ;;
|
|
3552
|
+
esac
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
# v6.0.0: Get a configuration value
|
|
3556
|
+
cmd_config_get() {
|
|
3557
|
+
local key="${1:-}"
|
|
3558
|
+
|
|
3559
|
+
if [[ -z "$key" ]]; then
|
|
3560
|
+
echo -e "${RED}Usage: loki config get <key>${NC}"
|
|
3561
|
+
return 1
|
|
3562
|
+
fi
|
|
3563
|
+
|
|
3564
|
+
local config_store="$LOKI_DIR/config/settings.json"
|
|
3565
|
+
if [ ! -f "$config_store" ]; then
|
|
3566
|
+
echo -e "${YELLOW}No config found (using defaults)${NC}"
|
|
3567
|
+
return 0
|
|
3568
|
+
fi
|
|
3569
|
+
|
|
3570
|
+
export LOKI_CFG_FILE="$config_store"
|
|
3571
|
+
export LOKI_CFG_KEY="$key"
|
|
3572
|
+
python3 << 'GET_CONFIG'
|
|
3573
|
+
import json, os
|
|
3574
|
+
cfg_file = os.environ["LOKI_CFG_FILE"]
|
|
3575
|
+
key = os.environ["LOKI_CFG_KEY"]
|
|
3576
|
+
|
|
3577
|
+
with open(cfg_file) as f:
|
|
3578
|
+
config = json.load(f)
|
|
3579
|
+
|
|
3580
|
+
parts = key.split(".")
|
|
3581
|
+
current = config
|
|
3582
|
+
for part in parts:
|
|
3583
|
+
if isinstance(current, dict) and part in current:
|
|
3584
|
+
current = current[part]
|
|
3585
|
+
else:
|
|
3586
|
+
print("")
|
|
3587
|
+
exit(0)
|
|
3588
|
+
|
|
3589
|
+
print(current if not isinstance(current, dict) else json.dumps(current, indent=2))
|
|
3590
|
+
GET_CONFIG
|
|
3591
|
+
unset LOKI_CFG_FILE LOKI_CFG_KEY
|
|
2771
3592
|
}
|
|
2772
3593
|
|
|
2773
3594
|
cmd_config_show() {
|
|
@@ -2825,7 +3646,17 @@ cmd_config_show() {
|
|
|
2825
3646
|
echo " max_iterations: ${LOKI_MAX_ITERATIONS:-1000}"
|
|
2826
3647
|
echo " autonomy_mode: ${LOKI_AUTONOMY_MODE:-perpetual}"
|
|
2827
3648
|
echo ""
|
|
3649
|
+
echo "v6.0.0 Settings:"
|
|
3650
|
+
echo " maxTier: ${LOKI_MAX_TIER:-(unlimited)}"
|
|
3651
|
+
echo " blind_validation: ${LOKI_BLIND_VALIDATION:-true}"
|
|
3652
|
+
echo " adversarial_testing: ${LOKI_ADVERSARIAL_TESTING:-true}"
|
|
3653
|
+
echo " spawn_timeout: ${LOKI_SPAWN_TIMEOUT:-120}s"
|
|
3654
|
+
echo " spawn_retries: ${LOKI_SPAWN_RETRIES:-2}"
|
|
3655
|
+
echo " issue_provider: ${LOKI_ISSUE_PROVIDER:-(auto-detect)}"
|
|
3656
|
+
echo " budget: ${LOKI_BUDGET_LIMIT:-(no limit)}"
|
|
3657
|
+
echo ""
|
|
2828
3658
|
echo -e "Run ${CYAN}loki config path${NC} to see all config file locations"
|
|
3659
|
+
echo -e "Run ${CYAN}loki config set <key> <value>${NC} to change a setting"
|
|
2829
3660
|
}
|
|
2830
3661
|
|
|
2831
3662
|
cmd_config_init() {
|
|
@@ -5843,6 +6674,9 @@ main() {
|
|
|
5843
6674
|
loki_telemetry "cli_command" "command=$command" 2>/dev/null || true
|
|
5844
6675
|
|
|
5845
6676
|
case "$command" in
|
|
6677
|
+
run)
|
|
6678
|
+
cmd_run "$@"
|
|
6679
|
+
;;
|
|
5846
6680
|
start)
|
|
5847
6681
|
cmd_start "$@"
|
|
5848
6682
|
;;
|
|
@@ -5897,6 +6731,12 @@ main() {
|
|
|
5897
6731
|
issue)
|
|
5898
6732
|
cmd_issue "$@"
|
|
5899
6733
|
;;
|
|
6734
|
+
watch)
|
|
6735
|
+
cmd_watch "$@"
|
|
6736
|
+
;;
|
|
6737
|
+
export)
|
|
6738
|
+
cmd_export "$@"
|
|
6739
|
+
;;
|
|
5900
6740
|
config)
|
|
5901
6741
|
cmd_config "$@"
|
|
5902
6742
|
;;
|