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/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> Generate PRD from GitHub issue and optionally start"
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 'issue':"
440
- echo " --repo OWNER/REPO Specify repository (default: auto-detect)"
441
- echo " --number NUM Specify issue number (alternative to URL)"
442
- echo " --start Start Loki Mode with generated PRD"
443
- echo " --dry-run Preview generated PRD without saving"
444
- echo " --output FILE Save PRD to custom path (default: .loki/prd-issue-N.md)"
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 pause # Pause execution"
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
- # Generate PRD from GitHub issue
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 " show Show current configuration (default)"
2766
- echo " init Create a config file from template"
2767
- echo " edit Open config file in editor"
2768
- echo " path Show config file paths"
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
  ;;