loki-mode 7.91.1 → 7.92.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, 8 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.91.1
6
+ # Loki Mode v7.92.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
408
408
 
409
409
  ---
410
410
 
411
- **v7.91.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.92.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.91.1
1
+ 7.92.0
package/autonomy/loki CHANGED
@@ -313,6 +313,112 @@ get_version() {
313
313
  fi
314
314
  }
315
315
 
316
+ # Stale-install nudge (bash route; mirrors loki-ts/src/util/update_check.ts).
317
+ # Best-effort, fail-silent, and adds zero latency on the hot path:
318
+ # - skipped entirely on non-TTY / CI / LOKI_NO_UPDATE_CHECK=1;
319
+ # - result cached in ~/.loki/cache/update-check-bash.json for >= 24h, so the
320
+ # network is hit at most once a day (separate file from the Bun cache, which
321
+ # stores millisecond timestamps; this file stores Unix seconds);
322
+ # - the registry fetch has a hard 1.5s timeout and is fully fail-silent;
323
+ # - the hint goes to STDERR so stdout stays byte-identical for scripts and the
324
+ # bash<->Bun parity tests.
325
+ # We never fabricate a version: the hint prints only when a real check (or a
326
+ # fresh cache from one) reports a strictly newer stable semver.
327
+ _loki_update_cache_path() {
328
+ echo "$HOME/.loki/cache/update-check-bash.json"
329
+ }
330
+
331
+ # _loki_semver_newer <latest> <current> -> 0 (true) iff latest is strictly newer.
332
+ # Rejects anything that is not a plain MAJOR.MINOR.PATCH (e.g. "unknown",
333
+ # prereleases): on any unparseable input it returns 1 (never nudge).
334
+ _loki_semver_newer() {
335
+ local latest="$1" current="$2"
336
+ case "$latest" in
337
+ [0-9]*.[0-9]*.[0-9]*) ;;
338
+ *) return 1 ;;
339
+ esac
340
+ case "$current" in
341
+ [0-9]*.[0-9]*.[0-9]*) ;;
342
+ *) return 1 ;;
343
+ esac
344
+ # Reject prerelease / build suffixes (anything past the third number).
345
+ case "$latest" in *[!0-9.]*) return 1 ;; esac
346
+ case "$current" in *[!0-9.]*) return 1 ;; esac
347
+ local l_maj l_min l_pat c_maj c_min c_pat
348
+ IFS='.' read -r l_maj l_min l_pat _ <<EOF
349
+ $latest
350
+ EOF
351
+ IFS='.' read -r c_maj c_min c_pat _ <<EOF
352
+ $current
353
+ EOF
354
+ # Default any empty component to 0 (defensive).
355
+ l_maj=${l_maj:-0}; l_min=${l_min:-0}; l_pat=${l_pat:-0}
356
+ c_maj=${c_maj:-0}; c_min=${c_min:-0}; c_pat=${c_pat:-0}
357
+ if [ "$l_maj" -ne "$c_maj" ]; then [ "$l_maj" -gt "$c_maj" ]; return; fi
358
+ if [ "$l_min" -ne "$c_min" ]; then [ "$l_min" -gt "$c_min" ]; return; fi
359
+ [ "$l_pat" -gt "$c_pat" ]
360
+ }
361
+
362
+ # maybe_print_update_hint: print the one-line update nudge to stderr if (and
363
+ # only if) a successful, non-fabricated check reports a strictly newer release.
364
+ # Never blocks, never slows the command, never touches stdout. Always returns 0.
365
+ maybe_print_update_hint() {
366
+ # Opt-out / non-interactive / CI: do nothing, touch neither disk nor network.
367
+ [ "${LOKI_NO_UPDATE_CHECK:-}" = "1" ] && return 0
368
+ [ -n "${CI:-}" ] && return 0
369
+ [ ! -t 1 ] && return 0
370
+
371
+ local current
372
+ current=$(get_version)
373
+ case "$current" in
374
+ [0-9]*.[0-9]*.[0-9]*) ;;
375
+ *) return 0 ;; # "unknown" or non-semver -> never nudge
376
+ esac
377
+
378
+ local cache_file latest=""
379
+ cache_file=$(_loki_update_cache_path)
380
+
381
+ # Fast path: a fresh (< 24h) cache means zero network.
382
+ if [ -f "$cache_file" ]; then
383
+ local checked_at now age
384
+ checked_at=$(sed -n 's/.*"checkedAt"[[:space:]]*:[[:space:]]*\([0-9]\{1,\}\).*/\1/p' "$cache_file" 2>/dev/null | head -1)
385
+ latest=$(sed -n 's/.*"latest"[[:space:]]*:[[:space:]]*"\([0-9][0-9.]*\)".*/\1/p' "$cache_file" 2>/dev/null | head -1)
386
+ if [ -n "$checked_at" ] && [ -n "$latest" ]; then
387
+ now=$(date +%s 2>/dev/null || echo 0)
388
+ age=$(( now - checked_at ))
389
+ if [ "$age" -ge 0 ] && [ "$age" -lt 86400 ]; then
390
+ if _loki_semver_newer "$latest" "$current"; then
391
+ printf 'A newer loki-mode is available (%s). Update: bun install -g loki-mode\n' "$latest" >&2
392
+ fi
393
+ return 0
394
+ fi
395
+ fi
396
+ latest="" # stale cache -> fall through to a fresh fetch
397
+ fi
398
+
399
+ # Cache miss/stale: query the registry. Hard timeout, fail-silent.
400
+ if command -v curl >/dev/null 2>&1; then
401
+ latest=$(curl -fsS --max-time 1.5 "https://registry.npmjs.org/loki-mode/latest" 2>/dev/null \
402
+ | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([0-9][0-9.]*\)".*/\1/p' | head -1)
403
+ fi
404
+ # No usable result -> print nothing, write nothing (never fabricate).
405
+ case "$latest" in
406
+ [0-9]*.[0-9]*.[0-9]*) ;;
407
+ *) return 0 ;;
408
+ esac
409
+
410
+ # Persist the fresh result (best-effort; a non-writable HOME must not break).
411
+ local now2
412
+ now2=$(date +%s 2>/dev/null || echo 0)
413
+ ( mkdir -p "$(dirname "$cache_file")" 2>/dev/null \
414
+ && printf '{"checkedAt":%s,"latest":"%s"}\n' "$now2" "$latest" > "$cache_file" 2>/dev/null ) || true
415
+
416
+ if _loki_semver_newer "$latest" "$current"; then
417
+ printf 'A newer loki-mode is available (%s). Update: bun install -g loki-mode\n' "$latest" >&2
418
+ fi
419
+ return 0
420
+ }
421
+
316
422
  # Ensure dashboard Python venv with all deps installed.
