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 +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +154 -27
- package/autonomy/provider-offer.sh +8 -1
- package/autonomy/run.sh +314 -2
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +5 -5
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.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, 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.
|
|
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.
|
|
411
|
+
**v7.92.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
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
|
|
826
|
+
echo "New here? Try these in order:"
|
|
721
827
|
echo " loki doctor Check your setup is ready (a few seconds)"
|
|
722
|
-
echo " loki
|
|
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 (
|
|
782
|
-
echo "
|
|
783
|
-
echo "
|
|
784
|
-
echo "
|
|
785
|
-
echo "'loki <
|
|
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 "
|
|
808
|
-
echo "
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.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.
|
|
398
|
+
asklokesh/loki-mode:7.92.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -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.
|
|
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
|
|
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=
|
|
805
|
+
//# debugId=AC88BFE80A60D0E764756E2164756E21
|
package/mcp/__init__.py
CHANGED
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.
|
|
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.
|
|
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",
|