loki-mode 7.81.0 → 7.82.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/completion-council.sh +88 -27
- package/autonomy/lib/config-map.sh +61 -15
- package/autonomy/loki +172 -27
- package/autonomy/run.sh +34 -13
- package/completions/_loki +63 -3
- package/completions/loki.bash +6 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +36 -10
- package/dashboard/registry.py +67 -24
- package/dashboard/server.py +131 -74
- package/docs/INSTALLATION.md +2 -2
- package/events/bus.py +17 -1
- package/events/emit.sh +15 -1
- package/loki-ts/dist/loki.js +2 -2
- package/lokistore/cloud.py +17 -1
- package/lokistore/factory.py +6 -1
- package/lokistore/local.py +11 -3
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +16 -4
- package/memory/cross_project.py +74 -5
- package/memory/embeddings.py +19 -1
- package/memory/retrieval.py +81 -153
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/codex.sh +9 -4
- package/memory/tree_index.py +0 -499
- package/memory/tree_search.py +0 -305
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.82.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.82.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.82.0
|
|
@@ -927,7 +927,7 @@ if not voters:
|
|
|
927
927
|
# v7.41.3: word-bounded + markdown-tolerant. VOTE:APPROVED and
|
|
928
928
|
# VOTE:APPROVE_WITH_CONCERNS must NOT match APPROVE; bold/quoted
|
|
929
929
|
# VOTE: APPROVE must match. Unmatched -> default REJECT (conservative).
|
|
930
|
-
vote_match = re.search(r'[*_> ]*VOTE[*_ ]*:[*_> ]*(APPROVE|REJECT|CANNOT_VALIDATE)(?![A-Za-z0-9_])', content)
|
|
930
|
+
vote_match = re.search(r'(?:^|[^A-Za-z0-9_])[*_> ]*VOTE[*_ ]*:[*_> ]*(APPROVE|REJECT|CANNOT_VALIDATE)(?![A-Za-z0-9_])', content)
|
|
931
931
|
reason_match = re.search(r'REASON\s*:\s*(.+?)(?:\n|\$)', content)
|
|
932
932
|
issues = []
|
|
933
933
|
for im in re.finditer(r'ISSUES\s*:\s*(CRITICAL|HIGH|MEDIUM|LOW)\s*:\s*(.+?)(?:\n|\$)', content):
|
|
@@ -2537,8 +2537,17 @@ council_aggregate_votes() {
|
|
|
2537
2537
|
local complete_count=0
|
|
2538
2538
|
local continue_count=0
|
|
2539
2539
|
local total_members=$COUNCIL_SIZE
|
|
2540
|
-
|
|
2541
|
-
|
|
2540
|
+
|
|
2541
|
+
# Per-member fields, collected as newline-delimited records and serialized to
|
|
2542
|
+
# JSON inside a python heredoc below. Building the JSON in bash with a sed
|
|
2543
|
+
# quote-escape (the old approach) produced INVALID JSON whenever a reason
|
|
2544
|
+
# contained a backslash or control character: the round-file write then
|
|
2545
|
+
# failed and the caller silently forced CONTINUE. json.dumps escapes
|
|
2546
|
+
# everything correctly, so the round file always parses.
|
|
2547
|
+
local _members=""
|
|
2548
|
+
local _roles=""
|
|
2549
|
+
local _vote_values=""
|
|
2550
|
+
local _reasons=""
|
|
2542
2551
|
|
|
2543
2552
|
local _council_roles=("requirements_verifier" "test_auditor" "devils_advocate")
|
|
2544
2553
|
local member=1
|
|
@@ -2561,20 +2570,38 @@ council_aggregate_votes() {
|
|
|
2561
2570
|
|
|
2562
2571
|
log_info " Evaluate member $member ($role): $vote_value - $vote_reason"
|
|
2563
2572
|
|
|
2564
|
-
#
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
# Escape double quotes in reason for JSON safety
|
|
2571
|
-
local safe_reason
|
|
2572
|
-
safe_reason=$(echo "$vote_reason" | sed 's/"/\\"/g')
|
|
2573
|
-
votes_json="${votes_json}{\"member\":$member,\"role\":\"$role\",\"vote\":\"$vote_value\",\"reason\":\"$safe_reason\"}"
|
|
2573
|
+
# Accumulate one field per line (reason kept single-line by upstream parse,
|
|
2574
|
+
# but newlines are normalized to spaces here to keep the line mapping 1:1).
|
|
2575
|
+
_members="${_members}${member}"$'\n'
|
|
2576
|
+
_roles="${_roles}${role}"$'\n'
|
|
2577
|
+
_vote_values="${_vote_values}${vote_value}"$'\n'
|
|
2578
|
+
_reasons="${_reasons}$(printf '%s' "$vote_reason" | tr '\n' ' ')"$'\n'
|
|
2574
2579
|
|
|
2575
2580
|
((member++))
|
|
2576
2581
|
done
|
|
2577
|
-
|
|
2582
|
+
|
|
2583
|
+
# Serialize the votes array with json.dumps so backslashes/control chars are
|
|
2584
|
+
# escaped correctly (BUG fix: sed-only escaping produced invalid JSON).
|
|
2585
|
+
local votes_json
|
|
2586
|
+
votes_json=$(_MEMBERS="$_members" _ROLES="$_roles" _VOTEVALS="$_vote_values" _REASONS="$_reasons" python3 -c "
|
|
2587
|
+
import json, os
|
|
2588
|
+
def lines(name):
|
|
2589
|
+
v = os.environ.get(name, '')
|
|
2590
|
+
return v.split('\n')[:-1] if v.endswith('\n') else (v.split('\n') if v else [])
|
|
2591
|
+
members = lines('_MEMBERS')
|
|
2592
|
+
roles = lines('_ROLES')
|
|
2593
|
+
votevals = lines('_VOTEVALS')
|
|
2594
|
+
reasons = lines('_REASONS')
|
|
2595
|
+
out = []
|
|
2596
|
+
for i in range(len(members)):
|
|
2597
|
+
out.append({
|
|
2598
|
+
'member': int(members[i]) if members[i].isdigit() else members[i],
|
|
2599
|
+
'role': roles[i] if i < len(roles) else '',
|
|
2600
|
+
'vote': votevals[i] if i < len(votevals) else '',
|
|
2601
|
+
'reason': reasons[i] if i < len(reasons) else '',
|
|
2602
|
+
})
|
|
2603
|
+
print(json.dumps(out))
|
|
2604
|
+
" 2>/dev/null || echo "[]")
|
|
2578
2605
|
|
|
2579
2606
|
# Calculate threshold: 2/3 majority
|
|
2580
2607
|
local threshold=$(( (total_members * 2 + 2) / 3 )) # ceiling of 2/3
|
|
@@ -2638,21 +2665,49 @@ council_devils_advocate_review() {
|
|
|
2638
2665
|
local issue_details=""
|
|
2639
2666
|
|
|
2640
2667
|
# Skeptical check 1: Are tests actually running and passing?
|
|
2641
|
-
|
|
2668
|
+
# Read the SAME structured signal the council already uses
|
|
2669
|
+
# (.loki/quality/test-results.json, written by run.sh:ensure_completion_test
|
|
2670
|
+
# _evidence; parsed the same way as council_evaluate_member ~2414-2438 and the
|
|
2671
|
+
# evidence gate). Parse verdict: runner=="none" => PASS (no real suite to
|
|
2672
|
+
# contradict completion), pass is False => FAIL, else PASS. The legacy log
|
|
2673
|
+
# glob is kept ONLY as an ADDITIONAL red signal -- its absence is NOT an issue
|
|
2674
|
+
# (nothing writes .loki/logs/test-*.log, so an empty glob is the normal case
|
|
2675
|
+
# and must never veto a unanimous COMPLETE on its own).
|
|
2676
|
+
local tr_file="$loki_dir/quality/test-results.json"
|
|
2677
|
+
if [ -f "$tr_file" ]; then
|
|
2678
|
+
local _tr_status
|
|
2679
|
+
_tr_status=$(_TR_FILE="$tr_file" python3 -c "
|
|
2680
|
+
import json, os, sys
|
|
2681
|
+
try:
|
|
2682
|
+
with open(os.environ['_TR_FILE']) as f:
|
|
2683
|
+
d = json.load(f)
|
|
2684
|
+
except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
2685
|
+
print('absent')
|
|
2686
|
+
sys.exit(0)
|
|
2687
|
+
runner = d.get('runner', 'none')
|
|
2688
|
+
passed = d.get('pass', True)
|
|
2689
|
+
if runner == 'none':
|
|
2690
|
+
print('pass')
|
|
2691
|
+
elif passed is False:
|
|
2692
|
+
print('fail')
|
|
2693
|
+
else:
|
|
2694
|
+
print('pass')
|
|
2695
|
+
" 2>/dev/null || echo "absent")
|
|
2696
|
+
if [ "$_tr_status" = "fail" ]; then
|
|
2697
|
+
((issues_found++))
|
|
2698
|
+
issue_details="${issue_details}structured test results red (pass==false); "
|
|
2699
|
+
fi
|
|
2700
|
+
fi
|
|
2701
|
+
# Additional source: any legacy test log that shows no pass indicator is a red
|
|
2702
|
+
# signal. Missing logs are NOT counted (this path is not written by the runner).
|
|
2703
|
+
local f
|
|
2642
2704
|
for f in "$loki_dir"/logs/test-*.log "$loki_dir"/logs/*test*.log; do
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
((issues_found++))
|
|
2648
|
-
issue_details="${issue_details}test log $(basename "$f") has no clear pass indicator; "
|
|
2649
|
-
fi
|
|
2705
|
+
[ -f "$f" ] || continue
|
|
2706
|
+
if ! tail -30 "$f" 2>/dev/null | grep -qiE "(passed|success|ok|all tests)"; then
|
|
2707
|
+
((issues_found++))
|
|
2708
|
+
issue_details="${issue_details}test log $(basename "$f") has no clear pass indicator; "
|
|
2650
2709
|
fi
|
|
2651
2710
|
done
|
|
2652
|
-
if [ "$has_test_results" = "false" ]; then
|
|
2653
|
-
((issues_found++))
|
|
2654
|
-
issue_details="${issue_details}no test result logs found at all; "
|
|
2655
|
-
fi
|
|
2656
2711
|
|
|
2657
2712
|
# Skeptical check 2: Are there still failing tasks in the queue?
|
|
2658
2713
|
if [ -f "$loki_dir/queue/failed.json" ]; then
|
|
@@ -2681,9 +2736,15 @@ council_devils_advocate_review() {
|
|
|
2681
2736
|
fi
|
|
2682
2737
|
|
|
2683
2738
|
# Skeptical check 5: Recent error events
|
|
2739
|
+
# events.jsonl uses the flat schema {"timestamp","type","data"} (events/emit.sh
|
|
2740
|
+
# :196; run.sh emit_event). There is no "level" field, so the old
|
|
2741
|
+
# "\"level\":\"error\"" grep never matched (dead check). Match the real shape:
|
|
2742
|
+
# a "type" whose name ends in error/fail/crash (e.g. provider_failover,
|
|
2743
|
+
# dashboard_crash), plus error/failed markers inside the data payload.
|
|
2684
2744
|
if [ -f "$loki_dir/events.jsonl" ]; then
|
|
2685
2745
|
local recent_errors
|
|
2686
|
-
recent_errors=$(tail -50 "$loki_dir/events.jsonl" 2>/dev/null | grep -ciE "
|
|
2746
|
+
recent_errors=$(tail -50 "$loki_dir/events.jsonl" 2>/dev/null | grep -ciE '"type"[[:space:]]*:[[:space:]]*"[a-z_]*(error|fail|crash)' 2>/dev/null | tr -d ' \n' || echo "0")
|
|
2747
|
+
[ -n "$recent_errors" ] || recent_errors=0
|
|
2687
2748
|
if [ "$recent_errors" -gt 0 ]; then
|
|
2688
2749
|
((issues_found++))
|
|
2689
2750
|
issue_details="${issue_details}$recent_errors recent error events; "
|
|
@@ -291,6 +291,59 @@ loki_escape_regex() {
|
|
|
291
291
|
printf '%s' "$input" | sed 's/[.[\*?+^${}|()\\]/\\&/g'
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
# YAML scalar extractor for the no-yq fallback. Resolves a FULL nested dotted
|
|
295
|
+
# path (e.g. completion.council.enabled) by descending indentation, so keys
|
|
296
|
+
# that share a last segment (dashboard.enabled vs notifications.enabled) never
|
|
297
|
+
# collide. Prints the scalar value (one line) or nothing if the path is absent.
|
|
298
|
+
# Pure awk so it is portable across GNU and BSD (POSIX classes, no \s, no sed
|
|
299
|
+
# quirks). Mirrors what `yq eval ".$path // \"\""` would return for scalars.
|
|
300
|
+
loki_yaml_fallback_extract() {
|
|
301
|
+
local file="$1" dotted_path="$2"
|
|
302
|
+
[ -f "$file" ] || return 0
|
|
303
|
+
awk -v path="$dotted_path" '
|
|
304
|
+
BEGIN { n = split(path, want, "."); depth = 0 }
|
|
305
|
+
{
|
|
306
|
+
line = $0
|
|
307
|
+
if (line ~ /^[[:space:]]*#/) next
|
|
308
|
+
if (line ~ /^[[:space:]]*$/) next
|
|
309
|
+
ind = 0
|
|
310
|
+
while (substr(line, ind + 1, 1) == " ") ind++
|
|
311
|
+
rest = substr(line, ind + 1)
|
|
312
|
+
if (rest !~ /^[^:]+:/) next
|
|
313
|
+
ci = index(rest, ":")
|
|
314
|
+
key = substr(rest, 1, ci - 1)
|
|
315
|
+
val = substr(rest, ci + 1)
|
|
316
|
+
# Pop stack entries that are siblings or shallower than this line.
|
|
317
|
+
while (depth > 0 && stack_ind[depth] >= ind) depth--
|
|
318
|
+
depth++
|
|
319
|
+
stack_ind[depth] = ind
|
|
320
|
+
stack_key[depth] = key
|
|
321
|
+
if (depth == n) {
|
|
322
|
+
ok = 1
|
|
323
|
+
for (i = 1; i <= n; i++) if (stack_key[i] != want[i]) { ok = 0; break }
|
|
324
|
+
if (ok) {
|
|
325
|
+
sub(/^[[:space:]]+/, "", val)
|
|
326
|
+
q = substr(val, 1, 1)
|
|
327
|
+
if (q == "\"" || q == "\047") {
|
|
328
|
+
# Quoted scalar: take chars up to the matching close quote
|
|
329
|
+
# so a "#" or trailing comment inside/after stays correct.
|
|
330
|
+
rest2 = substr(val, 2)
|
|
331
|
+
qi = index(rest2, q)
|
|
332
|
+
if (qi > 0) val = substr(rest2, 1, qi - 1)
|
|
333
|
+
else val = rest2
|
|
334
|
+
} else {
|
|
335
|
+
# Unquoted: drop a trailing comment, then trailing space.
|
|
336
|
+
sub(/[[:space:]]*#.*$/, "", val)
|
|
337
|
+
sub(/[[:space:]]+$/, "", val)
|
|
338
|
+
}
|
|
339
|
+
print val
|
|
340
|
+
exit
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
' "$file"
|
|
345
|
+
}
|
|
346
|
+
|
|
294
347
|
#===============================================================================
|
|
295
348
|
# 5a. ${VAR} env-ref expansion (NEVER eval)
|
|
296
349
|
#===============================================================================
|
|
@@ -528,16 +581,11 @@ loki_parse_yaml_file() {
|
|
|
528
581
|
if [ "$have_yq" = "1" ]; then
|
|
529
582
|
value="$(yq eval ".$yaml_path // \"\"" "$file" 2>/dev/null || true)"
|
|
530
583
|
else
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
#
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
escaped_key="$(loki_escape_regex "$key")"
|
|
537
|
-
value="$( { grep -E "^\s*${escaped_key}:" "$file" 2>/dev/null | head -1 \
|
|
538
|
-
| sed -E 's/.*:\s*//' | sed 's/#.*//' \
|
|
539
|
-
| sed 's/^["'\'']//;s/["'\'']$//' | tr -d '\n' \
|
|
540
|
-
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; } || true)"
|
|
584
|
+
# No-yq fallback: resolve the FULL nested dotted path by indentation
|
|
585
|
+
# so same-last-segment keys (dashboard.enabled vs notifications.enabled)
|
|
586
|
+
# never collide. `|| true` keeps a no-match from tripping set -e in the
|
|
587
|
+
# loki CLI (which runs set -euo pipefail).
|
|
588
|
+
value="$(loki_yaml_fallback_extract "$file" "$yaml_path" || true)"
|
|
541
589
|
fi
|
|
542
590
|
if [ "$value" = "null" ]; then value=""; fi
|
|
543
591
|
if [ -z "$value" ]; then continue; fi
|
|
@@ -831,17 +879,15 @@ loki_config_validate_file() {
|
|
|
831
879
|
done < "$f"
|
|
832
880
|
;;
|
|
833
881
|
yaml)
|
|
834
|
-
local mapping yaml_path env_var value
|
|
882
|
+
local mapping yaml_path env_var value have_yq=0
|
|
835
883
|
if command -v yq >/dev/null 2>&1; then have_yq=1; fi
|
|
836
884
|
for mapping in "${LOKI_CONFIG_MAP[@]}"; do
|
|
837
885
|
yaml_path="${mapping%%:*}"; env_var="${mapping##*:}"
|
|
838
886
|
if [ "$have_yq" = 1 ]; then
|
|
839
887
|
value="$(yq eval ".$yaml_path // \"\"" "$f" 2>/dev/null || true)"
|
|
840
888
|
else
|
|
841
|
-
|
|
842
|
-
value="$(
|
|
843
|
-
| sed -E 's/.*:\s*//' | sed 's/#.*//' | sed 's/^["'\'']//;s/["'\'']$//' \
|
|
844
|
-
| tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; } || true)"
|
|
889
|
+
# Full nested-path resolution (no same-last-segment collision).
|
|
890
|
+
value="$(loki_yaml_fallback_extract "$f" "$yaml_path" || true)"
|
|
845
891
|
fi
|
|
846
892
|
if [ "$value" = "null" ]; then value=""; fi
|
|
847
893
|
if [ -z "$value" ]; then continue; fi
|
package/autonomy/loki
CHANGED
|
@@ -1386,6 +1386,13 @@ cmd_start() {
|
|
|
1386
1386
|
echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget 5.00)${NC}"
|
|
1387
1387
|
exit 1
|
|
1388
1388
|
fi
|
|
1389
|
+
# Reject a non-positive budget: a 0 (or 0.00) cap makes
|
|
1390
|
+
# check_budget_limit pause before any work runs (cost >= 0 is
|
|
1391
|
+
# always true), which looks like a silent hang. Require > 0.
|
|
1392
|
+
if ! awk -v b="$2" 'BEGIN{exit !(b+0 > 0)}'; then
|
|
1393
|
+
echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
|
|
1394
|
+
exit 1
|
|
1395
|
+
fi
|
|
1389
1396
|
export LOKI_BUDGET_LIMIT="$2"
|
|
1390
1397
|
shift 2
|
|
1391
1398
|
else
|
|
@@ -1399,6 +1406,11 @@ cmd_start() {
|
|
|
1399
1406
|
echo -e "${RED}--budget requires a numeric USD amount (e.g., --budget=5.00)${NC}"
|
|
1400
1407
|
exit 1
|
|
1401
1408
|
fi
|
|
1409
|
+
# Reject a non-positive budget (see --budget arm above).
|
|
1410
|
+
if ! awk -v b="$budget_val" 'BEGIN{exit !(b+0 > 0)}'; then
|
|
1411
|
+
echo -e "${RED}--budget must be greater than 0 (a 0 budget pauses before any work)${NC}" >&2
|
|
1412
|
+
exit 1
|
|
1413
|
+
fi
|
|
1402
1414
|
export LOKI_BUDGET_LIMIT="$budget_val"
|
|
1403
1415
|
shift
|
|
1404
1416
|
;;
|
|
@@ -1613,6 +1625,10 @@ cmd_start() {
|
|
|
1613
1625
|
version=$(get_version)
|
|
1614
1626
|
local _ttfv_max_iter="${LOKI_MAX_ITERATIONS:-3}"
|
|
1615
1627
|
mkdir -p "$LOKI_DIR" 2>/dev/null || true
|
|
1628
|
+
# Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
|
|
1629
|
+
# into run.sh, so these are never cleaned on exit and accumulate).
|
|
1630
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
1631
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
1616
1632
|
local brief_prd="$LOKI_DIR/brief-prd-$$.md"
|
|
1617
1633
|
synthesize_brief_prd "$brief_prd" "$brief_text"
|
|
1618
1634
|
prd_file="$brief_prd"
|
|
@@ -2951,7 +2967,7 @@ cmd_why() {
|
|
|
2951
2967
|
--json) as_json=1 ;;
|
|
2952
2968
|
--help|-h) echo "Usage: loki why [--json] -- explain the last build's outcome and what to do next"; return 0 ;;
|
|
2953
2969
|
"" ) : ;;
|
|
2954
|
-
*) echo -e "${RED}Unknown flag: $1${NC}"; echo "Usage: loki why [--json]"; return 1 ;;
|
|
2970
|
+
*) echo -e "${RED}Unknown flag: $1${NC}" >&2; echo "Usage: loki why [--json]" >&2; return 1 ;;
|
|
2955
2971
|
esac
|
|
2956
2972
|
|
|
2957
2973
|
local loki_dir="${LOKI_DIR:-.loki}"
|
|
@@ -2985,7 +3001,10 @@ WHYJSON
|
|
|
2985
3001
|
# Human-readable report. The diagnosis maps the terminal status to a plain
|
|
2986
3002
|
# explanation + a concrete next action; everything is sourced from the files,
|
|
2987
3003
|
# nothing is invented.
|
|
3004
|
+
local _why_head_sha
|
|
3005
|
+
_why_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
|
2988
3006
|
_LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
|
|
3007
|
+
_LOKI_WHY_HEAD_SHA="$_why_head_sha" \
|
|
2989
3008
|
_LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
|
|
2990
3009
|
import json, os, glob
|
|
2991
3010
|
def load(p):
|
|
@@ -2994,7 +3013,22 @@ def load(p):
|
|
|
2994
3013
|
except Exception: return {}
|
|
2995
3014
|
state = load(os.environ.get("_LOKI_WHY_STATE", ""))
|
|
2996
3015
|
comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
|
|
2997
|
-
|
|
3016
|
+
|
|
3017
|
+
# completion.json uses a terminal-outcome vocabulary (complete / max_iterations /
|
|
3018
|
+
# failed / stopped / force_stopped / intervention) that differs from the GUIDE
|
|
3019
|
+
# keys below. Map it onto the GUIDE keys so the diagnosis is never "no mapping".
|
|
3020
|
+
# The live state-file status (running / exited / paused / ...) is preferred when
|
|
3021
|
+
# present; we only fall back to the (normalized) completion outcome.
|
|
3022
|
+
COMP_OUTCOME_ALIASES = {
|
|
3023
|
+
"complete": "council_approved",
|
|
3024
|
+
"max_iterations": "max_iterations_reached",
|
|
3025
|
+
"intervention": "paused",
|
|
3026
|
+
# failed / stopped / force_stopped already match GUIDE keys verbatim.
|
|
3027
|
+
}
|
|
3028
|
+
status = state.get("status")
|
|
3029
|
+
if not status:
|
|
3030
|
+
raw_outcome = comp.get("outcome") or ""
|
|
3031
|
+
status = COMP_OUTCOME_ALIASES.get(raw_outcome, raw_outcome) or "unknown"
|
|
2998
3032
|
exit_code = state.get("lastExitCode")
|
|
2999
3033
|
iters = state.get("iterationCount")
|
|
3000
3034
|
|
|
@@ -3026,10 +3060,25 @@ GUIDE = {
|
|
|
3026
3060
|
"Start a new build, or investigate why a force-stop was needed."),
|
|
3027
3061
|
"running": ("The recorded state says a build is still running (or crashed mid-run).",
|
|
3028
3062
|
"If no build is active it likely crashed; in durable mode (LOKI_DURABLE_STATE=1) a restart resumes, else loki start re-runs."),
|
|
3063
|
+
"exited": ("The build process exited mid-iteration (likely a crash, kill, or empty provider output).",
|
|
3064
|
+
"Read .loki/logs/ for the last error; if nothing is running it crashed -- re-run with loki start, or loki resume in durable mode."),
|
|
3029
3065
|
}
|
|
3030
3066
|
meaning, action = GUIDE.get(status, ("No diagnosis mapping for this status; see the raw fields below.",
|
|
3031
3067
|
"Check loki status and .loki/logs/ for detail."))
|
|
3032
3068
|
|
|
3069
|
+
# The completion.json branch/changes/PR belong to the LAST COMPLETED run. When
|
|
3070
|
+
# the live state shows a build that is still running or crashed mid-iteration
|
|
3071
|
+
# (running/exited), or when the completion record's head_sha no longer matches
|
|
3072
|
+
# the current git HEAD, those fields describe a PREVIOUS run -- not this one.
|
|
3073
|
+
# Label them honestly so a crashed run is never reported with a stale PR/branch.
|
|
3074
|
+
live_statuses = {"running", "exited"}
|
|
3075
|
+
head_sha = os.environ.get("_LOKI_WHY_HEAD_SHA", "")
|
|
3076
|
+
comp_head = comp.get("head_sha", "")
|
|
3077
|
+
comp_is_stale = bool(state.get("status") in live_statuses) or bool(
|
|
3078
|
+
head_sha and comp_head and comp_head != head_sha
|
|
3079
|
+
)
|
|
3080
|
+
comp_label = " (from previous completed run)" if comp_is_stale else ""
|
|
3081
|
+
|
|
3033
3082
|
print("Loki: why")
|
|
3034
3083
|
print("=" * 60)
|
|
3035
3084
|
print(f" Outcome : {status}")
|
|
@@ -3038,11 +3087,11 @@ if exit_code is not None:
|
|
|
3038
3087
|
if iters is not None:
|
|
3039
3088
|
print(f" Iterations : {iters}")
|
|
3040
3089
|
if comp.get("branch"):
|
|
3041
|
-
print(f" Branch : {comp['branch']}")
|
|
3090
|
+
print(f" Branch : {comp['branch']}{comp_label}")
|
|
3042
3091
|
if comp.get("files_changed") is not None:
|
|
3043
|
-
print(f" Changes : {comp.get('files_changed',0)} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)})")
|
|
3092
|
+
print(f" Changes : {comp.get('files_changed',0)} files (+{comp.get('insertions',0)}/-{comp.get('deletions',0)}){comp_label}")
|
|
3044
3093
|
if comp.get("pr_url"):
|
|
3045
|
-
print(f" PR : {comp['pr_url']}")
|
|
3094
|
+
print(f" PR : {comp['pr_url']}{comp_label}")
|
|
3046
3095
|
print()
|
|
3047
3096
|
print(f" What happened: {meaning}")
|
|
3048
3097
|
print(f" What to do : {action}")
|
|
@@ -5767,12 +5816,20 @@ cmd_preview() {
|
|
|
5767
5816
|
public=true
|
|
5768
5817
|
;;
|
|
5769
5818
|
--provider)
|
|
5770
|
-
|
|
5771
|
-
#
|
|
5772
|
-
#
|
|
5773
|
-
#
|
|
5774
|
-
|
|
5775
|
-
|
|
5819
|
+
# Guard against a missing OR flag-shaped value: an unguarded
|
|
5820
|
+
# shift when --provider is the last arg underflows under set -e,
|
|
5821
|
+
# and a bare "--provider --public" would otherwise swallow the
|
|
5822
|
+
# next flag as the provider name.
|
|
5823
|
+
case "${2:-}" in
|
|
5824
|
+
-*|"")
|
|
5825
|
+
echo "loki preview --provider requires a value (cloudflared|ngrok)" >&2
|
|
5826
|
+
return 1
|
|
5827
|
+
;;
|
|
5828
|
+
*)
|
|
5829
|
+
provider="$2"
|
|
5830
|
+
shift
|
|
5831
|
+
;;
|
|
5832
|
+
esac
|
|
5776
5833
|
;;
|
|
5777
5834
|
--yes)
|
|
5778
5835
|
assume_yes=true
|
|
@@ -6122,11 +6179,14 @@ cmd_deploy() {
|
|
|
6122
6179
|
return 0
|
|
6123
6180
|
;;
|
|
6124
6181
|
--dir)
|
|
6125
|
-
|
|
6126
|
-
#
|
|
6127
|
-
#
|
|
6128
|
-
#
|
|
6129
|
-
|
|
6182
|
+
# Guard against a missing OR flag-shaped value: an unguarded
|
|
6183
|
+
# shift when --dir is the last arg underflows under set -e, and a
|
|
6184
|
+
# bare "--dir --no-clip" would otherwise treat the next flag as
|
|
6185
|
+
# the scan directory.
|
|
6186
|
+
case "${2:-}" in
|
|
6187
|
+
-*|"") echo "loki deploy --dir requires a directory path" >&2; return 1 ;;
|
|
6188
|
+
*) dir="$2"; shift ;;
|
|
6189
|
+
esac
|
|
6130
6190
|
;;
|
|
6131
6191
|
--no-clip)
|
|
6132
6192
|
do_clip=false
|
|
@@ -8013,7 +8073,7 @@ cmd_assets() {
|
|
|
8013
8073
|
;;
|
|
8014
8074
|
*)
|
|
8015
8075
|
echo -e "${RED}Unknown subcommand: $subcommand${NC}" >&2
|
|
8016
|
-
echo "Run 'loki assets --help' for usage."
|
|
8076
|
+
echo "Run 'loki assets --help' for usage." >&2
|
|
8017
8077
|
return 1
|
|
8018
8078
|
;;
|
|
8019
8079
|
esac
|
|
@@ -11081,6 +11141,10 @@ cmd_quick() {
|
|
|
11081
11141
|
# BUG-PU-005: Use unique filename to prevent race conditions when
|
|
11082
11142
|
# multiple simultaneous `loki quick` commands run in the same project
|
|
11083
11143
|
mkdir -p "$LOKI_DIR"
|
|
11144
|
+
# Reap stale per-PID temp PRDs from prior runs (the process exec-replaces
|
|
11145
|
+
# into run.sh, so these are never cleaned on exit and accumulate).
|
|
11146
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
11147
|
+
find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
11084
11148
|
local quick_prd="$LOKI_DIR/quick-prd-$$.md"
|
|
11085
11149
|
cat > "$quick_prd" << QPRDEOF
|
|
11086
11150
|
# Quick Task
|
|
@@ -15482,12 +15546,22 @@ cmd_plan() {
|
|
|
15482
15546
|
done
|
|
15483
15547
|
|
|
15484
15548
|
if [ -z "$prd_file" ]; then
|
|
15549
|
+
# Under --json, a machine consumer expects JSON on every path, including
|
|
15550
|
+
# errors -- never ANSI-colored prose. Emit a structured error to stdout.
|
|
15551
|
+
if [ "$show_json" = true ]; then
|
|
15552
|
+
printf '{"error":"missing PRD file argument"}\n'
|
|
15553
|
+
return 2
|
|
15554
|
+
fi
|
|
15485
15555
|
echo -e "${RED}Usage: loki plan <PRD file>${NC}"
|
|
15486
15556
|
echo "Run 'loki plan --help' for usage."
|
|
15487
15557
|
return 2
|
|
15488
15558
|
fi
|
|
15489
15559
|
|
|
15490
15560
|
if [ ! -f "$prd_file" ]; then
|
|
15561
|
+
if [ "$show_json" = true ]; then
|
|
15562
|
+
printf '{"error":"PRD file not found","prd_file":"%s"}\n' "$prd_file"
|
|
15563
|
+
return 1
|
|
15564
|
+
fi
|
|
15491
15565
|
echo -e "${RED}PRD file not found: $prd_file${NC}"
|
|
15492
15566
|
return 1
|
|
15493
15567
|
fi
|
|
@@ -15528,6 +15602,60 @@ maybe_show_auto_plan() {
|
|
|
15528
15602
|
show_prd_plan "$abs_prd" "false" "false"
|
|
15529
15603
|
}
|
|
15530
15604
|
|
|
15605
|
+
# Suggest the closest known command for a typo (the did-you-mean hint).
|
|
15606
|
+
# Prints "Did you mean 'X'?" to stderr when an unknown command is within a
|
|
15607
|
+
# small Levenshtein distance of a real command. Silent (returns 1) when
|
|
15608
|
+
# nothing is close enough, so a genuinely-unknown command is not given a
|
|
15609
|
+
# misleading suggestion. Pure bash + awk (no extra deps); the candidate list
|
|
15610
|
+
# is the canonical set of top-level commands accepted by the dispatcher.
|
|
15611
|
+
_suggest_command() {
|
|
15612
|
+
local typo="$1"
|
|
15613
|
+
[ -n "$typo" ] || return 1
|
|
15614
|
+
# Canonical top-level command names (keep in sync with the dispatch case
|
|
15615
|
+
# below). Deprecated aliases are intentionally included so a typo of an
|
|
15616
|
+
# alias still resolves to a helpful pointer.
|
|
15617
|
+
local known="start run quick plan grill spec verify proof trust review \
|
|
15618
|
+
ultracode council demo dogfood heal modernize import issue github init template \
|
|
15619
|
+
status stop pause resume monitor watch watchdog dashboard web open preview share \
|
|
15620
|
+
memory context ctx checkpoint state report kpis cost metrics stats logs otel \
|
|
15621
|
+
syslog telemetry crash config provider doctor setup-skill onboard quickstart \
|
|
15622
|
+
welcome update self-update rollback failover cluster enterprise remote deploy docker \
|
|
15623
|
+
sandbox api mcp magic compound assets export wiki docs explain why audit compliance \
|
|
15624
|
+
secrets analyze optimize bench ci reset cleanup notify trigger voice sentrux \
|
|
15625
|
+
worktree wt projects cp rc trust-metrics serve agent code self_update test help version"
|
|
15626
|
+
local best
|
|
15627
|
+
best=$(printf '%s\n' $known | awk -v t="$typo" '
|
|
15628
|
+
function min3(a, b, c) { if (a < b) { if (a < c) return a; return c } if (b < c) return b; return c }
|
|
15629
|
+
function lev(s1, s2, n, m, i, j, prev, cur, cost, tmp) {
|
|
15630
|
+
n = length(s1); m = length(s2)
|
|
15631
|
+
if (n == 0) return m
|
|
15632
|
+
if (m == 0) return n
|
|
15633
|
+
for (j = 0; j <= m; j++) prev[j] = j
|
|
15634
|
+
for (i = 1; i <= n; i++) {
|
|
15635
|
+
cur[0] = i
|
|
15636
|
+
for (j = 1; j <= m; j++) {
|
|
15637
|
+
cost = (substr(s1, i, 1) == substr(s2, j, 1)) ? 0 : 1
|
|
15638
|
+
cur[j] = min3(prev[j] + 1, cur[j-1] + 1, prev[j-1] + cost)
|
|
15639
|
+
}
|
|
15640
|
+
for (j = 0; j <= m; j++) prev[j] = cur[j]
|
|
15641
|
+
}
|
|
15642
|
+
return prev[m]
|
|
15643
|
+
}
|
|
15644
|
+
{ d = lev(t, $1); if (d < bestd || NR == 1) { bestd = d; bestc = $1 } }
|
|
15645
|
+
END {
|
|
15646
|
+
# Only suggest when the edit distance is small relative to the typo
|
|
15647
|
+
# length: <=2 for short commands, <=3 for longer ones. This avoids
|
|
15648
|
+
# absurd suggestions (e.g. "xyz" -> "web").
|
|
15649
|
+
thr = (length(t) <= 4) ? 2 : 3
|
|
15650
|
+
if (bestd <= thr) print bestc
|
|
15651
|
+
}')
|
|
15652
|
+
if [ -n "$best" ]; then
|
|
15653
|
+
printf '%s\n' "$best"
|
|
15654
|
+
return 0
|
|
15655
|
+
fi
|
|
15656
|
+
return 1
|
|
15657
|
+
}
|
|
15658
|
+
|
|
15531
15659
|
# Main command dispatcher
|
|
15532
15660
|
main() {
|
|
15533
15661
|
# v7.5.18: early guard -- LOKI_PROVIDER=gemini is no longer supported.
|
|
@@ -15952,8 +16080,12 @@ main() {
|
|
|
15952
16080
|
fi
|
|
15953
16081
|
;;
|
|
15954
16082
|
*)
|
|
15955
|
-
echo -e "${RED}Unknown command: $command${NC}"
|
|
15956
|
-
|
|
16083
|
+
echo -e "${RED}Unknown command: $command${NC}" >&2
|
|
16084
|
+
local _suggestion
|
|
16085
|
+
if _suggestion=$(_suggest_command "$command"); then
|
|
16086
|
+
echo "Did you mean 'loki ${_suggestion}'?" >&2
|
|
16087
|
+
fi
|
|
16088
|
+
echo "Run 'loki help' for usage." >&2
|
|
15957
16089
|
exit 1
|
|
15958
16090
|
;;
|
|
15959
16091
|
esac
|
|
@@ -18329,9 +18461,15 @@ try:
|
|
|
18329
18461
|
print('No relevant memories found')
|
|
18330
18462
|
else:
|
|
18331
18463
|
for i, r in enumerate(results, 1):
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
score
|
|
18464
|
+
# Retrieval results expose _source/_score/_weighted_score and the
|
|
18465
|
+
# raw stored fields (pattern/description/goal/name), not
|
|
18466
|
+
# source/summary/score. Read the fields that actually exist so the
|
|
18467
|
+
# output is not a uniform [unknown] ... (score 0.00).
|
|
18468
|
+
source = r.get('_source', 'unknown')
|
|
18469
|
+
score = r.get('_score', r.get('_weighted_score', 0))
|
|
18470
|
+
summary = (r.get('pattern') or r.get('description') or r.get('goal')
|
|
18471
|
+
or (r.get('context') or {}).get('goal') or r.get('name')
|
|
18472
|
+
or 'No summary')[:80]
|
|
18335
18473
|
print(f'{i}. [{source}] {summary}... (score: {score:.2f})')
|
|
18336
18474
|
except ImportError as e:
|
|
18337
18475
|
print(f'Error: Required module not found - {e}')
|
|
@@ -21892,7 +22030,7 @@ cmd_trust_metrics() {
|
|
|
21892
22030
|
echo "Run 'loki trust-metrics' inside each project directory."
|
|
21893
22031
|
exit 2
|
|
21894
22032
|
;;
|
|
21895
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki trust-metrics --help' for usage."; exit 1 ;;
|
|
22033
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki trust-metrics --help' for usage." >&2; exit 1 ;;
|
|
21896
22034
|
esac
|
|
21897
22035
|
done
|
|
21898
22036
|
|
|
@@ -21947,7 +22085,7 @@ cmd_cost() {
|
|
|
21947
22085
|
--json) show_json=true; shift ;;
|
|
21948
22086
|
--last) last_n="${2:-0}"; shift 2 ;;
|
|
21949
22087
|
--last=*) last_n="${1#*=}"; shift ;;
|
|
21950
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki cost --help' for usage."; exit 1 ;;
|
|
22088
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki cost --help' for usage." >&2; exit 1 ;;
|
|
21951
22089
|
esac
|
|
21952
22090
|
done
|
|
21953
22091
|
|
|
@@ -22262,7 +22400,7 @@ cmd_metrics() {
|
|
|
22262
22400
|
echo "$response"
|
|
22263
22401
|
exit 0
|
|
22264
22402
|
;;
|
|
22265
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki metrics --help' for usage."; exit 1 ;;
|
|
22403
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki metrics --help' for usage." >&2; exit 1 ;;
|
|
22266
22404
|
esac
|
|
22267
22405
|
done
|
|
22268
22406
|
|
|
@@ -29444,7 +29582,7 @@ cmd_share() {
|
|
|
29444
29582
|
--private) visibility=""; shift ;;
|
|
29445
29583
|
--format) format="${2:-markdown}"; shift 2 ;;
|
|
29446
29584
|
--format=*) format="${1#*=}"; shift ;;
|
|
29447
|
-
*) echo -e "${RED}Unknown option: $1${NC}"; echo "Run 'loki share --help' for usage."; exit 1 ;;
|
|
29585
|
+
*) echo -e "${RED}Unknown option: $1${NC}" >&2; echo "Run 'loki share --help' for usage." >&2; exit 1 ;;
|
|
29448
29586
|
esac
|
|
29449
29587
|
done
|
|
29450
29588
|
|
|
@@ -30143,7 +30281,14 @@ cmd_docker() {
|
|
|
30143
30281
|
local -a fwd=()
|
|
30144
30282
|
while [ $# -gt 0 ]; do
|
|
30145
30283
|
case "$1" in
|
|
30146
|
-
--image)
|
|
30284
|
+
--image)
|
|
30285
|
+
# Guard against a missing OR flag-shaped value so "--image
|
|
30286
|
+
# --dry-run" cannot swallow the next flag as the image ref.
|
|
30287
|
+
case "${2:-}" in
|
|
30288
|
+
-*|"") echo "loki docker --image requires a value (e.g., --image asklokesh/loki-mode:latest)" >&2; return 1 ;;
|
|
30289
|
+
*) shift; export LOKI_DOCKER_IMAGE="$1"; shift ;;
|
|
30290
|
+
esac
|
|
30291
|
+
;;
|
|
30147
30292
|
--dry-run) dry_run=1; shift ;;
|
|
30148
30293
|
--api) with_api=1; fwd+=("$1"); shift ;;
|
|
30149
30294
|
*) fwd+=("$1"); shift ;;
|