loki-mode 6.60.0 → 6.62.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.
Files changed (64) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/app-runner.sh +34 -8
  4. package/autonomy/completion-council.sh +70 -32
  5. package/autonomy/issue-parser.sh +4 -7
  6. package/autonomy/loki +238 -119
  7. package/autonomy/notification-checker.py +49 -23
  8. package/autonomy/run.sh +162 -79
  9. package/autonomy/sandbox.sh +91 -24
  10. package/bin/loki-mode.js +1 -2
  11. package/bin/postinstall.js +10 -4
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/control.py +46 -36
  14. package/dashboard/database.py +21 -4
  15. package/dashboard/server.py +107 -78
  16. package/docs/BUG-AUDIT-v6.61.0.md +957 -0
  17. package/docs/INSTALLATION.md +2 -2
  18. package/events/bus.py +129 -28
  19. package/events/bus.ts +41 -27
  20. package/events/emit.sh +1 -1
  21. package/integrations/openclaw/README.md +139 -0
  22. package/integrations/openclaw/SKILL.md +88 -0
  23. package/integrations/openclaw/bridge/__init__.py +1 -0
  24. package/integrations/openclaw/bridge/__main__.py +88 -0
  25. package/integrations/openclaw/bridge/schema_map.py +180 -0
  26. package/integrations/openclaw/bridge/watcher.py +100 -0
  27. package/integrations/openclaw/scripts/format-progress.sh +80 -0
  28. package/integrations/openclaw/scripts/poll-status.sh +74 -0
  29. package/integrations/vibe-kanban.md +289 -0
  30. package/mcp/__init__.py +1 -1
  31. package/mcp/server.py +96 -73
  32. package/memory/consolidation.py +21 -6
  33. package/memory/engine.py +53 -26
  34. package/memory/layers/index_layer.py +16 -3
  35. package/memory/layers/timeline_layer.py +16 -3
  36. package/memory/retrieval.py +4 -1
  37. package/memory/schemas.py +4 -2
  38. package/memory/storage.py +25 -4
  39. package/memory/token_economics.py +9 -2
  40. package/memory/vector_index.py +2 -2
  41. package/package.json +3 -1
  42. package/providers/cline.sh +5 -4
  43. package/providers/codex.sh +27 -5
  44. package/providers/gemini.sh +59 -23
  45. package/providers/loader.sh +3 -2
  46. package/skills/parallel-workflows.md +9 -7
  47. package/state/__init__.py +10 -0
  48. package/state/index.ts +18 -0
  49. package/state/manager.py +1801 -0
  50. package/state/manager.ts +1774 -0
  51. package/state/sqlite_backend.py +188 -0
  52. package/state/test_manager.py +703 -0
  53. package/state/test_manager.ts +366 -0
  54. package/templates/README.md +19 -4
  55. package/templates/dashboard.md +45 -0
  56. package/templates/data-pipeline.md +45 -0
  57. package/templates/game.md +48 -0
  58. package/templates/microservice.md +49 -0
  59. package/templates/npm-library.md +42 -0
  60. package/templates/rest-api.md +170 -33
  61. package/templates/slack-bot.md +48 -0
  62. package/templates/web-scraper.md +45 -0
  63. package/web-app/server.py +360 -191
  64. package/templates/saas-app.md +0 -42
package/autonomy/loki CHANGED
@@ -696,13 +696,13 @@ cmd_start() {
696
696
  shift
697
697
  ;;
698
698
  --mirofish)
699
- mirofish_url="${2:-http://localhost:5001}"
700
- if [[ "${mirofish_url}" == --* ]] || [[ -z "${2:-}" ]]; then
701
- mirofish_url="http://localhost:5001"
699
+ if [[ -n "${2:-}" ]] && [[ "${2:-}" != --* ]]; then
700
+ mirofish_url="$2"
701
+ shift 2
702
702
  else
703
+ mirofish_url="http://localhost:5001"
703
704
  shift
704
705
  fi
705
- shift
706
706
  ;;
707
707
  --mirofish=*)
708
708
  mirofish_url="${1#*=}"
