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 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.21.0
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.21.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
386
+ **v7.22.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.21.0
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
- notify_all_complete
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
- notify_all_complete
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
- notify_all_complete
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.21.0"
10
+ __version__ = "7.22.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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.21.0
5
+ **Version:** v7.22.0
6
6
 
7
7
  ---
8
8
 
@@ -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.21.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}
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=C58800C688F6E31C64756E2164756E21
790
+ //# debugId=BA6A18624E28A6CC64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.21.0'
60
+ __version__ = '7.22.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.21.0",
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",