loki-mode 5.26.1 → 5.27.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: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v5.26.1
6
+ # Loki Mode v5.27.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -260,4 +260,4 @@ Auto-detected or force with `LOKI_COMPLEXITY`:
260
260
 
261
261
  ---
262
262
 
263
- **v5.26.1 | Release workflow fix, shell/dashboard/CI bugs | ~270 lines core**
263
+ **v5.27.0 | 57-bug audit fix (10 parallel agents, 3-member council review) | ~270 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.26.1
1
+ 5.27.0
@@ -226,6 +226,7 @@ council_vote() {
226
226
  1) role="requirements_verifier" ;;
227
227
  2) role="test_auditor" ;;
228
228
  3) role="devils_advocate" ;;
229
+ *) role="generalist" ;;
229
230
  esac
230
231
 
231
232
  log_info "Council member $member/$COUNCIL_SIZE ($role) reviewing..."
@@ -255,7 +256,24 @@ council_vote() {
255
256
  ((member++))
256
257
  done
257
258
 
258
- # Record vote results
259
+ # Anti-sycophancy check: if unanimous APPROVE, run devil's advocate
260
+ if [ $approve_count -eq $COUNCIL_SIZE ] && [ $COUNCIL_SIZE -ge 3 ]; then
261
+ log_warn "Unanimous approval detected - running anti-sycophancy check..."
262
+ local contrarian_verdict
263
+ contrarian_verdict=$(council_devils_advocate "$evidence_file" "$vote_dir")
264
+ local contrarian_vote
265
+ contrarian_vote=$(echo "$contrarian_verdict" | grep -oE "VOTE:\s*(APPROVE|REJECT)" | grep -oE "APPROVE|REJECT" | head -1)
266
+
267
+ if [ "$contrarian_vote" = "REJECT" ]; then
268
+ log_warn "Anti-sycophancy: Devil's advocate REJECTED unanimous approval"
269
+ log_warn "Overriding to require one more iteration for verification"
270
+ approve_count=$((approve_count - 1))
271
+ else
272
+ log_info "Anti-sycophancy: Devil's advocate confirmed approval"
273
+ fi
274
+ fi
275
+
276
+ # Record vote results (AFTER anti-sycophancy check so verdict reflects any override)
259
277
  _COUNCIL_STATE_FILE="$COUNCIL_STATE_DIR/state.json" \
260
278
  _COUNCIL_SIZE="$COUNCIL_SIZE" \
261
279
  _COUNCIL_APPROVE="$approve_count" \
@@ -291,23 +309,6 @@ with open(state_file, 'w') as f:
291
309
  json.dump(state, f, indent=2)
292
310
  " || log_warn "Failed to record council vote results"
293
311
 
294
- # Anti-sycophancy check: if unanimous APPROVE, run devil's advocate
295
- if [ $approve_count -eq $COUNCIL_SIZE ] && [ $COUNCIL_SIZE -ge 3 ]; then
296
- log_warn "Unanimous approval detected - running anti-sycophancy check..."
297
- local contrarian_verdict
298
- contrarian_verdict=$(council_devils_advocate "$evidence_file" "$vote_dir")
299
- local contrarian_vote
300
- contrarian_vote=$(echo "$contrarian_verdict" | grep -oE "VOTE:\s*(APPROVE|REJECT)" | grep -oE "APPROVE|REJECT" | head -1)
301
-
302
- if [ "$contrarian_vote" = "REJECT" ]; then
303
- log_warn "Anti-sycophancy: Devil's advocate REJECTED unanimous approval"
304
- log_warn "Overriding to require one more iteration for verification"
305
- approve_count=$((approve_count - 1))
306
- else
307
- log_info "Anti-sycophancy: Devil's advocate confirmed approval"
308
- fi
309
- fi
310
-
311
312
  echo ""
312
313
  log_info "Council verdict: $approve_count APPROVE / $reject_count REJECT (threshold: $COUNCIL_THRESHOLD)"
313
314
  echo -e "$verdicts"
@@ -29,7 +29,7 @@ fi
29
29
 
30
30
  # Check for TODO/FIXME in recent changes
31
31
  if [ -d "$CWD/.git" ]; then
32
- TODOS=$(git -C "$CWD" diff HEAD~1 2>/dev/null | grep -c "TODO\|FIXME" || echo "0")
32
+ TODOS=$(git -C "$CWD" diff HEAD~1 2>/dev/null | grep -c "TODO\|FIXME" 2>/dev/null) || TODOS=0
33
33
  if [ "$TODOS" -gt 0 ]; then
34
34
  GATE_RESULTS+=("new_todos: $TODOS")
35
35
  fi
@@ -11,6 +11,7 @@ METRICS_DIR="$CWD/.loki/metrics"
11
11
  mkdir -p "$METRICS_DIR"
12
12
 
13
13
  TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
14
- echo "{\"timestamp\":\"$TIMESTAMP\",\"tool\":\"$TOOL_NAME\",\"event\":\"PostToolUse\"}" >> "$METRICS_DIR/tool-usage.jsonl"
14
+ tool_escaped=$(printf '%s' "$TOOL_NAME" | sed 's/\\/\\\\/g; s/"/\\"/g')
15
+ echo "{\"timestamp\":\"$TIMESTAMP\",\"tool\":\"$tool_escaped\",\"event\":\"PostToolUse\"}" >> "$METRICS_DIR/tool-usage.jsonl"
15
16
 
16
17
  exit 0
@@ -10,13 +10,13 @@ CWD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).ge
10
10
 