@@ -1178,7 +1178,7 @@ list_running_sessions() {
1178
1178
  if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
1179
1179
  # Avoid duplicates (if also in sessions/)
1180
1180
  local dup=false
1181
- for s in "${sessions[@]}"; do
1181
+ for s in ${sessions[@]+"${sessions[@]}"}; do
1182
1182
  if [[ "$s" == "$sid:"* ]]; then dup=true; break; fi
1183
1183
  done
1184
1184
  if ! $dup; then
@@ -1186,7 +1186,7 @@ list_running_sessions() {
1186
1186
  fi
1187
1187
  fi
1188
1188
  done
1189
- printf '%s\n' "${sessions[@]}"
1189
+ printf '%s\n' ${sessions[@]+"${sessions[@]}"}
1190
1190
  }
1191
1191
 
1192
1192
  # Kill a PID with SIGTERM, wait, then SIGKILL if needed
@@ -1595,11 +1595,16 @@ cmd_resume() {
1595
1595
 
1596
1596
  # Show current status
1597
1597
  cmd_status() {
1598
- # Check for --json flag
1598
+ # Check for flags
1599
1599
  while [[ $# -gt 0 ]]; do
1600
1600
  case "$1" in
1601
1601
  --json) cmd_status_json; return $? ;;
1602
- *) shift ;;
1602
+ --help|-h) echo "Usage: loki status [--json]"; return 0 ;;
1603
+ *)
1604
+ echo -e "${RED}Unknown flag: $1${NC}"
1605
+ echo "Usage: loki status [--json]"
1606
+ return 1
1607
+ ;;
1603
1608
  esac
1604
1609
  done
1605
1610
 
@@ -1691,7 +1696,7 @@ cmd_status() {
1691
1696
 
1692
1697
  # Check pending tasks
1693
1698
  if [ -f "$LOKI_DIR/queue/pending.json" ]; then
1694
- local task_count=$(jq '.tasks | length' "$LOKI_DIR/queue/pending.json" 2>/dev/null || echo "0")
1699
+ local task_count=$(jq 'if type == "array" then length elif .tasks then .tasks | length else 0 end' "$LOKI_DIR/queue/pending.json" 2>/dev/null || echo "0")
1695
1700
  echo -e "${CYAN}Pending Tasks:${NC} $task_count"
1696
1701
  fi
1697
1702
 
@@ -2784,24 +2789,26 @@ cmd_dashboard_start() {
2784
2789
  echo "$host" > "${DASHBOARD_PID_DIR}/host"
2785
2790
  echo "$url_scheme" > "${DASHBOARD_PID_DIR}/scheme"
2786
2791
 
2787
- # Wait for PID file to be written (up to 10 seconds)
2788
- # Fixes race condition where PID file may not exist yet when checked
2789
- local pid_wait_retries=20
2790
- local pid_wait_interval=0.5
2791
- while [[ $pid_wait_retries -gt 0 ]]; do
2792
- if [[ -f "$DASHBOARD_PID_FILE" ]]; then
2792
+ # Wait for dashboard HTTP endpoint to become ready (up to 10 seconds)
2793
+ local health_retries=20
2794
+ local health_interval=0.5
2795
+ local health_ok=false
2796
+ while [[ $health_retries -gt 0 ]]; do
2797
+ if curl -sf "http://${host}:${port}/api/status" >/dev/null 2>&1; then
2798
+ health_ok=true
2793
2799
  break
2794
2800
  fi
2795
- sleep "$pid_wait_interval"
2796
- pid_wait_retries=$((pid_wait_retries - 1))
2801
+ # Also check if the process died
2802
+ if ! kill -0 "$new_pid" 2>/dev/null; then
2803
+ break
2804
+ fi
2805
+ sleep "$health_interval"
2806
+ health_retries=$((health_retries - 1))
2797
2807
  done
2798
- if [[ ! -f "$DASHBOARD_PID_FILE" ]]; then
2799
- echo -e "${YELLOW}Warning: Dashboard PID file not written within timeout${NC}"
2808
+ if ! $health_ok && kill -0 "$new_pid" 2>/dev/null; then
2809
+ echo -e "${YELLOW}Warning: Dashboard started but health check did not respond within timeout${NC}"
2800
2810
  fi
2801
2811
 
2802
- # Wait a moment and check if process is still running
2803
- sleep 1
2804
-
2805
2812
  if kill -0 "$new_pid" 2>/dev/null; then
2806
2813
  local url="${url_scheme}://${host}:${port}"
2807
2814
  echo -e "${GREEN}Dashboard server started${NC}"
@@ -3105,6 +3112,7 @@ cmd_web() {
3105
3112
  ;;
3106
3113
  *)
3107
3114
  # Treat unknown args as options to start (e.g., loki web --no-open)
3115
+ shift
3108
3116
  cmd_web_start "$subcommand" "$@"
3109
3117
  ;;
3110
3118
  esac
@@ -3147,10 +3155,18 @@ cmd_web_start() {
3147
3155
  shift
3148
3156
  ;;
3149
3157
  --port)
3158
+ if [[ -z "${2:-}" ]]; then
3159
+ echo -e "${RED}--port requires a port number${NC}"
3160
+ exit 1
3161
+ fi
3150
3162
  port="$2"
3151
3163
  shift 2
3152
3164
  ;;
3153
3165
  --prd)
3166
+ if [[ -z "${2:-}" ]]; then
3167
+ echo -e "${RED}--prd requires a file path${NC}"
3168
+ exit 1
3169
+ fi
3154
3170
  prd_file="$2"
3155
3171
  shift 2
3156
3172
  ;;
@@ -3356,7 +3372,7 @@ cmd_web_stop() {
3356
3372
  local pids_file="${LOKI_DIR}/purple-lab/child-pids.json"
3357
3373
  if [ -f "$pids_file" ]; then
3358
3374
  local tracked_pids
3359
- tracked_pids=$(python3 -c "import json; [print(p) for p in json.load(open('$pids_file'))]" 2>/dev/null || true)
3375
+ tracked_pids=$(LOKI_PIDS_FILE="$pids_file" python3 -c "import json, os; [print(p) for p in json.load(open(os.environ['LOKI_PIDS_FILE']))]" 2>/dev/null || true)
3360
3376
  if [ -n "$tracked_pids" ]; then
3361
3377
  echo "Cleaning up Purple Lab build processes..."
3362
3378
  for opid in $tracked_pids; do
@@ -3374,13 +3390,18 @@ cmd_web_stop() {
3374
3390
  fi
3375
3391
 
3376
3392
  # Also stop the Loki Dashboard if it was started by a Purple Lab session
3377
- local dash_pid
3378
- dash_pid=$(lsof -ti:57374 -sTCP:LISTEN 2>/dev/null | head -1 || true)
3379
- if [ -n "$dash_pid" ]; then
3380
- kill "$dash_pid" 2>/dev/null || true
3381
- sleep 1
3382
- kill -0 "$dash_pid" 2>/dev/null && kill -9 "$dash_pid" 2>/dev/null || true
3383
- echo "Loki Dashboard stopped (port 57374)"
3393
+ local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
3394
+ local dash_pid_file="${LOKI_DIR}/dashboard/dashboard.pid"
3395
+ if [ -f "$dash_pid_file" ]; then
3396
+ local dash_pid
3397
+ dash_pid=$(cat "$dash_pid_file" 2>/dev/null)
3398
+ if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
3399
+ kill "$dash_pid" 2>/dev/null || true
3400
+ sleep 1
3401
+ kill -0 "$dash_pid" 2>/dev/null && kill -9 "$dash_pid" 2>/dev/null || true
3402
+ echo "Loki Dashboard stopped (port $dash_port)"
3403
+ fi
3404
+ rm -f "$dash_pid_file" 2>/dev/null || true
3384
3405
  fi
3385
3406
  }
3386
3407
 
@@ -3536,7 +3557,7 @@ cmd_github() {
3536
3557
 
3537
3558
  # gh CLI
3538
3559
  if command -v gh &>/dev/null; then
3539
- echo -e " gh CLI: ${GREEN}installed$(gh --version 2>/dev/null | head -1 | sed 's/gh version /v/')${NC}"
3560
+ echo -e " gh CLI: ${GREEN}installed $(gh --version 2>/dev/null | head -1 | sed 's/gh version /v/')${NC}"
3540
3561
  if gh auth status &>/dev/null 2>&1; then
3541
3562
  echo -e " Auth: ${GREEN}authenticated${NC}"
3542
3563
  else
@@ -4802,16 +4823,9 @@ cmd_watch() {
4802
4823
 
4803
4824
  case "$watcher" in
4804
4825
  fswatch)
4805
- # Use fswatch for native macOS filesystem events
4806
- fswatch -1 --latency "$debounce" "$prd_path" | while read -r _event; do
4807
- _watch_trigger
4808
- _watch_status_line
4809
- # Re-attach fswatch for next change
4810
- fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1 || true
4811
- done
4812
- # Fallback: loop-based fswatch for continuous watching
4826
+ # Use fswatch with loop-based approach (avoids pipe subshell PID loss)
4813
4827
  while true; do
4814
- fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1
4828
+ fswatch -1 --latency "$debounce" "$prd_path" > /dev/null 2>&1 || true
4815
4829
  _watch_trigger
4816
4830
  _watch_status_line
4817
4831
  done
@@ -4920,14 +4934,14 @@ _export_json() {
4920
4934
  fi
4921
4935
 
4922
4936
  local json_output
4923
- json_output=$(python3 << 'EXPORT_JSON'
4937
+ json_output=$(LOKI_DIR="$LOKI_DIR" LOKI_VERSION="$(get_version)" python3 << 'EXPORT_JSON'
4924
4938
  import json, os, glob
4925
4939
  from datetime import datetime
4926
4940
 
4927
4941
  loki_dir = os.environ.get("LOKI_DIR", ".loki")
4928
4942
  export = {
4929
4943
  "exported_at": datetime.utcnow().isoformat() + "Z",
4930
- "version": "6.0.0",
4944
+ "version": os.environ.get("LOKI_VERSION", "0.0.0"),
4931
4945
  "session": {},
4932
4946
  "queue": {},
4933
4947
  "quality": {},
@@ -5045,7 +5059,7 @@ _export_csv() {
5045
5059
  fi
5046
5060
 
5047
5061
  local csv_output
5048
- csv_output=$(python3 << 'EXPORT_CSV'
5062
+ csv_output=$(LOKI_DIR="$LOKI_DIR" python3 << 'EXPORT_CSV'
5049
5063
  import json, os, csv, io
5050
5064
 
5051
5065
  loki_dir = os.environ.get("LOKI_DIR", ".loki")
@@ -5058,7 +5072,8 @@ for queue in ["pending", "in-progress", "completed", "failed"]:
5058
5072
  if os.path.exists(qf):
5059
5073
  try:
5060
5074
  with open(qf) as f:
5061
- tasks = json.load(f)
5075
+ data = json.load(f)
5076
+ tasks = data.get("tasks", data) if isinstance(data, dict) else data
5062
5077
  for t in tasks:
5063
5078
  writer.writerow([
5064
5079
  queue,
@@ -5191,18 +5206,34 @@ cmd_config() {
5191
5206
 
5192
5207
  # v6.0.0: Set a configuration value
5193
5208
  cmd_config_set() {
5209
+ local use_global=false
5210
+
5211
+ # Check for --global flag
5212
+ if [[ "${1:-}" == "--global" ]]; then
5213
+ use_global=true
5214
+ shift
5215
+ fi
5216
+
5194
5217
  local key="${1:-}"
5195
5218
  local value="${2:-}"
5196
5219
 
5197
5220
  if [[ -z "$key" || -z "$value" ]]; then
5198
- echo -e "${RED}Usage: loki config set <key> <value>${NC}"
5221
+ echo -e "${RED}Usage: loki config set [--global] <key> <value>${NC}"
5199
5222
  echo "Run 'loki config' for list of settable keys."
5200
5223
  return 1
5201
5224
  fi
5202
5225
 
5226
+ # Determine config directory: --global writes to ~/.config/loki-mode/
5227
+ local config_dir
5228
+ if $use_global; then
5229
+ config_dir="${HOME}/.config/loki-mode"
5230
+ else
5231
+ config_dir="$LOKI_DIR/config"
5232
+ fi
5233
+
5203
5234
  # Ensure config directory exists
5204
- mkdir -p "$LOKI_DIR/config"
5205
- local config_store="$LOKI_DIR/config/settings.json"
5235
+ mkdir -p "$config_dir"
5236
+ local config_store="$config_dir/settings.json"
5206
5237
 
5207
5238
  # Initialize if not exists
5208
5239
  if [ ! -f "$config_store" ]; then
@@ -5284,7 +5315,21 @@ for part in parts[:-1]:
5284
5315
  if part not in current or not isinstance(current[part], dict):
5285
5316
  current[part] = {}
5286
5317
  current = current[part]
5287
- current[parts[-1]] = value
5318
+ # Type coercion: try int, then float, then bool, else string
5319
+ def coerce_type(v):
5320
+ if v.lower() in ('true', 'false'):
5321
+ return v.lower() == 'true'
5322
+ try:
5323
+ return int(v)
5324
+ except ValueError:
5325
+ pass
5326
+ try:
5327
+ return float(v)
5328
+ except ValueError:
5329
+ pass
5330
+ return v
5331
+
5332
+ current[parts[-1]] = coerce_type(value)
5288
5333
 
5289
5334
  with open(cfg_file, "w") as f:
5290
5335
  json.dump(config, f, indent=2)
@@ -5319,8 +5364,7 @@ cmd_config_get() {
5319
5364
  return 0
5320
5365
  fi
5321
5366
 
5322
- export LOKI_CFG_FILE="$config_store"
5323
- export LOKI_CFG_KEY="$key"
5367
+ LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" \
5324
5368
  python3 << 'GET_CONFIG'
5325
5369
  import json, os
5326
5370
  cfg_file = os.environ["LOKI_CFG_FILE"]
@@ -5741,7 +5785,10 @@ cmd_doctor() {
5741
5785
  echo -e " ${GREEN}PASS${NC} $sname ${DIM}$short_path${NC}"
5742
5786
  pass_count=$((pass_count + 1))
5743
5787
  elif [ -L "$sdir" ]; then
5744
- echo -e " ${RED}FAIL${NC} $sname ${DIM}(broken symlink at $short_path)${NC}"
5788
+ local _target
5789
+ _target=$(readlink "$sdir" 2>/dev/null || echo "unknown")
5790
+ echo -e " ${RED}FAIL${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
5791
+ echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
5745
5792
  fail_count=$((fail_count + 1))
5746
5793
  else
5747
5794
  echo -e " ${YELLOW}WARN${NC} $sname ${DIM}(not found - run 'loki setup-skill')${NC}"
@@ -6946,30 +6993,34 @@ cmd_init() {
6946
6993
  ai-chatbot
6947
6994
  )
6948
6995
 
6949
- local -A TEMPLATE_LABELS=(
6950
- [simple-todo-app]="Simple Todo App"
6951
- [static-landing-page]="Static Landing Page"
6952
- [api-only]="REST API (No Frontend)"
6953
- [rest-api]="REST API"
6954
- [rest-api-auth]="REST API with Auth"
6955
- [cli-tool]="CLI Tool"
6956
- [discord-bot]="Discord Bot"
6957
- [chrome-extension]="Chrome Extension"
6958
- [blog-platform]="Blog Platform"
6959
- [full-stack-demo]="Full-Stack Demo"
6960
- [web-scraper]="Web Scraper"
6961
- [data-pipeline]="Data Pipeline"
6962
- [dashboard]="Analytics Dashboard"
6963
- [game]="Browser Game"
6964
- [slack-bot]="Slack Bot"
6965
- [npm-library]="npm Library"
6966
- [microservice]="Microservice"
6967
- [mobile-app]="Mobile App"
6968
- [saas-app]="SaaS Application"
6969
- [saas-starter]="SaaS Starter Kit"
6970
- [e-commerce]="E-Commerce Store"
6971
- [ai-chatbot]="AI Chatbot (RAG)"
6972
- )
6996
+ # Template label lookup function (bash 3.2 compatible - no associative arrays)
6997
+ _get_template_label() {
6998
+ case "$1" in
6999
+ simple-todo-app) echo "Simple Todo App" ;;
7000
+ static-landing-page) echo "Static Landing Page" ;;
7001
+ api-only) echo "REST API (No Frontend)" ;;
7002
+ rest-api) echo "REST API" ;;
7003
+ rest-api-auth) echo "REST API with Auth" ;;
7004
+ cli-tool) echo "CLI Tool" ;;
7005
+ discord-bot) echo "Discord Bot" ;;
7006
+ chrome-extension) echo "Chrome Extension" ;;
7007
+ blog-platform) echo "Blog Platform" ;;
7008
+ full-stack-demo) echo "Full-Stack Demo" ;;
7009
+ web-scraper) echo "Web Scraper" ;;
7010
+ data-pipeline) echo "Data Pipeline" ;;
7011
+ dashboard) echo "Analytics Dashboard" ;;
7012
+ game) echo "Browser Game" ;;
7013
+ slack-bot) echo "Slack Bot" ;;
7014
+ npm-library) echo "npm Library" ;;
7015
+ microservice) echo "Microservice" ;;
7016
+ mobile-app) echo "Mobile App" ;;
7017
+ saas-app) echo "SaaS Application" ;;
7018
+ saas-starter) echo "SaaS Starter Kit" ;;
7019
+ e-commerce) echo "E-Commerce Store" ;;
7020
+ ai-chatbot) echo "AI Chatbot (RAG)" ;;
7021
+ *) echo "$1" ;;
7022
+ esac
7023
+ }
6973
7024
 
