loki-mode 7.21.0 → 7.23.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/loki +78 -1
- package/autonomy/run.sh +433 -7
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +79 -1
- package/package.json +1 -1
- package/references/mcp-integration.md +65 -0
- package/skills/00-index.md +2 -0
- package/skills/parallel-workflows.md +96 -0
- package/skills/quality-gates.md +18 -0
- package/tools/hybrid_search.py +451 -0
- package/tools/index-codebase.py +296 -0
package/autonomy/run.sh
CHANGED
|
@@ -781,6 +781,21 @@ MAX_PARALLEL_SESSIONS=${LOKI_MAX_PARALLEL_SESSIONS:-3}
|
|
|
781
781
|
PARALLEL_TESTING=${LOKI_PARALLEL_TESTING:-true}
|
|
782
782
|
PARALLEL_DOCS=${LOKI_PARALLEL_DOCS:-true}
|
|
783
783
|
|
|
784
|
+
# Dynamic resource-aware session concurrency (Release 3, slice 3).
|
|
785
|
+
# DEFAULT OFF: when LOKI_DYNAMIC_CONCURRENCY is unset, effective_session_cap()
|
|
786
|
+
# returns exactly MAX_PARALLEL_SESSIONS, so behavior is identical to before.
|
|
787
|
+
# Opt in with LOKI_DYNAMIC_CONCURRENCY=1 to scale the session cap down when
|
|
788
|
+
# system CPU or memory is under pressure (read from .loki/state/resources.json).
|
|
789
|
+
DYNAMIC_CONCURRENCY=${LOKI_DYNAMIC_CONCURRENCY:-0}
|
|
790
|
+
# Optional higher ceiling on capable machines. Only takes effect with dynamic
|
|
791
|
+
# concurrency enabled; still resource-gated. Defaults to MAX_PARALLEL_SESSIONS.
|
|
792
|
+
MAX_PARALLEL_SESSIONS_CEILING=${LOKI_MAX_PARALLEL_SESSIONS_CEILING:-$MAX_PARALLEL_SESSIONS}
|
|
793
|
+
# Usage thresholds (percent). At/above CPU or MEM threshold the cap is halved.
|
|
794
|
+
CONCURRENCY_CPU_THRESHOLD=${LOKI_CONCURRENCY_CPU_THRESHOLD:-85}
|
|
795
|
+
CONCURRENCY_MEM_THRESHOLD=${LOKI_CONCURRENCY_MEM_THRESHOLD:-85}
|
|
796
|
+
# Critical threshold (percent). At/above this the cap is forced to 1.
|
|
797
|
+
CONCURRENCY_CRITICAL_THRESHOLD=${LOKI_CONCURRENCY_CRITICAL_THRESHOLD:-95}
|
|
798
|
+
|
|
784
799
|
# Gate Escalation Ladder (v6.10.0)
|
|
785
800
|
GATE_CLEAR_LIMIT=${LOKI_GATE_CLEAR_LIMIT:-3}
|
|
786
801
|
GATE_ESCALATE_LIMIT=${LOKI_GATE_ESCALATE_LIMIT:-5}
|
|
@@ -2355,6 +2370,15 @@ notify_all_complete() {
|
|
|
2355
2370
|
|
|
2356
2371
|
notify_intervention_needed() {
|
|
2357
2372
|
local reason="$1"
|
|
2373
|
+
# Delegate-then-notify: this helper ONLY fires the (gated) desktop ping. It
|
|
2374
|
+
# deliberately does NOT write the durable COMPLETION.txt / completion.json
|
|
2375
|
+
# record. Reason: notify_intervention_needed is also called from NON-terminal
|
|
2376
|
+
# sites (the perpetual-mode PAUSE auto-clear branch, uncertainty escalation)
|
|
2377
|
+
# where the run keeps going. Writing a "Needs input" durable file there would
|
|
2378
|
+
# falsely tell a detached user the run is done / blocked when it is not. The
|
|
2379
|
+
# durable intervention write now lives only at the genuinely blocking pause
|
|
2380
|
+
# sites (immediately before handle_pause), so the durable state matches the
|
|
2381
|
+
# actual run state.
|
|
2358
2382
|
send_notification "Intervention Needed" "$reason" "critical"
|
|
2359
2383
|
}
|
|
2360
2384
|
|
|
@@ -2363,6 +2387,265 @@ notify_rate_limit() {
|
|
|
2363
2387
|
send_notification "Rate Limited" "Waiting ${wait_time}s before retry" "normal"
|
|
2364
2388
|
}
|
|
2365
2389
|
|
|
2390
|
+
#===============================================================================
|
|
2391
|
+
# Delegate-then-notify: completion summary (Release 2, "delegate then notify")
|
|
2392
|
+
#
|
|
2393
|
+
# build_completion_summary <outcome> writes two durable files that survive a
|
|
2394
|
+
# detached (--bg) run where the terminal is gone and a bell would be useless:
|
|
2395
|
+
# .loki/COMPLETION.txt human plain text (no emojis, no dashes)
|
|
2396
|
+
# .loki/state/completion.json machine-readable record of the same facts
|
|
2397
|
+
# It also exports two strings for send_notification to consume:
|
|
2398
|
+
# _LOKI_SUMMARY_TITLE short notification subtitle
|
|
2399
|
+
# _LOKI_SUMMARY_BODY short notification body (outcome + branch + file count)
|
|
2400
|
+
#
|
|
2401
|
+
# All git reads are best-effort and non-fatal. The diff window is the run-start
|
|
2402
|
+
# SHA captured once at runner init (_LOKI_RUN_START_SHA); we REUSE it and never
|
|
2403
|
+
# recapture, so the reported diff matches the evidence gate's window exactly.
|
|
2404
|
+
#
|
|
2405
|
+
# This function NEVER sends a notification and NEVER gates on
|
|
2406
|
+
# NOTIFICATIONS_ENABLED: the files are state, not a notification, and must be
|
|
2407
|
+
# written even when desktop notifications are disabled. emit_completion_summary
|
|
2408
|
+
# below is the wrapper that writes the files AND (gated) fires the desktop ping.
|
|
2409
|
+
#===============================================================================
|
|
2410
|
+
build_completion_summary() {
|
|
2411
|
+
local outcome="${1:-complete}"
|
|
2412
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
2413
|
+
mkdir -p "$loki_dir/state" 2>/dev/null || true
|
|
2414
|
+
|
|
2415
|
+
# Human-readable outcome label and notification title.
|
|
2416
|
+
local outcome_label notify_title
|
|
2417
|
+
case "$outcome" in
|
|
2418
|
+
complete) outcome_label="Completed"; notify_title="Run complete" ;;
|
|
2419
|
+
max_iterations) outcome_label="Max iterations"; notify_title="Run stopped (max iterations)" ;;
|
|
2420
|
+
stopped) outcome_label="Stopped"; notify_title="Run stopped" ;;
|
|
2421
|
+
failed) outcome_label="Failed"; notify_title="Run failed" ;;
|
|
2422
|
+
intervention) outcome_label="Needs input"; notify_title="Input needed" ;;
|
|
2423
|
+
*) outcome_label="$outcome"; notify_title="Run finished" ;;
|
|
2424
|
+
esac
|
|
2425
|
+
|
|
2426
|
+
# Branch + diff stats vs the run-start SHA (best-effort; non-git or empty
|
|
2427
|
+
# baseline yields empty values, which we render as "unknown"/"0").
|
|
2428
|
+
local start_sha="${_LOKI_RUN_START_SHA:-}"
|
|
2429
|
+
local branch="" head_sha="" diff_stat="" files_changed=0 insertions=0 deletions=0 review_cmd=""
|
|
2430
|
+
branch="$( (cd "${TARGET_DIR:-.}" && git rev-parse --abbrev-ref HEAD) 2>/dev/null || true )"
|
|
2431
|
+
[ -z "$branch" ] && branch="unknown"
|
|
2432
|
+
head_sha="$( (cd "${TARGET_DIR:-.}" && git rev-parse HEAD) 2>/dev/null || true )"
|
|
2433
|
+
|
|
2434
|
+
if [ -n "$start_sha" ]; then
|
|
2435
|
+
diff_stat="$( (cd "${TARGET_DIR:-.}" && git diff --stat "${start_sha}..HEAD") 2>/dev/null || true )"
|
|
2436
|
+
# Parse the git diff --shortstat tail for counts (locale-stable enough
|
|
2437
|
+
# for our display; failures leave the zeros in place).
|
|
2438
|
+
local shortstat
|
|
2439
|
+
shortstat="$( (cd "${TARGET_DIR:-.}" && git diff --shortstat "${start_sha}..HEAD") 2>/dev/null || true )"
|
|
2440
|
+
if [ -n "$shortstat" ]; then
|
|
2441
|
+
files_changed="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' | head -1)"
|
|
2442
|
+
insertions="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' | head -1)"
|
|
2443
|
+
deletions="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' | head -1)"
|
|
2444
|
+
fi
|
|
2445
|
+
review_cmd="git diff ${start_sha}..HEAD"
|
|
2446
|
+
else
|
|
2447
|
+
review_cmd="git diff HEAD"
|
|
2448
|
+
fi
|
|
2449
|
+
[ -z "$files_changed" ] && files_changed=0
|
|
2450
|
+
[ -z "$insertions" ] && insertions=0
|
|
2451
|
+
[ -z "$deletions" ] && deletions=0
|
|
2452
|
+
|
|
2453
|
+
# Task counts: reuse the SAME queue reads as update_status_file.
|
|
2454
|
+
local pending=0 in_progress=0 completed=0 failed=0
|
|
2455
|
+
[ -f "$loki_dir/queue/pending.json" ] && pending=$(python3 -c "import json; print(len(json.load(open('$loki_dir/queue/pending.json'))))" 2>/dev/null || echo "0")
|
|
2456
|
+
[ -f "$loki_dir/queue/in-progress.json" ] && in_progress=$(python3 -c "import json; print(len(json.load(open('$loki_dir/queue/in-progress.json'))))" 2>/dev/null || echo "0")
|
|
2457
|
+
[ -f "$loki_dir/queue/completed.json" ] && completed=$(python3 -c "import json; print(len(json.load(open('$loki_dir/queue/completed.json'))))" 2>/dev/null || echo "0")
|
|
2458
|
+
[ -f "$loki_dir/queue/failed.json" ] && failed=$(python3 -c "import json; print(len(json.load(open('$loki_dir/queue/failed.json'))))" 2>/dev/null || echo "0")
|
|
2459
|
+
|
|
2460
|
+
# Optional delegate-mode extras populated by Slice 3 (branch isolation / PR).
|
|
2461
|
+
local delegate_branch="${_LOKI_DELEGATE_BRANCH_NAME:-}"
|
|
2462
|
+
local pr_url="${_LOKI_DELEGATE_PR_URL:-}"
|
|
2463
|
+
|
|
2464
|
+
local ts
|
|
2465
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)"
|
|
2466
|
+
|
|
2467
|
+
# ---- Durable human-readable file: .loki/COMPLETION.txt --------------------
|
|
2468
|
+
{
|
|
2469
|
+
echo "Loki Mode run summary"
|
|
2470
|
+
echo "====================="
|
|
2471
|
+
echo ""
|
|
2472
|
+
echo "Outcome: $outcome_label"
|
|
2473
|
+
echo "Branch: $branch"
|
|
2474
|
+
echo "Files changed: $files_changed (+$insertions / -$deletions)"
|
|
2475
|
+
echo "Finished: $ts"
|
|
2476
|
+
echo ""
|
|
2477
|
+
if [ -n "$delegate_branch" ]; then
|
|
2478
|
+
echo "Delegate branch: $delegate_branch"
|
|
2479
|
+
fi
|
|
2480
|
+
if [ -n "$pr_url" ]; then
|
|
2481
|
+
echo "Pull request: $pr_url"
|
|
2482
|
+
elif [ "$outcome" = "complete" ]; then
|
|
2483
|
+
echo "Pull request: not opened (set LOKI_DELEGATE_PR=1 to open one)"
|
|
2484
|
+
fi
|
|
2485
|
+
echo ""
|
|
2486
|
+
echo "Tasks: pending=$pending in_progress=$in_progress completed=$completed failed=$failed"
|
|
2487
|
+
echo ""
|
|
2488
|
+
echo "Review the work:"
|
|
2489
|
+
echo " $review_cmd"
|
|
2490
|
+
echo ""
|
|
2491
|
+
if [ -n "$diff_stat" ]; then
|
|
2492
|
+
echo "Diff stat:"
|
|
2493
|
+
echo "$diff_stat"
|
|
2494
|
+
else
|
|
2495
|
+
echo "Diff stat: (no changes detected vs run start, or git unavailable)"
|
|
2496
|
+
fi
|
|
2497
|
+
} > "$loki_dir/COMPLETION.txt" 2>/dev/null || true
|
|
2498
|
+
|
|
2499
|
+
# ---- Durable machine-readable file: .loki/state/completion.json -----------
|
|
2500
|
+
_LOKI_CS_OUTCOME="$outcome" \
|
|
2501
|
+
_LOKI_CS_BRANCH="$branch" \
|
|
2502
|
+
_LOKI_CS_START_SHA="$start_sha" \
|
|
2503
|
+
_LOKI_CS_HEAD_SHA="$head_sha" \
|
|
2504
|
+
_LOKI_CS_FILES="$files_changed" \
|
|
2505
|
+
_LOKI_CS_INS="$insertions" \
|
|
2506
|
+
_LOKI_CS_DEL="$deletions" \
|
|
2507
|
+
_LOKI_CS_REVIEW="$review_cmd" \
|
|
2508
|
+
_LOKI_CS_DELEGATE_BRANCH="$delegate_branch" \
|
|
2509
|
+
_LOKI_CS_PR_URL="$pr_url" \
|
|
2510
|
+
_LOKI_CS_TS="$ts" \
|
|
2511
|
+
_LOKI_CS_OUT_FILE="$loki_dir/state/completion.json" \
|
|
2512
|
+
python3 -c "
|
|
2513
|
+
import json, os, tempfile
|
|
2514
|
+
out = os.environ['_LOKI_CS_OUT_FILE']
|
|
2515
|
+
def i(v):
|
|
2516
|
+
try: return int(v)
|
|
2517
|
+
except (TypeError, ValueError): return 0
|
|
2518
|
+
rec = {
|
|
2519
|
+
'outcome': os.environ.get('_LOKI_CS_OUTCOME', ''),
|
|
2520
|
+
'branch': os.environ.get('_LOKI_CS_BRANCH', ''),
|
|
2521
|
+
'start_sha': os.environ.get('_LOKI_CS_START_SHA', ''),
|
|
2522
|
+
'head_sha': os.environ.get('_LOKI_CS_HEAD_SHA', ''),
|
|
2523
|
+
'files_changed': i(os.environ.get('_LOKI_CS_FILES')),
|
|
2524
|
+
'insertions': i(os.environ.get('_LOKI_CS_INS')),
|
|
2525
|
+
'deletions': i(os.environ.get('_LOKI_CS_DEL')),
|
|
2526
|
+
'review_cmd': os.environ.get('_LOKI_CS_REVIEW', ''),
|
|
2527
|
+
'delegate_branch': os.environ.get('_LOKI_CS_DELEGATE_BRANCH', ''),
|
|
2528
|
+
'pr_url': os.environ.get('_LOKI_CS_PR_URL', ''),
|
|
2529
|
+
'timestamp': os.environ.get('_LOKI_CS_TS', ''),
|
|
2530
|
+
}
|
|
2531
|
+
d = os.path.dirname(out)
|
|
2532
|
+
fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
|
|
2533
|
+
with os.fdopen(fd, 'w') as f:
|
|
2534
|
+
json.dump(rec, f, indent=2)
|
|
2535
|
+
os.replace(tmp, out)
|
|
2536
|
+
" 2>/dev/null || true
|
|
2537
|
+
|
|
2538
|
+
# ---- Short strings for the desktop notification --------------------------
|
|
2539
|
+
# Desktop body stays terse; full detail lives in COMPLETION.txt.
|
|
2540
|
+
_LOKI_SUMMARY_TITLE="$notify_title"
|
|
2541
|
+
_LOKI_SUMMARY_BODY="${outcome_label} on ${branch}: ${files_changed} files changed"
|
|
2542
|
+
if [ -n "$pr_url" ]; then
|
|
2543
|
+
_LOKI_SUMMARY_BODY="${_LOKI_SUMMARY_BODY}. PR: ${pr_url}"
|
|
2544
|
+
fi
|
|
2545
|
+
export _LOKI_SUMMARY_TITLE _LOKI_SUMMARY_BODY
|
|
2546
|
+
return 0
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
#===============================================================================
|
|
2550
|
+
# emit_completion_summary <outcome> [urgency]
|
|
2551
|
+
#
|
|
2552
|
+
# The single entry point every terminal state calls. It ALWAYS writes the
|
|
2553
|
+
# durable summary files (state, not a notification) and then fires ONE desktop
|
|
2554
|
+
# notification gated by the existing LOKI_NOTIFICATIONS flag (send_notification
|
|
2555
|
+
# already short-circuits when disabled, so the gate is implicit but explicit
|
|
2556
|
+
# here for clarity). Centralizing this keeps the success-only PR side effect
|
|
2557
|
+
# (Slice 3) in one place and prevents duplicate notifications.
|
|
2558
|
+
#===============================================================================
|
|
2559
|
+
emit_completion_summary() {
|
|
2560
|
+
local outcome="${1:-complete}"
|
|
2561
|
+
local urgency="${2:-normal}"
|
|
2562
|
+
build_completion_summary "$outcome"
|
|
2563
|
+
send_notification "${_LOKI_SUMMARY_TITLE:-Run finished}" "${_LOKI_SUMMARY_BODY:-}" "$urgency"
|
|
2564
|
+
return 0
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
#===============================================================================
|
|
2568
|
+
# on_run_complete (Slice 3: opt-in local git output on success)
|
|
2569
|
+
#
|
|
2570
|
+
# Called from every SUCCESS exit BEFORE emit_completion_summary so the PR url it
|
|
2571
|
+
# discovers is folded into the summary. Default behavior is a no-op: it only
|
|
2572
|
+
# acts when LOKI_DELEGATE_PR=1.
|
|
2573
|
+
#
|
|
2574
|
+
# LOKI_DELEGATE_PR=1 opens a LOCAL pull request from the user's machine, only if:
|
|
2575
|
+
# - this is a GitHub repo (gh + a github.com remote), AND
|
|
2576
|
+
# - `gh auth status` succeeds, AND
|
|
2577
|
+
# - the current branch is not main/master (never PR a default branch to itself)
|
|
2578
|
+
# It mirrors the proven pattern at autonomy/loki:5524-5527: push the branch,
|
|
2579
|
+
# then `gh pr create --head <branch>`. NO auto-merge. Every call is best-effort
|
|
2580
|
+
# (`|| true`); failures never block completion. This is a single sanctioned
|
|
2581
|
+
# local network call, never CI.
|
|
2582
|
+
#
|
|
2583
|
+
# Reconciliation with the existing GITHUB_PR path (run.sh create_github_pr,
|
|
2584
|
+
# invoked after run_autonomous returns when LOKI_GITHUB_PR=true): if GITHUB_PR
|
|
2585
|
+
# is already true we DEFER to that path and do nothing here, so a user who set
|
|
2586
|
+
# both knobs never gets a double PR.
|
|
2587
|
+
#===============================================================================
|
|
2588
|
+
on_run_complete() {
|
|
2589
|
+
# Default OFF.
|
|
2590
|
+
if [ "${LOKI_DELEGATE_PR:-0}" != "1" ]; then
|
|
2591
|
+
return 0
|
|
2592
|
+
fi
|
|
2593
|
+
# Defer to the existing dedicated PR path to avoid a double PR.
|
|
2594
|
+
if [ "${GITHUB_PR:-false}" = "true" ]; then
|
|
2595
|
+
return 0
|
|
2596
|
+
fi
|
|
2597
|
+
# Network-call timeout guard: a stalled network / auth prompt would
|
|
2598
|
+
# otherwise hang the completion path indefinitely in --bg. Run each network
|
|
2599
|
+
# call through `timeout 30` when available; fall back to the bare call if
|
|
2600
|
+
# timeout is not installed (a local wrapper keeps this set -u safe on bash
|
|
2601
|
+
# 3.2, where an empty array expansion would error). Keeps every existing
|
|
2602
|
+
# `|| true` non-fatal behavior.
|
|
2603
|
+
_loki_net() {
|
|
2604
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
2605
|
+
timeout 30 "$@"
|
|
2606
|
+
else
|
|
2607
|
+
"$@"
|
|
2608
|
+
fi
|
|
2609
|
+
}
|
|
2610
|
+
# Require gh + auth.
|
|
2611
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
2612
|
+
return 0
|
|
2613
|
+
fi
|
|
2614
|
+
if ! (cd "${TARGET_DIR:-.}" && _loki_net gh auth status) >/dev/null 2>&1; then
|
|
2615
|
+
return 0
|
|
2616
|
+
fi
|
|
2617
|
+
# Require a GitHub remote (skip silently on non-GitHub repos).
|
|
2618
|
+
local remote_url
|
|
2619
|
+
remote_url="$( (cd "${TARGET_DIR:-.}" && git config --get remote.origin.url) 2>/dev/null || true )"
|
|
2620
|
+
case "$remote_url" in
|
|
2621
|
+
*github.com*) : ;;
|
|
2622
|
+
*) return 0 ;;
|
|
2623
|
+
esac
|
|
2624
|
+
# Resolve current branch; never PR a default branch to itself.
|
|
2625
|
+
local branch
|
|
2626
|
+
branch="$( (cd "${TARGET_DIR:-.}" && git rev-parse --abbrev-ref HEAD) 2>/dev/null || true )"
|
|
2627
|
+
case "$branch" in
|
|
2628
|
+
""|main|master|HEAD) return 0 ;;
|
|
2629
|
+
esac
|
|
2630
|
+
log_info "LOKI_DELEGATE_PR=1: opening a local pull request for branch '$branch'..."
|
|
2631
|
+
# Push, then create. Non-interactive (no tty in --bg). Best-effort, each
|
|
2632
|
+
# network call bounded by the timeout guard above.
|
|
2633
|
+
(cd "${TARGET_DIR:-.}" && _loki_net git push -u origin "$branch") >/dev/null 2>&1 || true
|
|
2634
|
+
local pr_title
|
|
2635
|
+
pr_title="Loki Mode: ${branch}"
|
|
2636
|
+
local pr_url=""
|
|
2637
|
+
pr_url="$( (cd "${TARGET_DIR:-.}" && _loki_net gh pr create --title "$pr_title" --body "Opened by Loki Mode (delegate mode). Review locally before merge." --head "$branch") 2>/dev/null || true )"
|
|
2638
|
+
if [ -n "$pr_url" ]; then
|
|
2639
|
+
# Export so build_completion_summary folds the url into the summary.
|
|
2640
|
+
_LOKI_DELEGATE_PR_URL="$pr_url"
|
|
2641
|
+
export _LOKI_DELEGATE_PR_URL
|
|
2642
|
+
log_info "Pull request opened: $pr_url"
|
|
2643
|
+
else
|
|
2644
|
+
log_warn "LOKI_DELEGATE_PR=1: gh pr create did not return a URL (a PR may already exist for this branch)."
|
|
2645
|
+
fi
|
|
2646
|
+
return 0
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2366
2649
|
#===============================================================================
|
|
2367
2650
|
# Parallel Workflow Functions (Git Worktrees)
|
|
2368
2651
|
#===============================================================================
|
|
@@ -2491,6 +2774,72 @@ remove_worktree() {
|
|
|
2491
2774
|
log_info "Removed worktree: $stream_name"
|
|
2492
2775
|
}
|
|
2493
2776
|
|
|
2777
|
+
# Compute the effective parallel-session cap for the current scheduling pass.
|
|
2778
|
+
# Default-off contract: when LOKI_DYNAMIC_CONCURRENCY is not "1" this echoes
|
|
2779
|
+
# exactly MAX_PARALLEL_SESSIONS with zero file reads and zero subprocesses, so
|
|
2780
|
+
# the spawn decision is byte-identical to the pre-feature behavior.
|
|
2781
|
+
# When enabled, it starts from the configured ceiling and scales DOWN based on
|
|
2782
|
+
# .loki/state/resources.json. All reads are best-effort: a missing, empty, or
|
|
2783
|
+
# unparseable file (or non-numeric values) leaves the cap at the ceiling. The
|
|
2784
|
+
# result is always clamped to the range [1, ceiling] and never exceeds it.
|
|
2785
|
+
effective_session_cap() {
|
|
2786
|
+
# Fast default-off path: identical to today, no I/O, no subprocesses.
|
|
2787
|
+
if [ "${DYNAMIC_CONCURRENCY:-0}" != "1" ]; then
|
|
2788
|
+
echo "$MAX_PARALLEL_SESSIONS"
|
|
2789
|
+
return 0
|
|
2790
|
+
fi
|
|
2791
|
+
|
|
2792
|
+
# Ceiling is the upper bound when dynamic scaling is on.
|
|
2793
|
+
local ceiling="${MAX_PARALLEL_SESSIONS_CEILING:-$MAX_PARALLEL_SESSIONS}"
|
|
2794
|
+
# Guard against a non-numeric or sub-1 ceiling override.
|
|
2795
|
+
case "$ceiling" in
|
|
2796
|
+
''|*[!0-9]*) ceiling="$MAX_PARALLEL_SESSIONS" ;;
|
|
2797
|
+
esac
|
|
2798
|
+
[ "$ceiling" -lt 1 ] 2>/dev/null && ceiling=1
|
|
2799
|
+
|
|
2800
|
+
local cap="$ceiling"
|
|
2801
|
+
local resources_file=".loki/state/resources.json"
|
|
2802
|
+
|
|
2803
|
+
# No resource data -> best-effort, leave at ceiling.
|
|
2804
|
+
if [ ! -f "$resources_file" ]; then
|
|
2805
|
+
echo "$cap"
|
|
2806
|
+
return 0
|
|
2807
|
+
fi
|
|
2808
|
+
|
|
2809
|
+
# Read usage and status best-effort. Defaults keep the cap at the ceiling
|
|
2810
|
+
# if the file is empty, malformed, or missing keys.
|
|
2811
|
+
local cpu_usage mem_usage status
|
|
2812
|
+
cpu_usage=$(python3 -c "import json; print(json.load(open('$resources_file')).get('cpu', {}).get('usage_percent', 0))" 2>/dev/null || echo "0")
|
|
2813
|
+
mem_usage=$(python3 -c "import json; print(json.load(open('$resources_file')).get('memory', {}).get('usage_percent', 0))" 2>/dev/null || echo "0")
|
|
2814
|
+
status=$(python3 -c "import json; print(json.load(open('$resources_file')).get('overall_status', 'ok'))" 2>/dev/null || echo "ok")
|
|
2815
|
+
|
|
2816
|
+
# usage_percent can be a float (e.g. 85.3). Reduce to an integer part for
|
|
2817
|
+
# comparison and fall back to 0 if anything is non-numeric.
|
|
2818
|
+
cpu_usage="${cpu_usage%%.*}"
|
|
2819
|
+
mem_usage="${mem_usage%%.*}"
|
|
2820
|
+
case "$cpu_usage" in ''|*[!0-9]*) cpu_usage=0 ;; esac
|
|
2821
|
+
case "$mem_usage" in ''|*[!0-9]*) mem_usage=0 ;; esac
|
|
2822
|
+
|
|
2823
|
+
local crit="${CONCURRENCY_CRITICAL_THRESHOLD:-95}"
|
|
2824
|
+
local cpu_thr="${CONCURRENCY_CPU_THRESHOLD:-85}"
|
|
2825
|
+
local mem_thr="${CONCURRENCY_MEM_THRESHOLD:-85}"
|
|
2826
|
+
|
|
2827
|
+
if [ "$cpu_usage" -ge "$crit" ] || [ "$mem_usage" -ge "$crit" ]; then
|
|
2828
|
+
# Critical pressure: drop to a single session.
|
|
2829
|
+
cap=1
|
|
2830
|
+
elif [ "$cpu_usage" -ge "$cpu_thr" ] || [ "$mem_usage" -ge "$mem_thr" ] || [ "$status" != "ok" ]; then
|
|
2831
|
+
# Elevated pressure or a non-ok overall status: halve (integer floor).
|
|
2832
|
+
cap=$(( ceiling / 2 ))
|
|
2833
|
+
fi
|
|
2834
|
+
|
|
2835
|
+
# Clamp to [1, ceiling]. Never runaway, never zero.
|
|
2836
|
+
[ "$cap" -lt 1 ] && cap=1
|
|
2837
|
+
[ "$cap" -gt "$ceiling" ] && cap="$ceiling"
|
|
2838
|
+
|
|
2839
|
+
echo "$cap"
|
|
2840
|
+
return 0
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2494
2843
|
# Spawn a Claude session in a worktree
|
|
2495
2844
|
spawn_worktree_session() {
|
|
2496
2845
|
local stream_name="$1"
|
|
@@ -2510,9 +2859,11 @@ spawn_worktree_session() {
|
|
|
2510
2859
|
fi
|
|
2511
2860
|
done
|
|
2512
2861
|
|
|
2513
|
-
|
|
2862
|
+
local session_cap
|
|
2863
|
+
session_cap=$(effective_session_cap)
|
|
2864
|
+
if [ "$active_count" -ge "$session_cap" ]; then
|
|
2514
2865
|
# BUG-PAR-014: Max-sessions rejection queues spawn for retry
|
|
2515
|
-
log_warn "Max parallel sessions reached ($
|
|
2866
|
+
log_warn "Max parallel sessions reached ($session_cap). Queuing $stream_name for retry."
|
|
2516
2867
|
mkdir -p "${TARGET_DIR:-.}/.loki/signals"
|
|
2517
2868
|
echo "{\"stream\":\"$stream_name\",\"task\":\"$(echo "$task_prompt" | head -c 200)\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
|
|
2518
2869
|
> "${TARGET_DIR:-.}/.loki/signals/SPAWN_QUEUED_${stream_name}"
|
|
@@ -2944,7 +3295,9 @@ run_parallel_orchestrator() {
|
|
|
2944
3295
|
((active_count++))
|
|
2945
3296
|
fi
|
|
2946
3297
|
done
|
|
2947
|
-
|
|
3298
|
+
local _session_cap
|
|
3299
|
+
_session_cap=$(effective_session_cap)
|
|
3300
|
+
if [ "$active_count" -lt "$_session_cap" ]; then
|
|
2948
3301
|
for queued_signal in "${TARGET_DIR:-.}"/.loki/signals/SPAWN_QUEUED_*; do
|
|
2949
3302
|
[ -f "$queued_signal" ] || continue
|
|
2950
3303
|
local queued_stream
|
|
@@ -11473,6 +11826,28 @@ run_autonomous() {
|
|
|
11473
11826
|
# file, which the gate treats as inconclusive (pass-through).
|
|
11474
11827
|
local _start_sha_file=".loki/state/start-sha"
|
|
11475
11828
|
mkdir -p ".loki/state"
|
|
11829
|
+
|
|
11830
|
+
# Delegate-then-notify (Slice 3): LOKI_DELEGATE_BRANCH=1 (default OFF)
|
|
11831
|
+
# isolates this run's work on a fresh branch loki/delegate-<timestamp> so the
|
|
11832
|
+
# user's working branch stays clean. Created IN-PROCESS (plain git, no
|
|
11833
|
+
# detached child) only on a genuine fresh run (ITERATION_COUNT==0) so a
|
|
11834
|
+
# resume does not spawn a new branch each time. Best-effort: a non-git repo,
|
|
11835
|
+
# dirty tree that blocks checkout, or any git failure leaves the run on the
|
|
11836
|
+
# current branch (default behavior preserved). Done BEFORE the start-sha
|
|
11837
|
+
# capture so the diff window baselines to the new branch HEAD.
|
|
11838
|
+
if [ "${LOKI_DELEGATE_BRANCH:-0}" = "1" ] && [ "${ITERATION_COUNT:-0}" -eq 0 ]; then
|
|
11839
|
+
if (cd "${TARGET_DIR:-.}" && git rev-parse --git-dir) >/dev/null 2>&1; then
|
|
11840
|
+
local _delegate_branch="loki/delegate-$(date +%Y%m%d-%H%M%S)"
|
|
11841
|
+
if (cd "${TARGET_DIR:-.}" && git checkout -b "$_delegate_branch") >/dev/null 2>&1; then
|
|
11842
|
+
_LOKI_DELEGATE_BRANCH_NAME="$_delegate_branch"
|
|
11843
|
+
export _LOKI_DELEGATE_BRANCH_NAME
|
|
11844
|
+
log_info "LOKI_DELEGATE_BRANCH=1: isolated work on new branch '$_delegate_branch'"
|
|
11845
|
+
else
|
|
11846
|
+
log_warn "LOKI_DELEGATE_BRANCH=1: could not create branch (dirty tree or git error); continuing on current branch."
|
|
11847
|
+
fi
|
|
11848
|
+
fi
|
|
11849
|
+
fi
|
|
11850
|
+
|
|
11476
11851
|
if [ "${ITERATION_COUNT:-0}" -eq 0 ] || [ ! -s "$_start_sha_file" ]; then
|
|
11477
11852
|
(cd "${TARGET_DIR:-.}" && git rev-parse HEAD 2>/dev/null) > "$_start_sha_file" 2>/dev/null || true
|
|
11478
11853
|
fi
|
|
@@ -11556,6 +11931,13 @@ except Exception as exc:
|
|
|
11556
11931
|
# Check max iterations before starting
|
|
11557
11932
|
if check_max_iterations; then
|
|
11558
11933
|
log_error "Max iterations already reached. Reset with: rm .loki/autonomy-state.json"
|
|
11934
|
+
# Delegate-then-notify: terminal state. Mirror the in-loop max-iterations
|
|
11935
|
+
# site so a detached (--bg) run still writes COMPLETION.txt + fires the
|
|
11936
|
+
# ping on this pre-loop exit. _LOKI_RUN_START_SHA is already exported
|
|
11937
|
+
# above (runner init), so the diff window is correct. This return is
|
|
11938
|
+
# mutually exclusive with the in-loop site (it returns before the loop),
|
|
11939
|
+
# so there is no double-emit.
|
|
11940
|
+
emit_completion_summary max_iterations
|
|
11559
11941
|
return 1
|
|
11560
11942
|
fi
|
|
11561
11943
|
|
|
@@ -11583,6 +11965,9 @@ except Exception as exc:
|
|
|
11583
11965
|
# Check max iterations
|
|
11584
11966
|
if check_max_iterations; then
|
|
11585
11967
|
save_state $retry "max_iterations_reached" 0
|
|
11968
|
+
# Delegate-then-notify: terminal state, write summary + ping so a
|
|
11969
|
+
# detached run tells the user it stopped at the iteration cap.
|
|
11970
|
+
emit_completion_summary max_iterations
|
|
11586
11971
|
return 0
|
|
11587
11972
|
fi
|
|
11588
11973
|
|
|
@@ -12479,7 +12864,11 @@ if __name__ == "__main__":
|
|
|
12479
12864
|
log_info "Council voted to stop (convergence detected + requirements verified)"
|
|
12480
12865
|
log_info "Running memory consolidation..."
|
|
12481
12866
|
run_memory_consolidation
|
|
12482
|
-
|
|
12867
|
+
# Delegate-then-notify: optional local PR on success, then the
|
|
12868
|
+
# durable summary + desktop ping. on_run_complete is idempotent
|
|
12869
|
+
# and only opens a PR when LOKI_DELEGATE_PR=1 (default OFF).
|
|
12870
|
+
on_run_complete
|
|
12871
|
+
emit_completion_summary complete
|
|
12483
12872
|
save_state $retry "council_approved" 0
|
|
12484
12873
|
rm -f "$iter_output" 2>/dev/null
|
|
12485
12874
|
return 0
|
|
@@ -12542,7 +12931,10 @@ if __name__ == "__main__":
|
|
|
12542
12931
|
# Run memory consolidation on successful completion
|
|
12543
12932
|
log_info "Running memory consolidation..."
|
|
12544
12933
|
run_memory_consolidation
|
|
12545
|
-
|
|
12934
|
+
# Delegate-then-notify: optional local PR on success, then the
|
|
12935
|
+
# durable summary + desktop ping (see on_run_complete).
|
|
12936
|
+
on_run_complete
|
|
12937
|
+
emit_completion_summary complete
|
|
12546
12938
|
save_state $retry "completion_promise_fulfilled" 0
|
|
12547
12939
|
rm -f "$iter_output" 2>/dev/null
|
|
12548
12940
|
return 0
|
|
@@ -12636,6 +13028,9 @@ if __name__ == "__main__":
|
|
|
12636
13028
|
|
|
12637
13029
|
log_error "Max retries ($MAX_RETRIES) exceeded"
|
|
12638
13030
|
save_state $retry "failed" 1
|
|
13031
|
+
# Delegate-then-notify: terminal failure. critical urgency so the desktop
|
|
13032
|
+
# ping is louder; the summary file records where the partial work landed.
|
|
13033
|
+
emit_completion_summary failed critical
|
|
12639
13034
|
return 1
|
|
12640
13035
|
}
|
|
12641
13036
|
|
|
@@ -12778,11 +13173,19 @@ check_human_intervention() {
|
|
|
12778
13173
|
log_warn "PAUSE file created by budget limit - NOT auto-clearing in perpetual mode"
|
|
12779
13174
|
log_warn "Budget limit reached. Remove .loki/signals/BUDGET_EXCEEDED and .loki/PAUSE to continue."
|
|
12780
13175
|
notify_intervention_needed "Budget limit reached - execution paused" 2>/dev/null || true
|
|
13176
|
+
# Genuinely blocking pause: write the durable intervention record
|
|
13177
|
+
# now (state-only; the ping above already fired). This is the
|
|
13178
|
+
# correct site for the durable file because the run actually halts
|
|
13179
|
+
# here until the operator clears the budget signal.
|
|
13180
|
+
build_completion_summary intervention 2>/dev/null || true
|
|
12781
13181
|
local pause_result
|
|
12782
13182
|
handle_pause
|
|
12783
13183
|
pause_result=$?
|
|
12784
13184
|
rm -f "$loki_dir/PAUSE"
|
|
12785
13185
|
if [ "$pause_result" -eq 1 ]; then
|
|
13186
|
+
# STOP requested DURING the pause: relabel the durable record
|
|
13187
|
+
# as stopped (state-only; the user typed STOP and is aware).
|
|
13188
|
+
build_completion_summary stopped 2>/dev/null || true
|
|
12786
13189
|
return 2
|
|
12787
13190
|
fi
|
|
12788
13191
|
return 1
|
|
@@ -12796,12 +13199,17 @@ check_human_intervention() {
|
|
|
12796
13199
|
fi
|
|
12797
13200
|
log_warn "PAUSE file detected - pausing execution"
|
|
12798
13201
|
notify_intervention_needed "Execution paused via PAUSE file"
|
|
13202
|
+
# Genuinely blocking pause: write the durable intervention record now
|
|
13203
|
+
# (state-only; the ping above already fired).
|
|
13204
|
+
build_completion_summary intervention 2>/dev/null || true
|
|
12799
13205
|
local pause_result
|
|
12800
13206
|
handle_pause
|
|
12801
13207
|
pause_result=$?
|
|
12802
13208
|
rm -f "$loki_dir/PAUSE"
|
|
12803
13209
|
if [ "$pause_result" -eq 1 ]; then
|
|
12804
|
-
# STOP was requested during pause
|
|
13210
|
+
# STOP was requested during pause: relabel the durable record as
|
|
13211
|
+
# stopped (state-only; the user typed STOP and is aware).
|
|
13212
|
+
build_completion_summary stopped 2>/dev/null || true
|
|
12805
13213
|
return 2
|
|
12806
13214
|
fi
|
|
12807
13215
|
return 1
|
|
@@ -12814,11 +13222,16 @@ check_human_intervention() {
|
|
|
12814
13222
|
rm -f "$loki_dir/PAUSE_AT_CHECKPOINT"
|
|
12815
13223
|
notify_intervention_needed "Execution paused at checkpoint"
|
|
12816
13224
|
touch "$loki_dir/PAUSE"
|
|
13225
|
+
# Genuinely blocking pause: write the durable intervention record now
|
|
13226
|
+
# (state-only; the ping above already fired).
|
|
13227
|
+
build_completion_summary intervention 2>/dev/null || true
|
|
12817
13228
|
local pause_result
|
|
12818
13229
|
handle_pause
|
|
12819
13230
|
pause_result=$?
|
|
12820
13231
|
rm -f "$loki_dir/PAUSE"
|
|
12821
13232
|
if [ "$pause_result" -eq 1 ]; then
|
|
13233
|
+
# STOP requested during pause: relabel as stopped (state-only).
|
|
13234
|
+
build_completion_summary stopped 2>/dev/null || true
|
|
12822
13235
|
return 2
|
|
12823
13236
|
fi
|
|
12824
13237
|
return 1
|
|
@@ -12886,7 +13299,11 @@ check_human_intervention() {
|
|
|
12886
13299
|
fi
|
|
12887
13300
|
log_info "Running memory consolidation..."
|
|
12888
13301
|
run_memory_consolidation
|
|
12889
|
-
|
|
13302
|
+
# Delegate-then-notify: force-review approval is a real completion
|
|
13303
|
+
# (returns 2, which the run loop maps to a clean return 0). Treat it
|
|
13304
|
+
# like the other success exits: optional local PR + summary + ping.
|
|
13305
|
+
on_run_complete
|
|
13306
|
+
emit_completion_summary complete
|
|
12890
13307
|
save_state ${RETRY_COUNT:-0} "council_force_approved" 0
|
|
12891
13308
|
return 2 # Stop
|
|
12892
13309
|
fi
|
|
@@ -12897,6 +13314,12 @@ check_human_intervention() {
|
|
|
12897
13314
|
if [ -f "$loki_dir/STOP" ]; then
|
|
12898
13315
|
log_warn "STOP file detected - stopping execution"
|
|
12899
13316
|
rm -f "$loki_dir/STOP"
|
|
13317
|
+
# Delegate-then-notify: an explicit STOP file is a deliberate stop, but
|
|
13318
|
+
# a detached (--bg) user still benefits from a summary of partial work.
|
|
13319
|
+
# NOTE: the SIGTERM/`loki stop` group-kill path (cleanup handler near the
|
|
13320
|
+
# end of this file) is intentionally NOT notified: that user is at a
|
|
13321
|
+
# terminal issuing the stop and is already aware.
|
|
13322
|
+
emit_completion_summary stopped
|
|
12900
13323
|
return 2
|
|
12901
13324
|
fi
|
|
12902
13325
|
|
|
@@ -13364,6 +13787,9 @@ main() {
|
|
|
13364
13787
|
echo -e " ${DIM}Logs:${NC} tail -f $log_file"
|
|
13365
13788
|
echo -e " ${DIM}Status:${NC} cat .loki/STATUS.txt"
|
|
13366
13789
|
echo ""
|
|
13790
|
+
echo -e "${GREEN}You will be notified when done (or if input is needed).${NC}"
|
|
13791
|
+
echo -e " ${DIM}Summary on completion:${NC} cat .loki/COMPLETION.txt"
|
|
13792
|
+
echo ""
|
|
13367
13793
|
|
|
13368
13794
|
exit 0
|
|
13369
13795
|
fi
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.23.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var f8=Object.defineProperty;var u8=($)=>$;function c8($,Q){this[$]=u8.bind(null,Q)}var g=($,Q)=>{for(var Z in Q)f8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:c8.bind(Q,Z)})};var k=($,Q)=>()=>($&&(Q=$($=0)),Q);var X1=import.meta.require;var F$={};g(F$,{lokiDir:()=>P,homeLokiDir:()=>o1,findRepoRootForVersion:()=>d1,REPO_ROOT:()=>f});import{resolve as n,dirname as l1}from"path";import{fileURLToPath as p8}from"url";import{existsSync as L1}from"fs";import{homedir as l8}from"os";function d8(){let $=j$;for(let Q=0;Q<6;Q++){if(L1(n($,"VERSION"))&&L1(n($,"autonomy/run.sh")))return $;let Z=l1($);if(Z===$)break;$=Z}return n(j$,"..","..","..")}function d1($){let Q=$;for(let Z=0;Z<6;Z++){if(L1(n(Q,"VERSION"))&&L1(n(Q,"autonomy/run.sh")))return Q;let z=l1(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o1(){return n(l8(),".loki")}var j$,f;var y=k(()=>{j$=l1(p8(import.meta.url));f=d8()});import{readFileSync as o8}from"fs";import{resolve as n8,dirname as a8}from"path";import{fileURLToPath as s8}from"url";function k1(){if($1!==null)return $1;let $="7.
|
|
2
|
+
var f8=Object.defineProperty;var u8=($)=>$;function c8($,Q){this[$]=u8.bind(null,Q)}var g=($,Q)=>{for(var Z in Q)f8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:c8.bind(Q,Z)})};var k=($,Q)=>()=>($&&(Q=$($=0)),Q);var X1=import.meta.require;var F$={};g(F$,{lokiDir:()=>P,homeLokiDir:()=>o1,findRepoRootForVersion:()=>d1,REPO_ROOT:()=>f});import{resolve as n,dirname as l1}from"path";import{fileURLToPath as p8}from"url";import{existsSync as L1}from"fs";import{homedir as l8}from"os";function d8(){let $=j$;for(let Q=0;Q<6;Q++){if(L1(n($,"VERSION"))&&L1(n($,"autonomy/run.sh")))return $;let Z=l1($);if(Z===$)break;$=Z}return n(j$,"..","..","..")}function d1($){let Q=$;for(let Z=0;Z<6;Z++){if(L1(n(Q,"VERSION"))&&L1(n(Q,"autonomy/run.sh")))return Q;let z=l1(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o1(){return n(l8(),".loki")}var j$,f;var y=k(()=>{j$=l1(p8(import.meta.url));f=d8()});import{readFileSync as o8}from"fs";import{resolve as n8,dirname as a8}from"path";import{fileURLToPath as s8}from"url";function k1(){if($1!==null)return $1;let $="7.23.0";if(typeof $==="string"&&$.length>0)return $1=$,$1;try{let Q=a8(s8(import.meta.url)),Z=d1(Q);$1=o8(n8(Z,"VERSION"),"utf-8").trim()}catch{$1="unknown"}return $1}var $1=null;var n1=k(()=>{y()});var E$={};g(E$,{runOrThrow:()=>t8,run:()=>j,commandVersion:()=>i8,commandExists:()=>v,ShellError:()=>a1});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,K;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}K=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[H,X,q]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:H,stderr:X,exitCode:q}}finally{if(z)clearTimeout(z);if(K)clearTimeout(K)}}async function t8($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function v($){let Q=r8($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function r8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function i8($,Q="--version"){if(!await v($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a1;var d=k(()=>{a1=class a1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return e8?"":$}var e8,T,N,_,KZ,A,R,h,J;var c=k(()=>{e8=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),N=a("\x1B[0;32m"),_=a("\x1B[1;33m"),KZ=a("\x1B[0;34m"),A=a("\x1B[0;36m"),R=a("\x1B[1m"),h=a("\x1B[2m"),J=a("\x1B[0m")});import{existsSync as U7}from"fs";async function Q1(){if(B1!==void 0)return B1;let $="/opt/homebrew/bin/python3.12";if(U7($))return B1=$,$;let Q=await v("python3.12");if(Q)return B1=Q,Q;let Z=await v("python3");return B1=Z,Z}async function Z1($,Q={}){let Z=await Q1();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B1;var H1=k(()=>{d()});var d$={};g(d$,{runStatus:()=>N7});import{existsSync as b,readFileSync as q1,readdirSync as v$,statSync as f$}from"fs";import{resolve as D,basename as P7}from"path";import{homedir as L7}from"os";async function j7(){if(await v("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -787,4 +787,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
787
787
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
788
788
|
`),process.stderr.write(v8),2}}g$();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var l3=await p3(Bun.argv.slice(2));process.exit(l3);
|
|
789
789
|
|
|
790
|
-
//# debugId=
|
|
790
|
+
//# debugId=54BA5C5C5E9F714F64756E2164756E21
|
package/mcp/__init__.py
CHANGED