317
423
  # Uses ~/.loki/dashboard-venv (persistent, writable, survives npm/brew upgrades).
318
424
  # Sets DASHBOARD_PYTHON to the venv python3 path on success.
@@ -717,11 +823,11 @@ show_help() {
717
823
  echo ""
718
824
  echo "Usage: loki <command> [options]"
719
825
  echo ""
720
- echo "New here? Try one of these first:"
826
+ echo "New here? Try these in order:"
721
827
  echo " loki doctor Check your setup is ready (a few seconds)"
722
- echo " loki quick \"add a health endpoint\" One small task, start to finish"
723
- echo " loki demo Build a sample todo app end to end (real run)"
828
+ echo " loki quickstart Guided first build from your idea (no PRD needed)"
724
829
  echo " loki start ./prd.md Build from a spec (PRD file, GitHub issue, or no arg)"
830
+ echo " quickstart = guided first build; quick = one small task (3 iters max)."
725
831
  echo " Docs: https://github.com/asklokesh/loki-mode | Report a problem: loki crash"
726
832
  echo ""
727
833
  # CLI consolidation (Phase A): the front page presents ~17 canonical
@@ -778,11 +884,11 @@ show_help() {
778
884
  echo " version Show version"
779
885
  echo " help Show this help ('loki help aliases' for old names)"
780
886
  echo ""
781
- echo "More commands (ship, grill, spec, deploy, cleanup, init, watch, demo, web, api,"
782
- echo "logs, github, import, council, proof, audit, compliance, agent, template,"
783
- echo "magic, docs, wiki, ci, test, bench, secrets, telemetry, crash, worktree,"
784
- echo "failover, monitor, remote, ...) are dispatchable and documented via"
785
- echo "'loki <command> --help'."
887
+ echo "More commands (the most useful; many more are dispatchable):"
888
+ echo " Advanced: ship, deploy, init, web"
889
+ echo " Cloud + CI: github, import, ci"
890
+ echo " Debugging: logs, crash, cleanup"
891
+ echo "...run 'loki help <name>' for any command."
786
892
  echo ""
787
893
  echo "Aliases (deprecated): older command names still work; they print a"
788
894
  echo "one-line pointer to stderr and never alter --json output. See the full"
@@ -804,19 +910,8 @@ show_help() {
804
910
  echo " --bmad-project PATH Use BMAD Method project artifacts as input"
805
911
  echo " --openspec PATH Use OpenSpec change directory as input"
806
912
  echo ""
807
- echo "Options for 'run' (v6.0.0):"
808
- echo " --dry-run Preview generated PRD without starting"
809
- echo " --no-start Generate PRD but don't start execution"
810
- echo " --output FILE Save PRD to custom path"
811
- echo " --provider NAME AI provider: claude (default), codex, cline, aider"
812
- echo " --parallel Enable parallel mode with git worktrees"
813
- echo " --budget USD Set cost budget limit"
814
- echo ""
815
- echo "Progressive Isolation (for 'run'):"
816
- echo " --worktree, -w Git worktree isolation (separate branch)"
817
- echo " --pr Auto-create PR after completion (implies --worktree)"
818
- echo " --ship Auto-merge after PR (implies --pr)"
819
- echo " --detach, -d Run in background (implies --worktree)"
913
+ echo "Deprecated alias 'run': 'loki run <issue-ref>' still works as an alias"
914
+ echo "for 'loki start'. See its options with 'loki run --help'."
820
915
  echo ""
821
916
  echo "Examples:"
822
917
  echo ""
@@ -941,11 +1036,11 @@ show_landing() {
941
1036
  echo ""
942
1037
  echo -e "First time here? ${CYAN}loki doctor${NC} checks your setup (an AI provider CLI is required)."
943
1038
  echo ""
944
- echo "Get started:"
1039
+ echo "Get started (in order):"
1040
+ echo -e " ${CYAN}loki doctor${NC} Check your setup is ready (a few seconds)"
945
1041
  echo -e " ${CYAN}loki quickstart${NC} Guided first build from your idea (no PRD needed)"
946
1042
  echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub issue, or no arg)"
947
- echo -e " ${CYAN}loki demo${NC} Build a sample todo app end to end (real run)"
948
- echo -e " ${CYAN}loki dashboard start${NC} Start the live run monitor (then: loki dashboard open)"
1043
+ echo " quickstart = guided first build; quick = one small task (3 iters max)."
949
1044
  echo ""
950
1045
  echo -e "Need help? ${CYAN}loki help${NC} lists every command."
951
1046
  echo "Tip: preview cost and scope before building: loki plan <your-prd.md>"
@@ -3408,7 +3503,8 @@ cmd_why() {
3408
3503
  fi
3409
3504
 
3410
3505
  if [ "$as_json" = "1" ]; then
3411
- _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" python3 - <<'WHYJSON'
3506
+ _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
3507
+ _LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" python3 - <<'WHYJSON'
3412
3508
  import json, os
3413
3509
  def load(p):
3414
3510
  try:
@@ -3416,7 +3512,8 @@ def load(p):
3416
3512
  except Exception: return {}
3417
3513
  state = load(os.environ.get("_LOKI_WHY_STATE", ""))
3418
3514
  comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
3419
- print(json.dumps({"state": state, "completion": comp}, indent=2))
3515
+ last_error = load(os.environ.get("_LOKI_WHY_LAST_ERROR", ""))
3516
+ print(json.dumps({"state": state, "completion": comp, "last_error": last_error}, indent=2))
3420
3517
  WHYJSON
3421
3518
  return 0
3422
3519
  fi
@@ -3428,6 +3525,7 @@ WHYJSON
3428
3525
  _why_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
3429
3526
  _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
3430
3527
  _LOKI_WHY_HEAD_SHA="$_why_head_sha" \
3528
+ _LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" \
3431
3529
  _LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
3432
3530
  import json, os, glob
3433
3531
  def load(p):
@@ -3519,6 +3617,28 @@ print()
3519
3617
  print(f" What happened: {meaning}")
3520
3618
  print(f" What to do : {action}")
3521
3619
 
3620
+ # Surface the structured per-iteration error record, if one exists at
3621
+ # .loki/state/LAST_ERROR.json (the run.sh writer that produces it ships in the
3622
+ # SAME v7.92.0 release; on an older engine the file is simply absent and this
3623
+ # block stays dormant - never fabricated). run.sh clears it at the start of every
3624
+ # fresh run, so a present record belongs to the CURRENT run. We still only show it
3625
+ # when THIS run's outcome is itself a failure - never beside a success verdict
3626
+ # (that would be a fake-green-adjacent lie) - using the same SUCCESS set below.
3627
+ SUCCESS_STATUSES = {"council_approved", "completion_promise_fulfilled", "complete", "completed"}
3628
+ le = {}
3629
+ try:
3630
+ with open(os.environ.get("_LOKI_WHY_LAST_ERROR", "")) as f:
3631
+ le = json.load(f)
3632
+ except Exception:
3633
+ le = {}
3634
+ if isinstance(le, dict) and le.get("error_class") and status not in SUCCESS_STATUSES:
3635
+ print()
3636
+ _it = le.get("iteration")
3637
+ _it_s = f" (iteration {_it})" if _it not in (None, "") else ""
3638
+ print(f" Last error : {le.get('error_class')}{_it_s}")
3639
+ if le.get("brief"):
3640
+ print(f" {le.get('brief')}")
3641
+
3522
3642
  # Surface the latest structured handoff (already-captured context), honestly.
3523
3643
  hd = sorted(glob.glob(os.path.join(os.environ.get("_LOKI_WHY_HANDOFFS",""), "*.md")))
3524
3644
  if hd:
@@ -10104,6 +10224,9 @@ except Exception:
10104
10224
  echo ""
10105
10225
  echo "Next: loki quickstart (guided first build from your idea, no PRD needed)"
10106
10226
  echo " or loki demo (builds a sample todo app end to end) or loki start ./prd.md"
10227
+ # Best-effort stale-install nudge (stderr only; never blocks; off on
10228
+ # non-TTY/CI). Reached only on the human-readable path (--json returns above).
10229
+ maybe_print_update_hint
10107
10230
  return 0
10108
10231
  }
10109
10232
 
@@ -10476,6 +10599,9 @@ cmd_version() {
10476
10599
  ;;
10477
10600
  esac
10478
10601
  echo "Loki Mode v$(get_version)"
10602
+ # Best-effort stale-install nudge (stderr only; never blocks; off on
10603
+ # non-TTY/CI). Mirrors the Bun route's update check for parity.
10604
+ maybe_print_update_hint
10479
10605
  }
10480
10606
 
10481
10607
  # Secrets / credential management
@@ -16704,7 +16830,8 @@ main() {
16704
16830
  if _suggestion=$(_suggest_command "$command"); then
16705
16831
  echo "Did you mean 'loki ${_suggestion}'?" >&2
16706
16832
  fi
16707
- echo "Run 'loki help' for usage." >&2
16833
+ # Always point to the full command list, even with no near-miss match.
16834
+ echo "Run 'loki help' to see every command." >&2
16708
16835
  exit 1
16709
16836
  ;;
16710
16837
  esac
@@ -69,8 +69,15 @@ _po_non_interactive() {
69
69
  # successful install. Inherited stdio; Loki never handles credentials.
70
70
  _po_run_login() {
71
71
  # claude must actually be on PATH for login to make sense.
72
+ # If the install succeeded but the binary is not yet resolvable, npm's
73
+ # global bin is almost certainly not on PATH. Print the exact copy-paste
74
+ # fix. We print the literal $(npm config get prefix) form (single-quoted,
75
+ # NOT executed here) so the user runs it in their own shell and so this
76
+ # post-install path makes no extra npm invocation.
72
77
  if ! command -v claude >/dev/null 2>&1; then
73
- printf "%sInstalled, but 'claude' is not on your PATH yet. You may need to restart your shell or add npm's global bin to PATH (npm config get prefix). Run 'loki doctor' to recheck.%s\n" "$_PO_YELLOW" "$_PO_NC"
78
+ printf "%sInstalled, but 'claude' is not on your PATH yet. Add npm's global bin to your shell:%s\n" "$_PO_YELLOW" "$_PO_NC"
79
+ printf ' export PATH="$(npm config get prefix)/bin:$PATH"\n'
80
+ printf "Then restart your shell (or source your rc) and run: loki doctor\n"
74
81
  return 0
75
82
  fi
76
83
 
package/autonomy/run.sh CHANGED
@@ -1050,6 +1050,179 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
1050
1050
  log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
1051
1051
  log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2 || true; }
1052
1052
 
1053
+ #===============================================================================
1054
+ # Failure diagnosis helpers (T2.4 / T2.5 / T2.6)
1055
+ #
1056
+ # These make a failing or crashed build self-explanatory: a copy-pasteable
1057
+ # "loki why" hint on any non-zero exit, a durable CLASSIFIED LAST_ERROR record
1058
+ # on a failed iteration, and a best-effort terminal record on an untrapped
1059
+ # death. Every one is best-effort and NEVER alters the build's exit code.
1060
+ #===============================================================================
1061
+
1062
+ # _loki_surface_why_hint (T2.4): print the "loki why" hint to stderr and write
1063
+ # it to .loki/NEXT_STEPS.txt. Called once from main() finalization when the run
1064
+ # failed (result != 0). Best-effort: never crashes, never changes exit code.
1065
+ _loki_surface_why_hint() {
1066
+ local loki_dir="${TARGET_DIR:-.}/.loki"
1067
+ local hint="For a plain-language diagnosis of what happened, run: loki why"
1068
+ printf '%s\n' "$hint" >&2 || true
1069
+ mkdir -p "$loki_dir" 2>/dev/null || true
1070
+ printf '%s\n' "$hint" > "$loki_dir/NEXT_STEPS.txt" 2>/dev/null || true
1071
+ return 0
1072
+ }
1073
+
1074
+ # _loki_write_last_error (T2.5): write a durable, classified failure record to
1075
+ # .loki/state/LAST_ERROR.json. Schema:
1076
+ # {
1077
+ # "iteration": <int>,
1078
+ # "error_class": "provider_empty_output"|"build_timeout"|"rate_limited"
1079
+ # |"auth_error"|"unknown",
1080
+ # "brief": "<one honest sentence>",
1081
+ # "timestamp": "<UTC ISO-8601>"
1082
+ # }
1083
+ # This is what `loki why` (in autonomy/loki, NOT owned here) can later read.
1084
+ # Built via python3 so the free-text brief can never break the JSON. Entirely
1085
+ # best-effort: any failure is swallowed and the build is never crashed.
1086
+ # Usage: _loki_write_last_error <iteration> <error_class> <brief>
1087
+ _loki_write_last_error() {
1088
+ local iteration="${1:-0}"
1089
+ local error_class="${2:-unknown}"
1090
+ local brief="${3:-An iteration failed.}"
1091
+ local loki_dir="${TARGET_DIR:-.}/.loki"
1092
+ local state_dir="$loki_dir/state"
1093
+ mkdir -p "$state_dir" 2>/dev/null || true
1094
+ LOKI_LE_ITER="$iteration" \
1095
+ LOKI_LE_CLASS="$error_class" \
1096
+ LOKI_LE_BRIEF="$brief" \
1097
+ LOKI_LE_FILE="$state_dir/LAST_ERROR.json" \
1098
+ python3 -c "
1099
+ import json, os, tempfile
1100
+ try:
1101
+ rec = {
1102
+ 'iteration': int(os.environ.get('LOKI_LE_ITER', '0') or 0),
1103
+ 'error_class': os.environ.get('LOKI_LE_CLASS', 'unknown'),
1104
+ 'brief': os.environ.get('LOKI_LE_BRIEF', ''),
1105
+ 'timestamp': __import__('datetime').datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
1106
+ }
1107
+ target = os.environ['LOKI_LE_FILE']
1108
+ d = os.path.dirname(target)
1109
+ fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
1110
+ with os.fdopen(fd, 'w') as f:
1111
+ json.dump(rec, f)
1112
+ os.replace(tmp, target)
1113
+ except Exception:
1114
+ pass
1115
+ " 2>/dev/null || true
1116
+ return 0
1117
+ }
1118
+
1119
+ # _loki_classify_iteration_error (T2.5 helper): map an iteration's signals to one
1120
+ # of the LAST_ERROR error_class values. Conservative: only returns a specific
1121
+ # class when a signal confidently supports it, else "unknown". Never fabricates
1122
+ # build_timeout (no timeout signal is detected here, so it is reserved for a
1123
+ # caller that has one). Echoes the class on stdout.
1124
+ # Usage: _loki_classify_iteration_error <iter_output_file> <empty_output_flag 0|1>
1125
+ _loki_classify_iteration_error() {
1126
+ local iter_output="${1:-}"
1127
+ local empty_flag="${2:-0}"
1128
+ if [ "$empty_flag" = "1" ]; then
1129
+ echo "provider_empty_output"
1130
+ return 0
1131
+ fi
1132
+ # Rate limit: reuse the same detector the retry path uses.
1133
+ if [ -n "$iter_output" ] && [ -f "$iter_output" ] && detect_rate_limit "$iter_output" 2>/dev/null | grep -qE '^[1-9]'; then
1134
+ echo "rate_limited"
1135
+ return 0
1136
+ fi
1137
+ # Auth error: a clear 401/403/unauthorized in the output tail.
1138
+ if [ -n "$iter_output" ] && [ -f "$iter_output" ]; then
1139
+ local _tail
1140
+ _tail="$(tail -n 40 "$iter_output" 2>/dev/null || true)"
1141
+ if printf '%s\n' "$_tail" | grep -qiE '(http[ /]?40[13]|status[: ]+40[13]|unauthorized|invalid api key|authentication[_ ]error|401 )' 2>/dev/null; then
1142
+ echo "auth_error"
1143
+ return 0
1144
+ fi
1145
+ fi
1146
+ echo "unknown"
1147
+ return 0
1148
+ }
1149
+
1150
+ # _loki_terminal_record (T2.6, SAFE SUBSET): on an untrapped exit where the
1151
+ # persisted run status is still "running" (a true mid-provider-call crash on a
1152
+ # trappable signal -- SIGTERM/SIGINT/SIGHUP via the lock-release trap), leave a
1153
+ # best-effort, classified LAST_ERROR record so a post-crash `loki why` is not
1154
+ # stale. Piggybacks the existing lock-release EXIT trap (see main()) -- it does
1155
+ # NOT install a new broad EXIT trap.
1156
+ #
1157
+ # IMPORTANT (why this does NOT rewrite autonomy-state.json): the resume-detection
1158
+ # block in this file (search "Durable resume") keys the ENT-2 pod-loss resume on
1159
+ # prev_status == "running". Flipping the status to "exited" here would make a
1160
+ # crashed-but-resumable build (LOKI_DURABLE_STATE=1) reset to iteration 0 on the
1161
+ # next start, destroying durable progress. So this writes ONLY the LAST_ERROR
1162
+ # side-record (which is what `loki why` reads) and deliberately leaves the
1163
+ # status untouched. SIGKILL / power-loss are uncatchable (no trap fires); the
1164
+ # ENT-2 durable-resume path covers those. Never alters the exit code; best-effort.
1165
+ _loki_terminal_record() {
1166
+ local state_file
1167
+ state_file="$(_loki_state_file 2>/dev/null)" || return 0
1168
+ [ -n "$state_file" ] || return 0
1169
+ [ -f "$state_file" ] || return 0
1170
+ local _status
1171
+ _status="$(LOKI_TR_FILE="$state_file" python3 -c "
1172
+ import json, os
1173
+ try:
1174
+ print(json.load(open(os.environ['LOKI_TR_FILE'])).get('status','unknown'))
1175
+ except Exception:
1176
+ print('unknown')
1177
+ " 2>/dev/null || echo "unknown")"
1178
+ # Only act on a genuinely mid-flight "running" status. Any settled status
1179
+ # (council_approved, failed, exited, paused, ...) is left untouched.
1180
+ [ "$_status" = "running" ] || return 0
1181
+ # Leave a classified LAST_ERROR so `loki why` has something honest -- WITHOUT
1182
+ # touching autonomy-state.json (preserving the ENT-2 "running" resume signal).
1183
+ _loki_write_last_error "${ITERATION_COUNT:-0}" "unknown" \
1184
+ "The build process exited unexpectedly before finishing (possible crash or kill)." 2>/dev/null || true
1185
+ return 0
1186
+ }
1187
+
1188
+ # _loki_write_rate_limit_signal (T2.7): write a best-effort
1189
+ # .loki/signals/RATE_LIMITED file. This is FORWARD-LAID infrastructure: no
1190
+ # consumer reads it yet (a future dashboard / external watcher could, to tell a
1191
+ # normal provider rate-limit wait apart from a hang). The user-visible signal
1192
+ # today is the log_info line at the wait site; this file is the durable record. Schema:
1193
+ # {"rate_limited": true, "wait_seconds": <int>, "reset_time": "<string>"}
1194
+ # Built via python3 so the reset-time string can never break the JSON. Entirely
1195
+ # best-effort: never crashes, never alters the build.
1196
+ # Usage: _loki_write_rate_limit_signal <wait_seconds> <reset_time>
1197
+ _loki_write_rate_limit_signal() {
1198
+ local wait_seconds="${1:-0}"
1199
+ local reset_time="${2:-}"
1200
+ local signals_dir="${TARGET_DIR:-.}/.loki/signals"
1201
+ mkdir -p "$signals_dir" 2>/dev/null || true
1202
+ LOKI_RL_WAIT="$wait_seconds" \
1203
+ LOKI_RL_RESET="$reset_time" \
1204
+ LOKI_RL_FILE="$signals_dir/RATE_LIMITED" \
1205
+ python3 -c "
1206
+ import json, os, tempfile
1207
+ try:
1208
+ w = os.environ.get('LOKI_RL_WAIT', '0')
1209
+ try:
1210
+ w = int(w)
1211
+ except Exception:
1212
+ w = 0
1213
+ rec = {'rate_limited': True, 'wait_seconds': w, 'reset_time': os.environ.get('LOKI_RL_RESET', '')}
1214
+ target = os.environ['LOKI_RL_FILE']
1215
+ d = os.path.dirname(target)
1216
+ fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
1217
+ with os.fdopen(fd, 'w') as f:
1218
+ json.dump(rec, f)
1219
+ os.replace(tmp, target)
1220
+ except Exception:
1221
+ pass
1222
+ " 2>/dev/null || true
1223
+ return 0
1224
+ }
1225
+
1053
1226
  # Live Build HUD (v7.71.0): a single append-only per-iteration status line on the
1054
1227
  # interactive TTY path. Pure additive stdout decoration -- never piped into any
1055
1228
  # tee, so the dashboard agent.log and the stream-json parser are untouched. The
@@ -1798,9 +1971,93 @@ get_iteration_duration_ms() {
1798
1971
  # Supports Docker/K8s secret file mounts as fallback.
1799
1972
  #===============================================================================
1800
1973
 
1974
+ # Zero-friction preflight helpers (T1.1). These run for ALL environments
1975
+ # (not just Docker/K8s) BEFORE the build starts, via validate_api_keys. git is a
1976
+ # genuine hard requirement (the build inits a repo) so a missing git BLOCKS with a
1977
+ # copy-pasteable fix; node-version and network reachability are ADVISORY (warn and
1978
+ # continue, fail-open) so a probabilistic or optional signal never blocks a working
1979
+ # user. The goal: surface real problems early without ever wrongly refusing to start.
1980
+ #
1981
+ # _loki_check_node_version: ADVISORY only. If node is present and its major
1982
+ # version is < 18, log a warning (node only matters for node-based builds) and
1983
+ # continue. If node is absent entirely this is a NO-OP. Always returns 0 -- it
1984
+ # never blocks the build (fail-open); the real node call, if any, is the test.
1985
+ _loki_check_node_version() {
1986
+ command -v node >/dev/null 2>&1 || return 0
1987
+ local node_version major
1988
+ node_version="$(node --version 2>/dev/null || echo '')"
1989
+ # node --version -> "v20.11.0"; extract the leading major integer.
1990
+ major="$(printf '%s' "$node_version" | sed -E 's/^v?([0-9]+).*/\1/')"
1991
+ # Advisory only: node is OPTIONAL (absence is a no-op above, and many builds -
1992
+ # Python/Go/Rust - never touch node). A present-but-old node only matters for
1993
+ # node-based builds, so WARN and continue (fail-open); never hard-block a
1994
+ # working user. The actual node call (only for JS/TS work) is the real test.
1995
+ if [ -n "$major" ] && [[ "$major" =~ ^[0-9]+$ ]] && [ "$major" -lt 18 ]; then
1996
+ log_warn "Node.js >= 18 recommended for node-based builds; found ${node_version:-unknown}. Upgrade if your project uses node: https://nodejs.org"
1997
+ fi
1998
+ return 0
1999
+ }
2000
+
2001
+ # _loki_check_git_present: the build initializes a git repo, so git is required.
2002
+ _loki_check_git_present() {
2003
+ if ! command -v git >/dev/null 2>&1; then
2004
+ log_error "Git is required (the build initializes a repo). Install: https://git-scm.com/downloads"
2005
+ return 1
2006
+ fi
2007
+ return 0
2008
+ }
2009
+
2010
+ # _loki_check_network_reachable: ADVISORY only. A fast (3s) reachability probe to
2011
+ # the active provider endpoint that WARNS and continues if it cannot reach it -- a
2012
+ # curl failure does not prove the provider CLI cannot connect (3s timeout under
2013
+ # load, transient DNS, or a proxy set for the CLI but not the shell all curl-fail
2014
+ # while the real build succeeds). Always returns 0 (fail-open); the actual
2015
+ # provider call is the authoritative connectivity test. Skipped entirely when curl
2016
+ # is missing, when LOKI_SKIP_NET_PREFLIGHT=1, when ANTHROPIC_BASE_URL is set (alt
2017
+ # provider endpoint we cannot assume), or for any provider whose endpoint we do
2018
+ # not know. It NEVER blocks the build.
2019
+ _loki_check_network_reachable() {
2020
+ local provider="${1:-claude}"
2021
+ [ "${LOKI_SKIP_NET_PREFLIGHT:-}" = "1" ] && return 0
2022
+ command -v curl >/dev/null 2>&1 || return 0
2023
+ # Alternate provider base URL set -> do not assume the default endpoint.
2024
+ [ -n "${ANTHROPIC_BASE_URL:-}" ] && return 0
2025
+
2026
+ local endpoint=""
2027
+ case "$provider" in
2028
+ claude) endpoint="https://api.anthropic.com" ;;
2029
+ *) return 0 ;; # unknown endpoint -> fail open, never guess
2030
+ esac
2031
+
2032
+ # Advisory only: a fast curl probe failing does NOT prove the provider CLI
2033
+ # cannot connect (a 3s timeout under load, transient DNS, or a proxy set for
2034
+ # the CLI but not the shell all curl-fail while the real build succeeds). WARN
2035
+ # and continue - the actual provider call is the authoritative connectivity
2036
+ # test. Silence this with LOKI_SKIP_NET_PREFLIGHT=1. Never hard-block here.
2037
+ if ! curl -sS -m 3 -o /dev/null "$endpoint" 2>/dev/null; then
2038
+ log_warn "Could not verify network reachability to the AI provider (firewall/VPN/transient?). Continuing; the provider call will be the real test. Silence with LOKI_SKIP_NET_PREFLIGHT=1."
2039
+ fi
2040
+ return 0
2041
+ }
2042
+
1801
2043
  validate_api_keys() {
1802
2044
  local provider="${LOKI_PROVIDER:-claude}"
1803
2045
 
2046
+ # Zero-friction preflight (T1.1): toolchain + reachability checks that apply
2047
+ # to EVERY environment, run BEFORE the Docker/K8s early-return below so they
2048
+ # are not silently skipped in the common local case. Node/git are genuinely
2049
+ # required (node only when present-but-too-old); the network probe is
2050
+ # fail-open and opt-out (LOKI_SKIP_NET_PREFLIGHT=1).
2051
+ if ! _loki_check_node_version; then
2052
+ return 1
2053
+ fi
2054
+ if ! _loki_check_git_present; then
2055
+ return 1
2056
+ fi
2057
+ if ! _loki_check_network_reachable "$provider"; then
2058
+ return 1
2059
+ fi
2060
+
1804
2061
  # CLI tools (claude, codex, cline, aider) use their own login sessions.
1805
2062
  # Only require API keys inside Docker/K8s where CLI login isn't available.
1806
2063
  if [[ ! -f "/.dockerenv" ]] && [[ -z "${KUBERNETES_SERVICE_HOST:-}" ]]; then
@@ -16170,9 +16427,13 @@ if __name__ == "__main__":
16170
16427
  esac
16171
16428
 
16172
16429
  # BUG-EC-013: Detect empty provider output (0 bytes = no work done)
16430
+ # T2.5: track this distinct cause so the failure path can classify the
16431
+ # durable LAST_ERROR record as provider_empty_output specifically.
16432
+ local _empty_output=0
16173
16433
  if [ -f "$iter_output" ] && [ ! -s "$iter_output" ] && [ $exit_code -eq 0 ]; then
16174
16434
  log_warn "Provider returned empty output (0 bytes) despite exit code 0 -- treating as error"
16175
16435
  exit_code=1
16436
+ _empty_output=1
16176
16437
  fi
16177
16438
 
16178
16439
  save_state $retry "exited" $exit_code
@@ -16967,6 +17228,24 @@ else:
16967
17228
  # the "Will retry" log_warn below. TTY-gated, `|| true`, never tee'd.
16968
17229
  render_build_hud "${ITERATION_COUNT:-0}" "${rarv_phase:-?}" "${duration:-0}" || true
16969
17230
 
17231
+ # T2.5: durable, classified failure record for `loki why`. Best-effort,
17232
+ # never crashes the build. Skip signal-induced exits (130 SIGINT /
17233
+ # 143 SIGTERM / 137 SIGKILL): a user/operator interrupt is not an error
17234
+ # to record (mirrors the crash-capture exclusion above). Classification
17235
+ # is conservative -- provider_empty_output | rate_limited | auth_error,
17236
+ # else unknown; never fabricates a class that no signal supports.
17237
+ if [ "$exit_code" -ne 130 ] && [ "$exit_code" -ne 143 ] && [ "$exit_code" -ne 137 ]; then
17238
+ local _err_class _err_brief
17239
+ _err_class="$(_loki_classify_iteration_error "$iter_output" "${_empty_output:-0}")"
17240
+ case "$_err_class" in
17241
+ provider_empty_output) _err_brief="The provider returned no output (0 bytes) on this iteration -- no work was done." ;;
17242
+ rate_limited) _err_brief="The provider rate-limited the request; the build will wait and retry." ;;
17243
+ auth_error) _err_brief="The provider rejected the request as unauthorized (check your login or API key)." ;;
17244
+ *) _err_brief="Iteration ${ITERATION_COUNT:-?} failed with exit code ${exit_code} (cause not classified)." ;;
17245
+ esac
17246
+ _loki_write_last_error "${ITERATION_COUNT:-0}" "$_err_class" "$_err_brief" || true
17247
+ fi
17248
+
16970
17249
  # Checkpoint failed iteration state (v5.57.0)
16971
17250
  create_checkpoint "iteration-${ITERATION_COUNT} failed (exit=$exit_code)" "iteration-${ITERATION_COUNT}-fail"
16972
17251
 
@@ -16986,7 +17265,15 @@ else:
16986
17265
  wait_time=$rate_limit_wait
16987
17266
  local human_time=$(format_duration $wait_time)
16988
17267
  log_warn "Rate limit detected! Waiting until reset (~$human_time)..."
16989
- log_info "Rate limit resets at approximately $(date -v+${wait_time}S '+%I:%M %p' 2>/dev/null || date -d "+${wait_time} seconds" '+%I:%M %p' 2>/dev/null || echo 'soon')"
17268
+ local _reset_at
17269
+ _reset_at="$(date -v+${wait_time}S '+%I:%M %p' 2>/dev/null || date -d "+${wait_time} seconds" '+%I:%M %p' 2>/dev/null || echo 'soon')"
17270
+ log_info "Rate limit resets at approximately $_reset_at"
17271
+ # T2.7: elevate the wait to an explicit, reassuring INFO line (the
17272
+ # human time was previously only at DEBUG inside detect_rate_limit's
17273
+ # calculated-backoff branch) so a multi-minute wait does not look
17274
+ # like a hang, and persist a machine-readable signal for watchers.
17275
+ log_info "Rate-limited by the provider; waiting ~${wait_time}s (resets ${_reset_at}). This is normal, not a hang."
17276
+ _loki_write_rate_limit_signal "$wait_time" "$_reset_at" || true
16990
17277
  notify_rate_limit "$wait_time"
16991
17278
  else
16992
17279
  wait_time=$(calculate_wait $retry)
@@ -17026,6 +17313,10 @@ else:
17026
17313
  done
17027
17314
  echo ""
17028
17315
 
17316
+ # T2.7: the wait is over -- clear the RATE_LIMITED signal so it never
17317
+ # lingers stale once the build resumes. Best-effort.
17318
+ rm -f "${TARGET_DIR:-.}/.loki/signals/RATE_LIMITED" 2>/dev/null || true
17319
+
17029
17320
  # Clean up per-iteration output file
17030
17321
  rm -f "$iter_output" 2>/dev/null
17031
17322
 
@@ -18010,8 +18301,15 @@ main() {
18010
18301
  fi
18011
18302
  # Release on session-process exit so a fresh `loki start` can
18012
18303
  # immediately re-acquire after this one finishes / is killed.
18304
+ # T2.6: piggyback the existing lock-release trap (we deliberately do NOT
18305
+ # add a new broad EXIT trap) to write a best-effort terminal record on an
18306
+ # untrapped non-zero exit where the run status is still "running" -- so a
18307
+ # post-crash `loki why` is not stale. _loki_terminal_record is a strict
18308
+ # no-op on every graceful path (status already settled) and never alters
18309
+ # the exit code. SIGKILL/power-loss stay uncatchable (no trap fires); the
18310
+ # ENT-2 durable-resume path covers those.
18013
18311
  # shellcheck disable=SC2064
18014
- trap "safe_release_lock '$lock_file'" EXIT INT TERM HUP
18312
+ trap "_loki_terminal_record || true; safe_release_lock '$lock_file'" EXIT INT TERM HUP
18015
18313
 
18016
18314
  # Check PID file after acquiring lock
18017
18315
  if [ -f "$pid_file" ]; then
@@ -18174,6 +18472,13 @@ main() {
18174
18472
  fi
18175
18473
  fi
18176
18474
 
18475
+ # Clear any stale per-run diagnosis record from a PRIOR run before this one
18476
+ # starts. LAST_ERROR.json is a single side-record; if it survived a previous
18477
+ # failed run it must not surface next to THIS run's outcome (a stale error
18478
+ # shown beside a fresh success would be a fake-green-adjacent lie). Mirrors
18479
+ # the RATE_LIMITED signal clear. Best-effort; never blocks the run.
18480
+ rm -f "${TARGET_DIR:-.}/.loki/state/LAST_ERROR.json" 2>/dev/null || true
18481
+
18177
18482
  if [ "$PARALLEL_MODE" = "true" ]; then
18178
18483
  # Parallel mode: orchestrate multiple worktrees
18179
18484
  log_header "Running in Parallel Mode"
@@ -18423,6 +18728,13 @@ except (json.JSONDecodeError, OSError): pass
18423
18728
  " 2>/dev/null || true
18424
18729
  fi
18425
18730
 
18731
+ # T2.4: on ANY non-zero final result, surface a plain-language next step
18732
+ # (print to stderr + write .loki/NEXT_STEPS.txt). Single chokepoint at the
18733
+ # finalization exit so it fires once and never double-prints on success.
18734
+ if [ "$result" != "0" ]; then
18735
+ _loki_surface_why_hint || true
18736
+ fi
18737
+
18426
18738
  exit $result
18427
18739
  }
18428
18740
 
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.91.1"
10
+ __version__ = "7.92.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.91.1
5
+ **Version:** v7.92.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.91.1 start ./my-spec.md
398
+ asklokesh/loki-mode:7.92.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.91.1";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=H8(G8(import.meta.url)),Z=e$(Q);Q$=W8(U8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var u1={};b(u1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>f1,commandVersion:()=>N8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>m1});async function f1($,Q=m1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([f1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=E8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function E8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function N8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var m1=16777216,Q1;var d=L(()=>{Q1=class Q1 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 r($){return S8?"":$}var S8,T,S,_,lZ,I,k,y,J;var c=L(()=>{S8=(process.env.NO_COLOR??"").length>0;T=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),lZ=r("\x1B[0;34m"),I=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as u8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(u8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var q0={};b(q0,{runStatus:()=>W3});import{existsSync as v,readFileSync as U$,readdirSync as i1,statSync as e1}from"fs";import{resolve as D,basename as $3}from"path";import{homedir as Q3}from"os";function $0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Q0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=$0($),U=$0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function z3(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
2
+ var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.92.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=H8(G8(import.meta.url)),Z=e$(Q);Q$=W8(U8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var u1={};b(u1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>f1,commandVersion:()=>N8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>m1});async function f1($,Q=m1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([f1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=E8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function E8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function N8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var m1=16777216,Q1;var d=L(()=>{Q1=class Q1 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 r($){return S8?"":$}var S8,T,S,_,lZ,I,k,y,J;var c=L(()=>{S8=(process.env.NO_COLOR??"").length>0;T=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),lZ=r("\x1B[0;34m"),I=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as u8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(u8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var q0={};b(q0,{runStatus:()=>W3});import{existsSync as v,readFileSync as U$,readdirSync as i1,statSync as e1}from"fs";import{resolve as D,basename as $3}from"path";import{homedir as Q3}from"os";function $0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Q0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=$0($),U=$0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function z3(){if(await f("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)
@@ -402,9 +402,9 @@ Start a session with: loki start <prd>`}}let X=Y3(z);return{exitCode:0,stdout:Q?
402
402
  `);if(process.env.LOKI_TS_ENTRY)process.stdout.write(` ${A("pass")} LOKI_TS_ENTRY override: ${process.env.LOKI_TS_ENTRY}
403
403
  `);if(process.env.BUN_FROM_SOURCE==="1"||process.env.BUN_FROM_SOURCE==="true")process.stdout.write(` ${A("pass")} BUN_FROM_SOURCE set: shim prefers loki-ts/src/ over dist/
404
404
  `);let u=await Z$();if(u!==null){let x=(await F([u,"-c","import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"],{timeoutMs:5000})).stdout.trim();if(x.startsWith("3.12"))process.stdout.write(` ${A("pass")} Python 3.12 (chromadb / sentence-transformers): ${x} at ${u}
405
- `);else if(x)process.stdout.write(` ${A("warn")} Python 3.12 NOT found -- using ${x} at ${u}; chromadb / sentence-transformers may fail. Install python3.12 (brew install python@3.12 / apt install python3.12).
406
- `);else process.stdout.write(` ${A("warn")} Python 3 found at ${u} but version probe failed; chromadb may not work.
407
- `)}else process.stdout.write(` ${A("warn")} Python 3 not on PATH -- memory + MCP integrations disabled.
405
+ `);else if(x)process.stdout.write(` ${A("warn")} Python 3.12 recommended for memory vector search (chromadb / sentence-transformers); found ${x} at ${u}. Core memory and the rest of Loki work without it. Install: brew install python@3.12 (macOS) or apt install python3.12 (Debian/Ubuntu).
406
+ `);else process.stdout.write(` ${A("warn")} Python 3 found at ${u} but version probe failed; memory vector search (chromadb / sentence-transformers) may not work. The rest of Loki is unaffected.
407
+ `)}else process.stdout.write(` ${A("warn")} Python 3 not on PATH -- memory + MCP integrations disabled. The rest of Loki works without them.
408
408
  `);if(process.stdout.write(`
409
409
  `),process.stdout.write(`${k}Summary:${J} ${S}${$.pass} passed${J}, ${T}${$.fail} failed${J}, ${_}${$.warn} warnings${J}
410
410
 
@@ -802,4 +802,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
802
802
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
803
803
  `),process.stderr.write(Z8),2}}r1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var FZ=await jZ(Bun.argv.slice(2));process.exit(FZ);
804
804
 
805
- //# debugId=66E8E0C857CDBC4264756E2164756E21
805
+ //# debugId=AC88BFE80A60D0E764756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.91.1'
60
+ __version__ = '7.92.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.91.1",
4
+ "version": "7.92.0",
5
5
  "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 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.91.1",
5
+ "version": "7.92.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",