loki-mode 7.21.0 → 7.22.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/run.sh +345 -4
- 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/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.22.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -383,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
|
386
|
-
**v7.
|
|
386
|
+
**v7.22.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.22.0
|
package/autonomy/run.sh
CHANGED
|
@@ -2355,6 +2355,15 @@ notify_all_complete() {
|
|
|
2355
2355
|
|
|
2356
2356
|
notify_intervention_needed() {
|
|
2357
2357
|
local reason="$1"
|
|
2358
|
+
# Delegate-then-notify: this helper ONLY fires the (gated) desktop ping. It
|
|
2359
|
+
# deliberately does NOT write the durable COMPLETION.txt / completion.json
|
|
2360
|
+
# record. Reason: notify_intervention_needed is also called from NON-terminal
|
|
2361
|
+
# sites (the perpetual-mode PAUSE auto-clear branch, uncertainty escalation)
|
|
2362
|
+
# where the run keeps going. Writing a "Needs input" durable file there would
|
|
2363
|
+
# falsely tell a detached user the run is done / blocked when it is not. The
|
|
2364
|
+
# durable intervention write now lives only at the genuinely blocking pause
|
|
2365
|
+
# sites (immediately before handle_pause), so the durable state matches the
|
|
2366
|
+
# actual run state.
|
|
2358
2367
|
send_notification "Intervention Needed" "$reason" "critical"
|
|
2359
2368
|
}
|
|
2360
2369
|
|
|
@@ -2363,6 +2372,265 @@ notify_rate_limit() {
|
|
|
2363
2372
|
send_notification "Rate Limited" "Waiting ${wait_time}s before retry" "normal"
|
|
2364
2373
|
}
|
|
2365
2374
|
|
|
2375
|
+
#===============================================================================
|
|
2376
|
+
# Delegate-then-notify: completion summary (Release 2, "delegate then notify")
|
|
2377
|
+
#
|
|
2378
|
+
# build_completion_summary <outcome> writes two durable files that survive a
|
|
2379
|
+
# detached (--bg) run where the terminal is gone and a bell would be useless:
|
|
2380
|
+
# .loki/COMPLETION.txt human plain text (no emojis, no dashes)
|
|
2381
|
+
# .loki/state/completion.json machine-readable record of the same facts
|
|
2382
|
+
# It also exports two strings for send_notification to consume:
|
|
2383
|
+
# _LOKI_SUMMARY_TITLE short notification subtitle
|
|
2384
|
+
# _LOKI_SUMMARY_BODY short notification body (outcome + branch + file count)
|
|
2385
|
+
#
|
|
2386
|
+
# All git reads are best-effort and non-fatal. The diff window is the run-start
|
|
2387
|
+
# SHA captured once at runner init (_LOKI_RUN_START_SHA); we REUSE it and never
|
|
2388
|
+
# recapture, so the reported diff matches the evidence gate's window exactly.
|
|
2389
|
+
#
|
|
2390
|
+
# This function NEVER sends a notification and NEVER gates on
|
|
2391
|
+
# NOTIFICATIONS_ENABLED: the files are state, not a notification, and must be
|
|
2392
|
+
# written even when desktop notifications are disabled. emit_completion_summary
|
|
2393
|
+
# below is the wrapper that writes the files AND (gated) fires the desktop ping.
|
|
2394
|
+
#===============================================================================
|
|
2395
|
+
build_completion_summary() {
|
|
2396
|
+
local outcome="${1:-complete}"
|
|
2397
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
2398
|
+
mkdir -p "$loki_dir/state" 2>/dev/null || true
|
|
2399
|
+
|
|
2400
|
+
# Human-readable outcome label and notification title.
|
|
2401
|
+
local outcome_label notify_title
|
|
2402
|
+
case "$outcome" in
|
|
2403
|
+
complete) outcome_label="Completed"; notify_title="Run complete" ;;
|
|
2404
|
+
max_iterations) outcome_label="Max iterations"; notify_title="Run stopped (max iterations)" ;;
|
|
2405
|
+
stopped) outcome_label="Stopped"; notify_title="Run stopped" ;;
|
|
2406
|
+
failed) outcome_label="Failed"; notify_title="Run failed" ;;
|
|
2407
|
+
intervention) outcome_label="Needs input"; notify_title="Input needed" ;;
|
|
2408
|
+
*) outcome_label="$outcome"; notify_title="Run finished" ;;
|
|
2409
|
+
esac
|
|
2410
|
+
|
|
2411
|
+
# Branch + diff stats vs the run-start SHA (best-effort; non-git or empty
|
|
2412
|
+
# baseline yields empty values, which we render as "unknown"/"0").
|
|
2413
|
+
local start_sha="${_LOKI_RUN_START_SHA:-}"
|
|
2414
|
+
local branch="" head_sha="" diff_stat="" files_changed=0 insertions=0 deletions=0 review_cmd=""
|
|
2415
|
+
branch="$( (cd "${TARGET_DIR:-.}" && git rev-parse --abbrev-ref HEAD) 2>/dev/null || true )"
|
|
2416
|
+
[ -z "$branch" ] && branch="unknown"
|
|
2417
|
+
head_sha="$( (cd "${TARGET_DIR:-.}" && git rev-parse HEAD) 2>/dev/null || true )"
|
|
2418
|
+
|
|
2419
|
+
if [ -n "$start_sha" ]; then
|
|
2420
|
+
diff_stat="$( (cd "${TARGET_DIR:-.}" && git diff --stat "${start_sha}..HEAD") 2>/dev/null || true )"
|
|
2421
|
+
# Parse the git diff --shortstat tail for counts (locale-stable enough
|
|
2422
|
+
# for our display; failures leave the zeros in place).
|
|
2423
|
+
local shortstat
|
|
2424
|
+
shortstat="$( (cd "${TARGET_DIR:-.}" && git diff --shortstat "${start_sha}..HEAD") 2>/dev/null || true )"
|
|
2425
|
+
if [ -n "$shortstat" ]; then
|
|
2426
|
+
files_changed="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' | head -1)"
|
|
2427
|
+
insertions="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' | head -1)"
|
|
2428
|
+
deletions="$(printf '%s\n' "$shortstat" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' | head -1)"
|
|
2429
|
+
fi
|
|
2430
|
+
review_cmd="git diff ${start_sha}..HEAD"
|
|
2431
|
+
else
|
|
2432
|
+
review_cmd="git diff HEAD"
|
|
2433
|
+
fi
|
|
2434
|
+
[ -z "$files_changed" ] && files_changed=0
|
|
2435
|
+
[ -z "$insertions" ] && insertions=0
|
|
2436
|
+
[ -z "$deletions" ] && deletions=0
|
|
2437
|
+
|
|
2438
|
+
# Task counts: reuse the SAME queue reads as update_status_file.
|
|
2439
|
+
local pending=0 in_progress=0 completed=0 failed=0
|
|
2440
|
+
[ -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")
|
|
2441
|
+
[ -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")
|
|
2442
|
+
[ -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")
|
|
2443
|
+
[ -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")
|
|
2444
|
+
|
|
2445
|
+
# Optional delegate-mode extras populated by Slice 3 (branch isolation / PR).
|
|
2446
|
+
local delegate_branch="${_LOKI_DELEGATE_BRANCH_NAME:-}"
|
|
2447
|
+
local pr_url="${_LOKI_DELEGATE_PR_URL:-}"
|
|
2448
|
+
|
|
2449
|
+
local ts
|
|
2450
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)"
|
|
2451
|
+
|
|
2452
|
+
# ---- Durable human-readable file: .loki/COMPLETION.txt --------------------
|
|
2453
|
+
{
|
|
2454
|
+
echo "Loki Mode run summary"
|
|
2455
|
+
echo "====================="
|
|
2456
|
+
echo ""
|
|
2457
|
+
echo "Outcome: $outcome_label"
|
|
2458
|
+
echo "Branch: $branch"
|
|
2459
|
+
echo "Files changed: $files_changed (+$insertions / -$deletions)"
|
|
2460
|
+
echo "Finished: $ts"
|
|
2461
|
+
echo ""
|
|
2462
|
+
if [ -n "$delegate_branch" ]; then
|
|
2463
|
+
echo "Delegate branch: $delegate_branch"
|
|
2464
|
+
fi
|
|
2465
|
+
if [ -n "$pr_url" ]; then
|
|
2466
|
+
echo "Pull request: $pr_url"
|
|
2467
|
+
elif [ "$outcome" = "complete" ]; then
|
|
2468
|
+
echo "Pull request: not opened (set LOKI_DELEGATE_PR=1 to open one)"
|
|
2469
|
+
fi
|
|
2470
|
+
echo ""
|
|
2471
|
+
echo "Tasks: pending=$pending in_progress=$in_progress completed=$completed failed=$failed"
|
|
2472
|
+
echo ""
|
|
2473
|
+
echo "Review the work:"
|
|
2474
|
+
echo " $review_cmd"
|
|
2475
|
+
echo ""
|
|
2476
|
+
if [ -n "$diff_stat" ]; then
|
|
2477
|
+
echo "Diff stat:"
|
|
2478
|
+
echo "$diff_stat"
|
|
2479
|
+
else
|
|
2480
|
+
echo "Diff stat: (no changes detected vs run start, or git unavailable)"
|
|
2481
|
+
fi
|
|
2482
|
+
} > "$loki_dir/COMPLETION.txt" 2>/dev/null || true
|
|
2483
|
+
|
|
2484
|
+
# ---- Durable machine-readable file: .loki/state/completion.json -----------
|
|
2485
|
+
_LOKI_CS_OUTCOME="$outcome" \
|
|
2486
|
+
_LOKI_CS_BRANCH="$branch" \
|
|
2487
|
+
_LOKI_CS_START_SHA="$start_sha" \
|
|
2488
|
+
_LOKI_CS_HEAD_SHA="$head_sha" \
|
|
2489
|
+
_LOKI_CS_FILES="$files_changed" \
|
|
2490
|
+
_LOKI_CS_INS="$insertions" \
|
|
2491
|
+
_LOKI_CS_DEL="$deletions" \
|
|
2492
|
+
_LOKI_CS_REVIEW="$review_cmd" \
|
|
2493
|
+
_LOKI_CS_DELEGATE_BRANCH="$delegate_branch" \
|
|
2494
|
+
_LOKI_CS_PR_URL="$pr_url" \
|
|
2495
|
+
_LOKI_CS_TS="$ts" \
|
|
2496
|
+
_LOKI_CS_OUT_FILE="$loki_dir/state/completion.json" \
|
|
2497
|
+
python3 -c "
|
|
2498
|
+
import json, os, tempfile
|
|
2499
|
+
out = os.environ['_LOKI_CS_OUT_FILE']
|
|
2500
|
+
def i(v):
|
|
2501
|
+
try: return int(v)
|
|
2502
|
+
except (TypeError, ValueError): return 0
|
|
2503
|
+
rec = {
|
|
2504
|
+
'outcome': os.environ.get('_LOKI_CS_OUTCOME', ''),
|
|
2505
|
+
'branch': os.environ.get('_LOKI_CS_BRANCH', ''),
|
|
2506
|
+
'start_sha': os.environ.get('_LOKI_CS_START_SHA', ''),
|
|
2507
|
+
'head_sha': os.environ.get('_LOKI_CS_HEAD_SHA', ''),
|
|
2508
|
+
'files_changed': i(os.environ.get('_LOKI_CS_FILES')),
|
|
2509
|
+
'insertions': i(os.environ.get('_LOKI_CS_INS')),
|
|
2510
|
+
'deletions': i(os.environ.get('_LOKI_CS_DEL')),
|
|
2511
|
+
'review_cmd': os.environ.get('_LOKI_CS_REVIEW', ''),
|
|
2512
|
+
'delegate_branch': os.environ.get('_LOKI_CS_DELEGATE_BRANCH', ''),
|
|
2513
|
+
'pr_url': os.environ.get('_LOKI_CS_PR_URL', ''),
|
|
2514
|
+
'timestamp': os.environ.get('_LOKI_CS_TS', ''),
|
|
2515
|
+
}
|
|
2516
|
+
d = os.path.dirname(out)
|
|
2517
|
+
fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
|
|
2518
|
+
with os.fdopen(fd, 'w') as f:
|
|
2519
|
+
json.dump(rec, f, indent=2)
|
|
2520
|
+
os.replace(tmp, out)
|
|
2521
|
+
" 2>/dev/null || true
|
|
2522
|
+
|
|
2523
|
+
# ---- Short strings for the desktop notification --------------------------
|
|
2524
|
+
# Desktop body stays terse; full detail lives in COMPLETION.txt.
|
|
2525
|
+
_LOKI_SUMMARY_TITLE="$notify_title"
|
|
2526
|
+
_LOKI_SUMMARY_BODY="${outcome_label} on ${branch}: ${files_changed} files changed"
|
|
2527
|
+
if [ -n "$pr_url" ]; then
|
|
2528
|
+
_LOKI_SUMMARY_BODY="${_LOKI_SUMMARY_BODY}. PR: ${pr_url}"
|
|
2529
|
+
fi
|
|
2530
|
+
export _LOKI_SUMMARY_TITLE _LOKI_SUMMARY_BODY
|
|
2531
|
+
return 0
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
#===============================================================================
|
|
2535
|
+
# emit_completion_summary <outcome> [urgency]
|
|
2536
|
+
#
|
|
2537
|
+
# The single entry point every terminal state calls. It ALWAYS writes the
|
|
2538
|
+
# durable summary files (state, not a notification) and then fires ONE desktop
|
|
2539
|
+
# notification gated by the existing LOKI_NOTIFICATIONS flag (send_notification
|
|
2540
|
+
# already short-circuits when disabled, so the gate is implicit but explicit
|
|
2541
|
+
# here for clarity). Centralizing this keeps the success-only PR side effect
|
|
2542
|
+
# (Slice 3) in one place and prevents duplicate notifications.
|
|
2543
|
+
#===============================================================================
|
|
2544
|
+
emit_completion_summary() {
|
|
2545
|
+
local outcome="${1:-complete}"
|
|
2546
|
+
local urgency="${2:-normal}"
|
|
2547
|
+
build_completion_summary "$outcome"
|
|
2548
|
+
send_notification "${_LOKI_SUMMARY_TITLE:-Run finished}" "${_LOKI_SUMMARY_BODY:-}" "$urgency"
|
|
2549
|
+
return 0
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
#===============================================================================
|
|
2553
|
+
# on_run_complete (Slice 3: opt-in local git output on success)
|
|
2554
|
+
#
|
|
2555
|
+
# Called from every SUCCESS exit BEFORE emit_completion_summary so the PR url it
|
|
2556
|
+
# discovers is folded into the summary. Default behavior is a no-op: it only
|
|
2557
|
+
# acts when LOKI_DELEGATE_PR=1.
|
|
2558
|
+
#
|
|
2559
|
+
# LOKI_DELEGATE_PR=1 opens a LOCAL pull request from the user's machine, only if:
|
|
2560
|
+
# - this is a GitHub repo (gh + a github.com remote), AND
|
|
2561
|
+
# - `gh auth status` succeeds, AND
|
|
2562
|
+
# - the current branch is not main/master (never PR a default branch to itself)
|
|
2563
|
+
# It mirrors the proven pattern at autonomy/loki:5524-5527: push the branch,
|
|
2564
|
+
# then `gh pr create --head <branch>`. NO auto-merge. Every call is best-effort
|
|
2565
|
+
# (`|| true`); failures never block completion. This is a single sanctioned
|
|
2566
|
+
# local network call, never CI.
|
|
2567
|
+
#
|
|
2568
|
+
# Reconciliation with the existing GITHUB_PR path (run.sh create_github_pr,
|
|
2569
|
+
# invoked after run_autonomous returns when LOKI_GITHUB_PR=true): if GITHUB_PR
|
|
2570
|
+
# is already true we DEFER to that path and do nothing here, so a user who set
|
|
2571
|
+
# both knobs never gets a double PR.
|
|
2572
|
+
#===============================================================================
|
|
2573
|
+
on_run_complete() {
|
|
2574
|
+
# Default OFF.
|
|
2575
|
+
if [ "${LOKI_DELEGATE_PR:-0}" != "1" ]; then
|
|
2576
|
+
return 0
|
|
2577
|
+
fi
|
|
2578
|
+
# Defer to the existing dedicated PR path to avoid a double PR.
|
|
2579
|
+
if [ "${GITHUB_PR:-false}" = "true" ]; then
|
|
2580
|
+
return 0
|
|
2581
|
+
fi
|
|
2582
|
+
# Network-call timeout guard: a stalled network / auth prompt would
|
|
2583
|
+
# otherwise hang the completion path indefinitely in --bg. Run each network
|
|
2584
|
+
# call through `timeout 30` when available; fall back to the bare call if
|
|
2585
|
+
# timeout is not installed (a local wrapper keeps this set -u safe on bash
|
|
2586
|
+
# 3.2, where an empty array expansion would error). Keeps every existing
|
|
2587
|
+
# `|| true` non-fatal behavior.
|
|
2588
|
+
_loki_net() {
|
|
2589
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
2590
|
+
timeout 30 "$@"
|
|
2591
|
+
else
|
|
2592
|
+
"$@"
|
|
2593
|
+
fi
|
|
2594
|
+
}
|
|
2595
|
+
# Require gh + auth.
|
|
2596
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
2597
|
+
return 0
|
|
2598
|
+
fi
|
|
2599
|
+
if ! (cd "${TARGET_DIR:-.}" && _loki_net gh auth status) >/dev/null 2>&1; then
|
|
2600
|
+
return 0
|
|
2601
|
+
fi
|
|
2602
|
+
# Require a GitHub remote (skip silently on non-GitHub repos).
|
|
2603
|
+
local remote_url
|
|
2604
|
+
remote_url="$( (cd "${TARGET_DIR:-.}" && git config --get remote.origin.url) 2>/dev/null || true )"
|
|
2605
|
+
case "$remote_url" in
|
|
2606
|
+
*github.com*) : ;;
|
|
2607
|
+
*) return 0 ;;
|
|
2608
|
+
esac
|
|
2609
|
+
# Resolve current branch; never PR a default branch to itself.
|
|
2610
|
+
local branch
|
|
2611
|
+
branch="$( (cd "${TARGET_DIR:-.}" && git rev-parse --abbrev-ref HEAD) 2>/dev/null || true )"
|
|
2612
|
+
case "$branch" in
|
|
2613
|
+
""|main|master|HEAD) return 0 ;;
|
|
2614
|
+
esac
|
|
2615
|
+
log_info "LOKI_DELEGATE_PR=1: opening a local pull request for branch '$branch'..."
|
|
2616
|
+
# Push, then create. Non-interactive (no tty in --bg). Best-effort, each
|
|
2617
|
+
# network call bounded by the timeout guard above.
|
|
2618
|
+
(cd "${TARGET_DIR:-.}" && _loki_net git push -u origin "$branch") >/dev/null 2>&1 || true
|
|
2619
|
+
local pr_title
|
|
2620
|
+
pr_title="Loki Mode: ${branch}"
|
|
2621
|
+
local pr_url=""
|
|
2622
|
+
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 )"
|
|
2623
|
+
if [ -n "$pr_url" ]; then
|
|
2624
|
+
# Export so build_completion_summary folds the url into the summary.
|
|
2625
|
+
_LOKI_DELEGATE_PR_URL="$pr_url"
|
|
2626
|
+
export _LOKI_DELEGATE_PR_URL
|
|
2627
|
+
log_info "Pull request opened: $pr_url"
|
|
2628
|
+
else
|
|
2629
|
+
log_warn "LOKI_DELEGATE_PR=1: gh pr create did not return a URL (a PR may already exist for this branch)."
|
|
2630
|
+
fi
|
|
2631
|
+
return 0
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2366
2634
|
#===============================================================================
|
|
2367
2635
|
# Parallel Workflow Functions (Git Worktrees)
|
|
2368
2636
|
#===============================================================================
|
|
@@ -11473,6 +11741,28 @@ run_autonomous() {
|
|
|
11473
11741
|
# file, which the gate treats as inconclusive (pass-through).
|
|
11474
11742
|
local _start_sha_file=".loki/state/start-sha"
|
|
11475
11743
|
mkdir -p ".loki/state"
|
|
11744
|
+
|
|
11745
|
+
# Delegate-then-notify (Slice 3): LOKI_DELEGATE_BRANCH=1 (default OFF)
|
|
11746
|
+
# isolates this run's work on a fresh branch loki/delegate-<timestamp> so the
|
|
11747
|
+
# user's working branch stays clean. Created IN-PROCESS (plain git, no
|
|
11748
|
+
# detached child) only on a genuine fresh run (ITERATION_COUNT==0) so a
|
|
11749
|
+
# resume does not spawn a new branch each time. Best-effort: a non-git repo,
|
|
11750
|
+
# dirty tree that blocks checkout, or any git failure leaves the run on the
|
|
11751
|
+
# current branch (default behavior preserved). Done BEFORE the start-sha
|
|
11752
|
+
# capture so the diff window baselines to the new branch HEAD.
|
|
11753
|
+
if [ "${LOKI_DELEGATE_BRANCH:-0}" = "1" ] && [ "${ITERATION_COUNT:-0}" -eq 0 ]; then
|
|
11754
|
+
if (cd "${TARGET_DIR:-.}" && git rev-parse --git-dir) >/dev/null 2>&1; then
|
|
11755
|
+
local _delegate_branch="loki/delegate-$(date +%Y%m%d-%H%M%S)"
|
|
11756
|
+
if (cd "${TARGET_DIR:-.}" && git checkout -b "$_delegate_branch") >/dev/null 2>&1; then
|
|
11757
|
+
_LOKI_DELEGATE_BRANCH_NAME="$_delegate_branch"
|
|
11758
|
+
export _LOKI_DELEGATE_BRANCH_NAME
|
|
11759
|
+
log_info "LOKI_DELEGATE_BRANCH=1: isolated work on new branch '$_delegate_branch'"
|
|
11760
|
+
else
|
|
11761
|
+
log_warn "LOKI_DELEGATE_BRANCH=1: could not create branch (dirty tree or git error); continuing on current branch."
|
|
11762
|
+
fi
|
|
11763
|
+
fi
|
|
11764
|
+
fi
|
|
11765
|
+
|
|
11476
11766
|
if [ "${ITERATION_COUNT:-0}" -eq 0 ] || [ ! -s "$_start_sha_file" ]; then
|
|
11477
11767
|
(cd "${TARGET_DIR:-.}" && git rev-parse HEAD 2>/dev/null) > "$_start_sha_file" 2>/dev/null || true
|
|
11478
11768
|
fi
|
|
@@ -11556,6 +11846,13 @@ except Exception as exc:
|
|
|
11556
11846
|
# Check max iterations before starting
|
|
11557
11847
|
if check_max_iterations; then
|
|
11558
11848
|
log_error "Max iterations already reached. Reset with: rm .loki/autonomy-state.json"
|
|
11849
|
+
# Delegate-then-notify: terminal state. Mirror the in-loop max-iterations
|
|
11850
|
+
# site so a detached (--bg) run still writes COMPLETION.txt + fires the
|
|
11851
|
+
# ping on this pre-loop exit. _LOKI_RUN_START_SHA is already exported
|
|
11852
|
+
# above (runner init), so the diff window is correct. This return is
|
|
11853
|
+
# mutually exclusive with the in-loop site (it returns before the loop),
|
|
11854
|
+
# so there is no double-emit.
|
|
11855
|
+
emit_completion_summary max_iterations
|
|
11559
11856
|
return 1
|
|
11560
11857
|
fi
|
|
11561
11858
|
|
|
@@ -11583,6 +11880,9 @@ except Exception as exc:
|
|
|
11583
11880
|
# Check max iterations
|
|
11584
11881
|
if check_max_iterations; then
|
|
11585
11882
|
save_state $retry "max_iterations_reached" 0
|
|
11883
|
+
# Delegate-then-notify: terminal state, write summary + ping so a
|
|
11884
|
+
# detached run tells the user it stopped at the iteration cap.
|
|
11885
|
+
emit_completion_summary max_iterations
|
|
11586
11886
|
return 0
|
|
11587
11887
|
fi
|
|
11588
11888
|
|
|
@@ -12479,7 +12779,11 @@ if __name__ == "__main__":
|
|
|
12479
12779
|
log_info "Council voted to stop (convergence detected + requirements verified)"
|
|
12480
12780
|
log_info "Running memory consolidation..."
|
|
12481
12781
|
run_memory_consolidation
|
|
12482
|
-
|
|
12782
|
+
# Delegate-then-notify: optional local PR on success, then the
|
|
12783
|
+
# durable summary + desktop ping. on_run_complete is idempotent
|
|
12784
|
+
# and only opens a PR when LOKI_DELEGATE_PR=1 (default OFF).
|
|
12785
|
+
on_run_complete
|
|
12786
|
+
emit_completion_summary complete
|
|
12483
12787
|
save_state $retry "council_approved" 0
|
|
12484
12788
|
rm -f "$iter_output" 2>/dev/null
|
|
12485
12789
|
return 0
|
|
@@ -12542,7 +12846,10 @@ if __name__ == "__main__":
|
|
|
12542
12846
|
# Run memory consolidation on successful completion
|
|
12543
12847
|
log_info "Running memory consolidation..."
|
|
12544
12848
|
run_memory_consolidation
|
|
12545
|
-
|
|
12849
|
+
# Delegate-then-notify: optional local PR on success, then the
|
|
12850
|
+
# durable summary + desktop ping (see on_run_complete).
|
|
12851
|
+
on_run_complete
|
|
12852
|
+
emit_completion_summary complete
|
|
12546
12853
|
save_state $retry "completion_promise_fulfilled" 0
|
|
12547
12854
|
rm -f "$iter_output" 2>/dev/null
|
|
12548
12855
|
return 0
|
|
@@ -12636,6 +12943,9 @@ if __name__ == "__main__":
|
|
|
12636
12943
|
|
|
12637
12944
|
log_error "Max retries ($MAX_RETRIES) exceeded"
|
|
12638
12945
|
save_state $retry "failed" 1
|
|
12946
|
+
# Delegate-then-notify: terminal failure. critical urgency so the desktop
|
|
12947
|
+
# ping is louder; the summary file records where the partial work landed.
|
|
12948
|
+
emit_completion_summary failed critical
|
|
12639
12949
|
return 1
|
|
12640
12950
|
}
|
|
12641
12951
|
|
|
@@ -12778,11 +13088,19 @@ check_human_intervention() {
|
|
|
12778
13088
|
log_warn "PAUSE file created by budget limit - NOT auto-clearing in perpetual mode"
|
|
12779
13089
|
log_warn "Budget limit reached. Remove .loki/signals/BUDGET_EXCEEDED and .loki/PAUSE to continue."
|
|
12780
13090
|
notify_intervention_needed "Budget limit reached - execution paused" 2>/dev/null || true
|
|
13091
|
+
# Genuinely blocking pause: write the durable intervention record
|
|
13092
|
+
# now (state-only; the ping above already fired). This is the
|
|
13093
|
+
# correct site for the durable file because the run actually halts
|
|
13094
|
+
# here until the operator clears the budget signal.
|
|
13095
|
+
build_completion_summary intervention 2>/dev/null || true
|
|
12781
13096
|
local pause_result
|
|
12782
13097
|
handle_pause
|
|
12783
13098
|
pause_result=$?
|
|
12784
13099
|
rm -f "$loki_dir/PAUSE"
|
|
12785
13100
|
if [ "$pause_result" -eq 1 ]; then
|
|
13101
|
+
# STOP requested DURING the pause: relabel the durable record
|
|
13102
|
+
# as stopped (state-only; the user typed STOP and is aware).
|
|
13103
|
+
build_completion_summary stopped 2>/dev/null || true
|
|
12786
13104
|
return 2
|
|
12787
13105
|
fi
|
|
12788
13106
|
return 1
|
|
@@ -12796,12 +13114,17 @@ check_human_intervention() {
|
|
|
12796
13114
|
fi
|
|
12797
13115
|
log_warn "PAUSE file detected - pausing execution"
|
|
12798
13116
|
notify_intervention_needed "Execution paused via PAUSE file"
|
|
13117
|
+
# Genuinely blocking pause: write the durable intervention record now
|
|
13118
|
+
# (state-only; the ping above already fired).
|
|
13119
|
+
build_completion_summary intervention 2>/dev/null || true
|
|
12799
13120
|
local pause_result
|
|
12800
13121
|
handle_pause
|
|
12801
13122
|
pause_result=$?
|
|
12802
13123
|
rm -f "$loki_dir/PAUSE"
|
|
12803
13124
|
if [ "$pause_result" -eq 1 ]; then
|
|
12804
|
-
# STOP was requested during pause
|
|
13125
|
+
# STOP was requested during pause: relabel the durable record as
|
|
13126
|
+
# stopped (state-only; the user typed STOP and is aware).
|
|
13127
|
+
build_completion_summary stopped 2>/dev/null || true
|
|
12805
13128
|
return 2
|
|
12806
13129
|
fi
|
|
12807
13130
|
return 1
|
|
@@ -12814,11 +13137,16 @@ check_human_intervention() {
|
|
|
12814
13137
|
rm -f "$loki_dir/PAUSE_AT_CHECKPOINT"
|
|
12815
13138
|
notify_intervention_needed "Execution paused at checkpoint"
|
|
12816
13139
|
touch "$loki_dir/PAUSE"
|
|
13140
|
+
# Genuinely blocking pause: write the durable intervention record now
|
|
13141
|
+
# (state-only; the ping above already fired).
|
|
13142
|
+
build_completion_summary intervention 2>/dev/null || true
|
|
12817
13143
|
local pause_result
|
|
12818
13144
|
handle_pause
|
|
12819
13145
|
pause_result=$?
|
|
12820
13146
|
rm -f "$loki_dir/PAUSE"
|
|
12821
13147
|
if [ "$pause_result" -eq 1 ]; then
|
|
13148
|
+
# STOP requested during pause: relabel as stopped (state-only).
|
|
13149
|
+
build_completion_summary stopped 2>/dev/null || true
|
|
12822
13150
|
return 2
|
|
12823
13151
|
fi
|
|
12824
13152
|
return 1
|
|
@@ -12886,7 +13214,11 @@ check_human_intervention() {
|
|
|
12886
13214
|
fi
|
|
12887
13215
|
log_info "Running memory consolidation..."
|
|
12888
13216
|
run_memory_consolidation
|
|
12889
|
-
|
|
13217
|
+
# Delegate-then-notify: force-review approval is a real completion
|
|
13218
|
+
# (returns 2, which the run loop maps to a clean return 0). Treat it
|
|
13219
|
+
# like the other success exits: optional local PR + summary + ping.
|
|
13220
|
+
on_run_complete
|
|
13221
|
+
emit_completion_summary complete
|
|
12890
13222
|
save_state ${RETRY_COUNT:-0} "council_force_approved" 0
|
|
12891
13223
|
return 2 # Stop
|
|
12892
13224
|
fi
|
|
@@ -12897,6 +13229,12 @@ check_human_intervention() {
|
|
|
12897
13229
|
if [ -f "$loki_dir/STOP" ]; then
|
|
12898
13230
|
log_warn "STOP file detected - stopping execution"
|
|
12899
13231
|
rm -f "$loki_dir/STOP"
|
|
13232
|
+
# Delegate-then-notify: an explicit STOP file is a deliberate stop, but
|
|
13233
|
+
# a detached (--bg) user still benefits from a summary of partial work.
|
|
13234
|
+
# NOTE: the SIGTERM/`loki stop` group-kill path (cleanup handler near the
|
|
13235
|
+
# end of this file) is intentionally NOT notified: that user is at a
|
|
13236
|
+
# terminal issuing the stop and is already aware.
|
|
13237
|
+
emit_completion_summary stopped
|
|
12900
13238
|
return 2
|
|
12901
13239
|
fi
|
|
12902
13240
|
|
|
@@ -13364,6 +13702,9 @@ main() {
|
|
|
13364
13702
|
echo -e " ${DIM}Logs:${NC} tail -f $log_file"
|
|
13365
13703
|
echo -e " ${DIM}Status:${NC} cat .loki/STATUS.txt"
|
|
13366
13704
|
echo ""
|
|
13705
|
+
echo -e "${GREEN}You will be notified when done (or if input is needed).${NC}"
|
|
13706
|
+
echo -e " ${DIM}Summary on completion:${NC} cat .loki/COMPLETION.txt"
|
|
13707
|
+
echo ""
|
|
13367
13708
|
|
|
13368
13709
|
exit 0
|
|
13369
13710
|
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.22.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.22.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=BA6A18624E28A6CC64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.22.0",
|
|
4
4
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|