loki-mode 7.81.1 → 7.83.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 +641 -45
- package/autonomy/run.sh +202 -31
- package/completions/_loki +84 -6
- package/completions/loki.bash +25 -7
- 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 +7 -4
- 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.83.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.83.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.83.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
|