6974
7025
  local template_count=${#TEMPLATE_NAMES[@]}
6975
7026
 
@@ -7067,7 +7118,7 @@ cmd_init() {
7067
7118
  else
7068
7119
  echo ","
7069
7120
  fi
7070
- printf ' {"name": "%s", "label": "%s"}' "$tname" "${TEMPLATE_LABELS[$tname]}"
7121
+ printf ' {"name": "%s", "label": "%s"}' "$tname" "$(_get_template_label "$tname")"
7071
7122
  done
7072
7123
  echo ""
7073
7124
  echo "]"
@@ -7082,7 +7133,7 @@ cmd_init() {
7082
7133
  elif [[ "$tname" == "mobile-app" ]]; then
7083
7134
  echo -e " ${DIM}Complex:${NC}"
7084
7135
  fi
7085
- printf " %2d. ${CYAN}%-22s${NC} %s\n" "$idx" "$tname" "${TEMPLATE_LABELS[$tname]}"
7136
+ printf " %2d. ${CYAN}%-22s${NC} %s\n" "$idx" "$tname" "$(_get_template_label "$tname")"
7086
7137
  idx=$((idx + 1))
7087
7138
  done
7088
7139
  echo ""
@@ -7105,7 +7156,7 @@ cmd_init() {
7105
7156
  elif [[ "$tname" == "mobile-app" ]]; then
7106
7157
  echo -e " ${DIM}Complex:${NC}"
7107
7158
  fi
7108
- printf " %2d. %-22s %s\n" "$idx" "$tname" "${TEMPLATE_LABELS[$tname]}"
7159
+ printf " %2d. %-22s %s\n" "$idx" "$tname" "$(_get_template_label "$tname")"
7109
7160
  idx=$((idx + 1))
7110
7161
  done
7111
7162
  echo ""
@@ -7119,7 +7170,27 @@ cmd_init() {
7119
7170
 
7120
7171
  template_name="${TEMPLATE_NAMES[$((choice - 1))]}"
7121
7172
  echo ""
7122
- echo -e "Selected: ${CYAN}$template_name${NC} (${TEMPLATE_LABELS[$template_name]})"
7173
+ echo -e "Selected: ${CYAN}$template_name${NC} ($(_get_template_label "$template_name"))"
7174
+ fi
7175
+
7176
+ # Validate template_name against known list before filesystem lookup
7177
+ local _tpl_valid=false
7178
+ for _tpl_check in "${TEMPLATE_NAMES[@]}"; do
7179
+ if [[ "$_tpl_check" == "$template_name" ]]; then
7180
+ _tpl_valid=true
7181
+ break
7182
+ fi
7183
+ done
7184
+ if ! $_tpl_valid; then
7185
+ echo -e "${RED}Unknown template: $template_name${NC}"
7186
+ echo ""
7187
+ echo "Available templates:"
7188
+ for tname in "${TEMPLATE_NAMES[@]}"; do
7189
+ echo " $tname"
7190
+ done
7191
+ echo ""
7192
+ echo "Run 'loki init --list' for details."
7193
+ exit 1
7123
7194
  fi
7124
7195
 
7125
7196
  # Resolve template file
@@ -7161,6 +7232,19 @@ cmd_init() {
7161
7232
  target_dir="$(pwd)"
7162
7233
  fi
7163
7234
 
7235
+ # Guard: check if target directory has an active .loki/ session
7236
+ if [[ -n "$project_name" ]] && [[ -d "$target_dir/.loki" ]]; then
7237
+ if [[ -f "$target_dir/.loki/loki.pid" ]]; then
7238
+ local _guard_pid
7239
+ _guard_pid=$(cat "$target_dir/.loki/loki.pid" 2>/dev/null)
7240
+ if [[ -n "$_guard_pid" ]] && kill -0 "$_guard_pid" 2>/dev/null; then
7241
+ echo -e "${RED}Cannot initialize: active session running in $target_dir. Stop it first.${NC}"
7242
+ return 1
7243
+ fi
7244
+ fi
7245
+ echo -e "${YELLOW}Reinitializing existing .loki/ directory in $target_dir${NC}"
7246
+ fi
7247
+
7164
7248
  # Build config JSON
7165
7249
  local config_json
7166
7250
  config_json=$(cat <<ENDJSON
@@ -7246,7 +7330,7 @@ ENDGITIGNORE
7246
7330
  if [[ -n "$project_name" ]]; then
7247
7331
  echo -e " ${GREEN}CREATE${NC} $target_dir/"
7248
7332
  fi
7249
- echo -e " ${GREEN}CREATE${NC} $target_dir/prd.md (${TEMPLATE_LABELS[$template_name]} template)"
7333
+ echo -e " ${GREEN}CREATE${NC} $target_dir/prd.md ($(_get_template_label "$template_name") template)"
7250
7334
  echo -e " ${GREEN}CREATE${NC} $target_dir/.loki/"
7251
7335
  echo -e " ${GREEN}CREATE${NC} $target_dir/.loki/loki.config.json"
7252
7336
  echo -e " ${GREEN}CREATE${NC} $target_dir/README.md"
@@ -7278,8 +7362,10 @@ ENDGITIGNORE
7278
7362
  echo "$config_json" > "$target_dir/.loki/loki.config.json"
7279
7363
 
7280
7364
  # Only write README if it does not already exist
7365
+ local did_write_readme=false
7281
7366
  if [[ ! -f "$target_dir/README.md" ]]; then
7282
7367
  echo "$readme_content" > "$target_dir/README.md"
7368
+ did_write_readme=true
7283
7369
  fi
7284
7370
 
7285
7371
  # Git init (unless --no-git or already in a git repo)
@@ -7297,9 +7383,9 @@ ENDGITIGNORE
7297
7383
  echo -e "${GREEN}Project scaffolded:${NC} $target_dir"
7298
7384
  echo ""
7299
7385
  echo " Files created:"
7300
- echo -e " prd.md ${DIM}${TEMPLATE_LABELS[$template_name]} template${NC}"
7386
+ echo -e " prd.md ${DIM}$(_get_template_label "$template_name") template${NC}"
7301
7387
  echo -e " .loki/loki.config.json ${DIM}project configuration${NC}"
7302
- if [[ ! -f "$target_dir/README.md" ]] || [[ -n "$project_name" ]]; then
7388
+ if $did_write_readme; then
7303
7389
  echo -e " README.md ${DIM}project readme${NC}"
7304
7390
  fi
7305
7391
  if $did_git_init; then
@@ -10617,24 +10703,25 @@ cmd_worktree() {
10617
10703
  fi
10618
10704
 
10619
10705
  # Validate JSON is parseable and contains required fields
10620
- if ! python3 -c "
10621
- import json, sys
10706
+ if ! LOKI_SIGNAL_FILE="$signal_file" python3 << 'MERGE_VALIDATE_PY'
10707
+ import json, sys, os
10622
10708
  try:
10623
- data = json.load(open('$signal_file'))
10709
+ data = json.load(open(os.environ['LOKI_SIGNAL_FILE']))
10624
10710
  assert data.get('branch'), 'missing branch'
10625
10711
  assert data.get('worktree'), 'missing worktree'
10626
10712
  except Exception as e:
10627
10713
  print(str(e), file=sys.stderr)
10628
10714
  sys.exit(1)
10629
- " 2>/dev/null; then
10715
+ MERGE_VALIDATE_PY
10716
+ then
10630
10717
  echo -e "${RED}Invalid or corrupted merge signal file${NC}"
10631
10718
  exit 1
10632
10719
  fi
10633
10720
 
10634
10721
  local branch
10635
- branch=$(python3 -c "import json; print(json.load(open('$signal_file'))['branch'])")
10722
+ branch=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['branch'])")
10636
10723
  local worktree_path
10637
- worktree_path=$(python3 -c "import json; print(json.load(open('$signal_file'))['worktree'])")
10724
+ worktree_path=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['worktree'])")
10638
10725
 
10639
10726
  if git merge --no-ff "$branch" -m "merge($name): auto-merge from parallel worktree"; then
10640
10727
  echo -e "${GREEN}Merge successful${NC}"
@@ -10771,30 +10858,46 @@ cmd_state() {
10771
10858
  esac
10772
10859
  done
10773
10860
 
10774
- PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
10775
- import json, sys
10776
- sys.path.insert(0, '${SKILL_DIR:-.}')
10861
+ PYTHONPATH="${SKILL_DIR:-.}" \
10862
+ LOKI_STATE_QUERY_TYPE="$query_type" \
10863
+ LOKI_STATE_AGENT_ID="$agent_id" \
10864
+ LOKI_STATE_EVENT_TYPE="$event_type" \
10865
+ LOKI_STATE_TOPIC="$topic" \
10866
+ LOKI_STATE_CLUSTER_ID="$cluster_id" \
10867
+ LOKI_STATE_MIGRATION_ID="$migration_id" \
10868
+ LOKI_STATE_LIMIT="$limit" \
10869
+ LOKI_STATE_SKILL_DIR="${SKILL_DIR:-.}" \
10870
+ python3 << 'STATE_QUERY_PY'
10871
+ import json, sys, os
10872
+ sys.path.insert(0, os.environ.get('LOKI_STATE_SKILL_DIR', '.'))
10777
10873
  from state.sqlite_backend import SqliteStateBackend
10778
10874
  db = SqliteStateBackend()
10779
10875
 
10780
- query_type = '${query_type}'
10876
+ query_type = os.environ.get('LOKI_STATE_QUERY_TYPE', '')
10877
+ agent_id = os.environ.get('LOKI_STATE_AGENT_ID', '') or None
10878
+ event_type = os.environ.get('LOKI_STATE_EVENT_TYPE', '') or None
10879
+ topic = os.environ.get('LOKI_STATE_TOPIC', '') or None
10880
+ cluster_id = os.environ.get('LOKI_STATE_CLUSTER_ID', '') or None
10881
+ migration_id = os.environ.get('LOKI_STATE_MIGRATION_ID', '') or None
10882
+ limit = int(os.environ.get('LOKI_STATE_LIMIT', '20'))
10883
+
10781
10884
  if query_type == 'events':
10782
10885
  results = db.query_events(
10783
- event_type='${event_type}' or None,
10784
- agent_id='${agent_id}' or None,
10785
- migration_id='${migration_id}' or None,
10786
- limit=int('${limit}')
10886
+ event_type=event_type,
10887
+ agent_id=agent_id,
10888
+ migration_id=migration_id,
10889
+ limit=limit
10787
10890
  )
10788
10891
  elif query_type == 'messages':
10789
10892
  results = db.query_messages(
10790
- topic='${topic}' or None,
10791
- cluster_id='${cluster_id}' or None,
10792
- limit=int('${limit}')
10893
+ topic=topic,
10894
+ cluster_id=cluster_id,
10895
+ limit=limit
10793
10896
  )
10794
10897
  elif query_type == 'checkpoints':
10795
10898
  results = db.query_checkpoints(
10796
- migration_id='${migration_id}' or None,
10797
- limit=int('${limit}')
10899
+ migration_id=migration_id,
10900
+ limit=limit
10798
10901
  )
10799
10902
  else:
10800
10903
  print(f'Unknown query type: {query_type}')
@@ -10807,7 +10910,8 @@ else:
10807
10910
  print(json.dumps(r, indent=2))
10808
10911
  print('---')
10809
10912
  print(f'{len(results)} result(s)')
10810
- " 2>&1 || {
10913
+ STATE_QUERY_PY
10914
+ 2>&1 || {
10811
10915
  echo -e "${RED}Error querying state database${NC}"
10812
10916
  return 1
10813
10917
  }
@@ -13961,14 +14065,17 @@ try {
13961
14065
  mkdir -p .loki
13962
14066
  local config_file=".loki/config.json"
13963
14067
  if [ -f "$config_file" ]; then
13964
- python3 -c "
13965
- import json
13966
- with open('$config_file') as f:
14068
+ LOKI_TELEM_CFG="$config_file" LOKI_TELEM_ENDPOINT="$endpoint" \
14069
+ python3 << 'TELEM_ENABLE_PY'
14070
+ import json, os
14071
+ cfg = os.environ['LOKI_TELEM_CFG']
14072
+ ep = os.environ['LOKI_TELEM_ENDPOINT']
14073
+ with open(cfg) as f:
13967
14074
  config = json.load(f)
13968
- config['otel_endpoint'] = '$endpoint'
13969
- with open('$config_file', 'w') as f:
14075
+ config['otel_endpoint'] = ep
14076
+ with open(cfg, 'w') as f:
13970
14077
  json.dump(config, f, indent=2)
13971
- " 2>/dev/null
14078
+ TELEM_ENABLE_PY
13972
14079
  else
13973
14080
  echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
13974
14081
  fi
@@ -13984,14 +14091,15 @@ with open('$config_file', 'w') as f:
13984
14091
 
13985
14092
  local config_file=".loki/config.json"
13986
14093
  if [ -f "$config_file" ]; then
13987
- python3 -c "
13988
- import json
13989
- with open('$config_file') as f:
14094
+ LOKI_TELEM_CFG="$config_file" python3 << 'TELEM_DISABLE_PY'
14095
+ import json, os
14096
+ cfg = os.environ['LOKI_TELEM_CFG']
14097
+ with open(cfg) as f:
13990
14098
  config = json.load(f)
13991
14099
  config.pop('otel_endpoint', None)
13992
- with open('$config_file', 'w') as f:
14100
+ with open(cfg, 'w') as f:
13993
14101
  json.dump(config, f, indent=2)
13994
- " 2>/dev/null
14102
+ TELEM_DISABLE_PY
13995
14103
  fi
13996
14104
 
13997
14105
  echo -e " Telemetry disabled"
@@ -14158,9 +14266,10 @@ cmd_remote() {
14158
14266
  prd_abs="$(cd "$(dirname "$prd_file")" && pwd)/$(basename "$prd_file")"
14159
14267
  fi
14160
14268
 
14161
- # Start dashboard in background if enabled
14269
+ # Start dashboard in background if enabled, with cleanup trap
14162
14270
  if [ "${LOKI_DASHBOARD:-true}" = "true" ]; then
14163
14271
  cmd_api start >/dev/null 2>&1 &
14272
+ trap 'cmd_api stop >/dev/null 2>&1 || true' EXIT INT TERM
14164
14273
  fi
14165
14274
 
14166
14275
  local version=$(get_version)
@@ -14759,9 +14868,9 @@ METRICS_SCRIPT
14759
14868
  fi
14760
14869
 
14761
14870
  echo "Uploading metrics report..."
14762
- local gist_desc="Loki Mode productivity report - ${project_name:-project} ($(date +%Y-%m-%d))"
14763
14871
  local project_name
14764
14872
  project_name=$(basename "$(pwd)")
14873
+ local gist_desc="Loki Mode productivity report - ${project_name:-project} ($(date +%Y-%m-%d))"
14765
14874
  local gist_url
14766
14875
  gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" --public 2>&1)
14767
14876
  local gist_exit=$?
@@ -16764,12 +16873,13 @@ except Exception:
16764
16873
  # Gate 2: Security Scan
16765
16874
  _ci_scan() {
16766
16875
  local pattern="$1" sev="$2" cat="$3" finding="$4" suggestion="$5"
16876
+ # Only scan added lines (starting with "+") to avoid flagging deleted code
16767
16877
  while IFS= read -r match; do
16768
16878
  [ -z "$match" ] && continue
16769
16879
  local ml
16770
16880
  ml=$(echo "$match" | cut -d: -f1)
16771
16881
  _ci_add_finding "diff" "$ml" "$sev" "$cat" "$finding" "$suggestion"
16772
- done < <(echo "$diff_content" | grep -nE "$pattern" 2>/dev/null || true)
16882
+ done < <(echo "$diff_content" | grep -n '^+' | grep -E "$pattern" 2>/dev/null || true)
16773
16883
  }
16774
16884
 
16775
16885
  # Hardcoded secrets
@@ -16830,6 +16940,7 @@ except Exception:
16830
16940
  # --- Test suggestions ---
16831
16941
  local test_suggestions=""
16832
16942
  if [ "$ci_test_suggest" = true ] && [ -n "$changed_files" ]; then
16943
+ export LOKI_CI_CHANGED_FILES="$changed_files"
16833
16944
  test_suggestions=$(python3 << 'TEST_SUGGEST_PY'
16834
16945
  import sys, os
16835
16946
 
@@ -16859,8 +16970,15 @@ for f in changed:
16859
16970
  suggestions.append({"file": f, "test_file": test_path, "alt_test_file": alt_path,
16860
16971
  "framework": "pytest", "hint": f"Add unit tests for {name} module"})
16861
16972
  elif ext in (".js", ".ts", ".jsx", ".tsx"):
16862
- test_ext = ext.replace(".ts", ".test.ts").replace(".js", ".test.js").replace(".tsx", ".test.tsx").replace(".jsx", ".test.jsx")
16863
- if test_ext == ext:
16973
+ if ext == ".tsx":
16974
+ test_ext = ".test.tsx"
16975
+ elif ext == ".jsx":
16976
+ test_ext = ".test.jsx"
16977
+ elif ext == ".ts":
16978
+ test_ext = ".test.ts"
16979
+ elif ext == ".js":
16980
+ test_ext = ".test.js"
16981
+ else:
16864
16982
  test_ext = f".test{ext}"
16865
16983
  test_path = os.path.join(dirpath, f"{name}{test_ext}")
16866
16984
  suggestions.append({"file": f, "test_file": test_path,
@@ -17034,6 +17152,7 @@ CI_JSON_OUT
17034
17152
  if [ -n "${test_suggestions:-}" ] && [ "$test_suggestions" != "[]" ]; then
17035
17153
  echo "### Test Suggestions"
17036
17154
  echo ""
17155
+ export LOKI_CI_TEST_SUGG="${test_suggestions}"
17037
17156
  python3 -c "
17038
17157
  import json, os
17039
17158
  tests = json.loads(os.environ.get('LOKI_CI_TEST_SUGG', '[]'))
@@ -18770,7 +18889,7 @@ cmd_share() {
18770
18889
  tmpfile=$(mktemp "/tmp/loki-share-XXXXXX.$ext")
18771
18890
 
18772
18891
  echo "Generating session report..."
18773
- if ! loki report --format "$format" > "$tmpfile" 2>/dev/null; then
18892
+ if ! "$0" report --format "$format" > "$tmpfile" 2>/dev/null; then
18774
18893
  echo -e "${RED}Failed to generate session report${NC}"
18775
18894
  rm -f "$tmpfile"
18776
18895
  exit 1