11
11
  # Dangerous command patterns
12
12
  BLOCKED_PATTERNS=(
13
- "^rm -rf /$"
14
- "^rm -rf ~$"
15
- "^rm -rf \\\$HOME$"
13
+ "^rm -rf /"
14
+ "^rm -rf ~"
15
+ "^rm -rf \\\$HOME"
16
16
  "> /dev/sd"
17
17
  "^mkfs "
18
18
  "^dd if=/dev/zero"
19
- "^chmod -R 777 /$"
19
+ "^chmod -R 777 /"
20
20
  )
21
21
 
22
22
  # Check for blocked patterns
@@ -38,7 +38,8 @@ done
38
38
  # Log command to audit trail
39
39
  LOG_DIR="$CWD/.loki/logs"
40
40
  mkdir -p "$LOG_DIR"
41
- echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"command\":$(echo "$COMMAND" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')}" >> "$LOG_DIR/bash-audit.jsonl"
41
+ printf '%s' "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"command\":$(echo "$COMMAND" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')}" >> "$LOG_DIR/bash-audit.jsonl"
42
+ echo >> "$LOG_DIR/bash-audit.jsonl"
42
43
 
43
44
  # Allow command
44
45
  cat << EOF
package/autonomy/loki CHANGED
@@ -55,9 +55,12 @@ find_skill_dir() {
55
55
  local script_dir
56
56
  script_dir=$(dirname "$script_path")
57
57
 
58
+ # Check script's own parent first (most likely current), then installed skill, then cwd
59
+ local script_parent
60
+ script_parent="$(cd "$script_dir/.." 2>/dev/null && pwd)"
58
61
  local dirs=(
62
+ "$script_parent"
59
63
  "$HOME/.claude/skills/loki-mode"
60
- "$script_dir/.."
61
64
  "."
62
65
  )
63
66
 
@@ -444,6 +447,18 @@ cmd_start() {
444
447
 
445
448
  if [ -n "$prd_file" ]; then
446
449
  args+=("$prd_file")
450
+ else
451
+ # No PRD file specified -- warn and confirm before consuming API credits
452
+ echo -e "${YELLOW}Warning: No PRD file specified.${NC}"
453
+ echo "Loki Mode will start autonomous execution in the current directory"
454
+ echo "without a requirements document."
455
+ echo ""
456
+ echo -e "This will consume API credits. Continue? [y/N] \c"
457
+ read -r confirm
458
+ if [[ ! "$confirm" =~ ^[Yy] ]]; then
459
+ echo "Aborted. Usage: loki start <path-to-prd.md>"
460
+ exit 0
461
+ fi
447
462
  fi
448
463
 
449
464
  # Load saved provider if not specified on command line
@@ -604,15 +619,16 @@ cmd_stop() {
604
619
  if [ -f "$LOKI_DIR/session.json" ]; then
605
620
  # Use python for reliable JSON update, fall back to overwrite
606
621
  python3 -c "
607
- import json, sys
622
+ import json, sys, os
608
623
  try:
609
- with open('$LOKI_DIR/session.json', 'r+') as f:
624
+ p = os.path.join(sys.argv[1], 'session.json')
625
+ with open(p, 'r+') as f:
610
626
  d = json.load(f)
611
627
  d['status'] = 'stopped'
612
628
  f.seek(0); f.truncate()
613
629
  json.dump(d, f)
614
630
  except: pass
615
- " 2>/dev/null || true
631
+ " "$LOKI_DIR" 2>/dev/null || true
616
632
  fi
617
633
 
618
634
  # Clean up control files
@@ -692,15 +708,31 @@ cmd_pause() {
692
708
  cmd_resume() {
693
709
  if [ ! -d "$LOKI_DIR" ]; then
694
710
  echo -e "${YELLOW}No .loki directory found.${NC}"
695
- echo "Nothing to resume. Start a session with: loki start"
711
+ echo "No session to resume. Start a session with: loki start"
712
+ exit 0
713
+ fi
714
+
715
+ # Check if there is anything to resume before touching signal files
716
+ local has_pause_signal=false
717
+ local has_stop_signal=false
718
+ [ -f "$LOKI_DIR/PAUSE" ] && has_pause_signal=true
719
+ [ -f "$LOKI_DIR/STOP" ] && has_stop_signal=true
720
+ local session_running=false
721
+ is_session_running && session_running=true
722
+
723
+ # If nothing is paused/stopped and no session is running, exit early
724
+ if ! $has_pause_signal && ! $has_stop_signal && ! $session_running; then
725
+ echo -e "${YELLOW}No session to resume.${NC}"
726
+ echo "Start a session with: loki start"
696
727
  exit 0
697
728
  fi
698
729
 
730
+ # Clear the signal file if one exists
699
731
  local removed_signal=""
700
- if [ -f "$LOKI_DIR/PAUSE" ]; then
732
+ if $has_pause_signal; then
701
733
  rm -f "$LOKI_DIR/PAUSE"
702
734
  removed_signal="PAUSE"
703
- elif [ -f "$LOKI_DIR/STOP" ]; then
735
+ elif $has_stop_signal; then
704
736
  rm -f "$LOKI_DIR/STOP"
705
737
  removed_signal="STOP"
706
738
  fi
@@ -716,19 +748,15 @@ cmd_resume() {
716
748
  --action-sequence '["check_signals", "clear_signal", "resume"]' \
717
749
  --outcome success \
718
750
  --confidence 0.85
719
- if is_session_running; then
751
+ if $session_running; then
720
752
  echo -e "${GREEN}$removed_signal signal cleared. Session will resume automatically.${NC}"
721
753
  else
722
754
  echo -e "${GREEN}$removed_signal signal cleared.${NC}"
723
755
  echo "Session is not running. Start with: loki start"
724
756
  fi
725
757
  else
726
- if is_session_running; then
727
- echo -e "${CYAN}Session is running normally.${NC}"
728
- else
729
- echo -e "${YELLOW}No pause/stop signals found.${NC}"
730
- echo "Session is not running. Start with: loki start"
731
- fi
758
+ # Session is running but no signals to clear
759
+ echo -e "${CYAN}Session is running normally. Nothing to resume.${NC}"
732
760
  fi
733
761
  }
734
762
 
@@ -914,7 +942,7 @@ cmd_provider_set() {
914
942
  echo "Install: npm install -g @openai/codex"
915
943
  ;;
916
944
  gemini)
917
- echo "Install: npm install -g @anthropic-ai/gemini-cli"
945
+ echo "Install: npm install -g @google/gemini-cli"
918
946
  ;;
919
947
  esac
920
948
  echo ""
@@ -998,7 +1026,7 @@ cmd_provider_info() {
998
1026
  echo "Name: Codex CLI"
999
1027
  echo "Vendor: OpenAI"
1000
1028
  echo "CLI: codex"
1001
- echo "Flag: exec --dangerously-bypass-approvals-and-sandbox"
1029
+ echo "Flag: --full-auto"
1002
1030
  echo ""
1003
1031
  echo "Features:"
1004
1032
  echo " - Autonomous mode"
@@ -1010,7 +1038,7 @@ cmd_provider_info() {
1010
1038
  echo "Name: Gemini CLI"
1011
1039
  echo "Vendor: Google"
1012
1040
  echo "CLI: gemini"
1013
- echo "Flag: --yolo"
1041
+ echo "Flag: --approval-mode=yolo"
1014
1042
  echo ""
1015
1043
  echo "Features:"
1016
1044
  echo " - Autonomous mode"
@@ -1544,7 +1572,7 @@ cmd_issue_parse() {
1544
1572
  ;;
1545
1573
  --format)
1546
1574
  format="${2:-yaml}"
1547
- shift 2
1575
+ if [ $# -ge 2 ]; then shift 2; else shift; fi
1548
1576
  ;;
1549
1577
  --format=*)
1550
1578
  format="${1#*=}"
@@ -1552,7 +1580,7 @@ cmd_issue_parse() {
1552
1580
  ;;
1553
1581
  --output|-o)
1554
1582
  output_file="${2:-}"
1555
- shift 2
1583
+ if [ $# -ge 2 ]; then shift 2; else shift; fi
1556
1584
  ;;
1557
1585
  --output=*)
1558
1586
  output_file="${1#*=}"
@@ -2263,7 +2291,7 @@ cmd_api() {
2263
2291
  fi
2264
2292
 
2265
2293
  # Start server
2266
- mkdir -p "$LOKI_DIR"
2294
+ mkdir -p "$LOKI_DIR/logs"
2267
2295
  nohup node "$api_server" --port "$port" > "$LOKI_DIR/logs/api.log" 2>&1 &
2268
2296
  local new_pid=$!
2269
2297
  echo "$new_pid" > "$pid_file"
@@ -2584,6 +2612,8 @@ send_slack_notification() {
2584
2612
  project_name=$(basename "$(pwd)")
2585
2613
  local timestamp
2586
2614
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')
2615
+ local message_escaped
2616
+ message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
2587
2617
 
2588
2618
  # Build Slack payload with blocks for better formatting
2589
2619
  local payload
@@ -2601,7 +2631,7 @@ send_slack_notification() {
2601
2631
  "type": "section",
2602
2632
  "text": {
2603
2633
  "type": "mrkdwn",
2604
- "text": "$message"
2634
+ "text": "$message_escaped"
2605
2635
  }
2606
2636
  },
2607
2637
  {
@@ -2638,6 +2668,8 @@ send_discord_notification() {
2638
2668
  project_name=$(basename "$(pwd)")
2639
2669
  local timestamp
2640
2670
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')
2671
+ local message_escaped
2672
+ message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
2641
2673
 
2642
2674
  # Build Discord embed payload
2643
2675
  local payload
@@ -2646,7 +2678,7 @@ send_discord_notification() {
2646
2678
  "embeds": [
2647
2679
  {
2648
2680
  "title": "Loki Mode: $event_type",
2649
- "description": "$message",
2681
+ "description": "$message_escaped",
2650
2682
  "color": 5814783,
2651
2683
  "footer": {
2652
2684
  "text": "Project: $project_name | $timestamp"
@@ -2677,6 +2709,8 @@ send_webhook_notification() {
2677
2709
  project_name=$(basename "$(pwd)")
2678
2710
  local timestamp
2679
2711
  timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
2712
+ local message_escaped
2713
+ message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
2680
2714
 
2681
2715
  # Build generic JSON payload
2682
2716
  local payload
@@ -2684,7 +2718,7 @@ send_webhook_notification() {
2684
2718
  {
2685
2719
  "source": "loki-mode",
2686
2720
  "event": "$event_type",
2687
- "message": "$message",
2721
+ "message": "$message_escaped",
2688
2722
  "project": "$project_name",
2689
2723
  "timestamp": "$timestamp",
2690
2724
  "cwd": "$(pwd)"
@@ -2855,14 +2889,15 @@ cmd_reset() {
2855
2889
  # Reset only retry count
2856
2890
  if command -v python3 &> /dev/null; then
2857
2891
  python3 -c "
2858
- import json
2859
- with open('$LOKI_DIR/autonomy-state.json', 'r') as f:
2892
+ import json, sys, os
2893
+ p = os.path.join(sys.argv[1], 'autonomy-state.json')
2894
+ with open(p, 'r') as f:
2860
2895
  state = json.load(f)
2861
2896
  state['retryCount'] = 0
2862
2897
  state['status'] = 'reset'
2863
- with open('$LOKI_DIR/autonomy-state.json', 'w') as f:
2898
+ with open(p, 'w') as f:
2864
2899
  json.dump(state, f, indent=4)
2865
- " 2>/dev/null
2900
+ " "$LOKI_DIR" 2>/dev/null
2866
2901
  echo -e "${GREEN}Retry count reset to 0${NC}"
2867
2902
  else
2868
2903
  rm -f "$LOKI_DIR/autonomy-state.json"
@@ -2970,11 +3005,12 @@ cmd_memory() {
2970
3005
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2971
3006
  if [ -f "$learnings_dir/patterns.jsonl" ]; then
2972
3007
  python3 -c "
2973
- import json
2974
- limit = $limit
2975
- project_filter = '$project'
3008
+ import json, sys, os
3009
+ limit = int(sys.argv[1])
3010
+ project_filter = sys.argv[2]
3011
+ filepath = os.path.join(sys.argv[3], 'patterns.jsonl')
2976
3012
  count = 0
2977
- with open('$learnings_dir/patterns.jsonl', 'r') as f:
3013
+ with open(filepath, 'r') as f:
2978
3014
  for line in f:
2979
3015
  if count >= limit:
2980
3016
  break
@@ -2988,7 +3024,7 @@ with open('$learnings_dir/patterns.jsonl', 'r') as f:
2988
3024
  except: pass
2989
3025
  if count == 0:
2990
3026
  print('(empty - no patterns recorded)')
2991
- " 2>/dev/null
3027
+ " "$limit" "$project" "$learnings_dir" 2>/dev/null
2992
3028
  else
2993
3029
  echo "(empty - no patterns recorded)"
2994
3030
  fi
@@ -2999,11 +3035,12 @@ if count == 0:
2999
3035
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
3000
3036
  if [ -f "$learnings_dir/mistakes.jsonl" ]; then
3001
3037
  python3 -c "
3002
- import json
3003
- limit = $limit
3004
- project_filter = '$project'
3038
+ import json, sys, os
3039
+ limit = int(sys.argv[1])
3040
+ project_filter = sys.argv[2]
3041
+ filepath = os.path.join(sys.argv[3], 'mistakes.jsonl')
3005
3042
  count = 0
3006
- with open('$learnings_dir/mistakes.jsonl', 'r') as f:
3043
+ with open(filepath, 'r') as f:
3007
3044
  for line in f:
3008
3045
  if count >= limit:
3009
3046
  break
@@ -3017,7 +3054,7 @@ with open('$learnings_dir/mistakes.jsonl', 'r') as f:
3017
3054
  except: pass
3018
3055
  if count == 0:
3019
3056
  print('(empty - no mistakes recorded)')
3020
- " 2>/dev/null
3057
+ " "$limit" "$project" "$learnings_dir" 2>/dev/null
3021
3058
  else
3022
3059
  echo "(empty - no mistakes recorded)"
3023
3060
  fi
@@ -3028,11 +3065,12 @@ if count == 0:
3028
3065
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
3029
3066
  if [ -f "$learnings_dir/successes.jsonl" ]; then
3030
3067
  python3 -c "
3031
- import json
3032
- limit = $limit
3033
- project_filter = '$project'
3068
+ import json, sys, os
3069
+ limit = int(sys.argv[1])
3070
+ project_filter = sys.argv[2]
3071
+ filepath = os.path.join(sys.argv[3], 'successes.jsonl')
3034
3072
  count = 0
3035
- with open('$learnings_dir/successes.jsonl', 'r') as f:
3073
+ with open(filepath, 'r') as f:
3036
3074
  for line in f:
3037
3075
  if count >= limit:
3038
3076
  break
@@ -3046,18 +3084,18 @@ with open('$learnings_dir/successes.jsonl', 'r') as f:
3046
3084
  except: pass
3047
3085
  if count == 0:
3048
3086
  print('(empty - no successes recorded)')
3049
- " 2>/dev/null
3087
+ " "$limit" "$project" "$learnings_dir" 2>/dev/null
3050
3088
  else
3051
3089
  echo "(empty - no successes recorded)"
3052
3090
  fi
3053
3091
  ;;
3054
3092
 
3055
3093
  all)
3056
- cmd_memory show patterns --limit "$limit"
3094
+ cmd_memory show patterns --limit "$limit" --project "$project"
3057
3095
  echo ""
3058
- cmd_memory show mistakes --limit "$limit"
3096
+ cmd_memory show mistakes --limit "$limit" --project "$project"
3059
3097
  echo ""
3060
- cmd_memory show successes --limit "$limit"
3098
+ cmd_memory show successes --limit "$limit" --project "$project"
3061
3099
  ;;
3062
3100
 
3063
3101
  *)
@@ -3078,12 +3116,13 @@ if count == 0:
3078
3116
  echo -e "${BOLD}Search Results for: $query${NC}"
3079
3117
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
3080
3118
 
3119
+ local query_escaped="${query//\'/\'\\\'\'}"
3081
3120
  python3 -c "
3082
3121
  import json
3083
3122
  import os
3084
3123
 
3085
3124
  learnings_dir = '$learnings_dir'
3086
- query = '$query'.lower()
3125
+ query = '${query_escaped}'.lower()
3087
3126
 
3088
3127
  for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
3089
3128
  filepath = os.path.join(learnings_dir, filename)