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 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.81.1
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.81.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
409
+ **v7.83.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.81.1
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
- local votes_json="["
2541
- local first=true
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
- # Build JSON array entry
2565
- if [ "$first" = "true" ]; then
2566
- first=false
2567
- else
2568
- votes_json="${votes_json},"
2569
- fi
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
- votes_json="${votes_json}]"
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
- local has_test_results=false
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
- if [ -f "$f" ]; then
2644
- has_test_results=true
2645
- # Look for test runner output indicating pass
2646
- if ! tail -30 "$f" 2>/dev/null | grep -qiE "(passed|success|ok|all tests)"; then
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 "\"level\":\s*\"error\"" 2>/dev/null || echo "0")
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
- # grep/sed fallback: match the LAST path segment as a key.
532
- # `|| true` keeps the no-match case (grep rc=1 under pipefail) from
533
- # tripping set -e in the loki CLI (which runs set -euo pipefail).
534
- local key escaped_key
535
- key="${yaml_path##*.}"
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 key escaped_key have_yq=0
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
- key="${yaml_path##*.}"; escaped_key="$(loki_escape_regex "$key")"
842
- value="$( { grep -E "^\s*${escaped_key}:" "$f" 2>/dev/null | head -1 \
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