loki-mode 6.71.0 → 6.72.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 (91) hide show
  1. package/README.md +9 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/hooks/migration-hooks.sh +26 -0
  5. package/autonomy/loki +429 -92
  6. package/autonomy/run.sh +219 -38
  7. package/dashboard/__init__.py +1 -1
  8. package/dashboard/server.py +101 -19
  9. package/docs/INSTALLATION.md +20 -11
  10. package/docs/bug-fixes/agent-01-cli-fixes.md +101 -0
  11. package/docs/bug-fixes/agent-02-purplelab-fixes.md +88 -0
  12. package/docs/bug-fixes/agent-03-dashboard-fixes.md +119 -0
  13. package/docs/bug-fixes/agent-04-memory-fixes.md +105 -0
  14. package/docs/bug-fixes/agent-05-provider-fixes.md +86 -0
  15. package/docs/bug-fixes/agent-06-integration-fixes.md +101 -0
  16. package/docs/bug-fixes/agent-07-dash-run-fixes.md +101 -0
  17. package/docs/bug-fixes/agent-08-docker-fixes.md +164 -0
  18. package/docs/bug-fixes/agent-09-e2e-build-fixes.md +69 -0
  19. package/docs/bug-fixes/agent-10-e2e-fullstack-fixes.md +102 -0
  20. package/docs/bug-fixes/agent-11-e2e-session-fixes.md +70 -0
  21. package/docs/bug-fixes/agent-12-scenario-fixes.md +120 -0
  22. package/docs/bug-fixes/agent-13-enterprise-fixes.md +143 -0
  23. package/docs/bug-fixes/agent-14-uat-newuser-fixes.md +88 -0
  24. package/docs/bug-fixes/agent-15-uat-poweruser-fixes.md +132 -0
  25. package/docs/bug-fixes/agent-19-code-review.md +316 -0
  26. package/docs/bug-fixes/agent-20-architecture-review.md +331 -0
  27. package/docs/competitive/bolt-new-analysis.md +579 -0
  28. package/docs/competitive/emergence-others-analysis.md +605 -0
  29. package/docs/competitive/replit-lovable-analysis.md +622 -0
  30. package/docs/test-scenarios/edge-cases.md +813 -0
  31. package/docs/test-scenarios/enterprise-scenarios.md +732 -0
  32. package/mcp/__init__.py +1 -1
  33. package/mcp/server.py +49 -5
  34. package/memory/consolidation.py +33 -0
  35. package/memory/embeddings.py +10 -1
  36. package/memory/engine.py +83 -38
  37. package/memory/retrieval.py +36 -0
  38. package/memory/storage.py +56 -4
  39. package/memory/token_economics.py +14 -2
  40. package/memory/vector_index.py +36 -7
  41. package/package.json +1 -1
  42. package/providers/gemini.sh +89 -2
  43. package/templates/README.md +1 -1
  44. package/templates/cli-tool.md +30 -0
  45. package/templates/dashboard.md +4 -0
  46. package/templates/data-pipeline.md +4 -0
  47. package/templates/discord-bot.md +47 -0
  48. package/templates/game.md +4 -0
  49. package/templates/microservice.md +4 -0
  50. package/templates/npm-library.md +4 -0
  51. package/templates/rest-api-auth.md +50 -20
  52. package/templates/rest-api.md +15 -0
  53. package/templates/saas-starter.md +1 -1
  54. package/templates/slack-bot.md +36 -0
  55. package/templates/static-landing-page.md +9 -1
  56. package/templates/web-scraper.md +4 -0
  57. package/web-app/dist/assets/Badge-CeBkFjo6.js +1 -0
  58. package/web-app/dist/assets/Button-yuhqo8Fq.js +1 -0
  59. package/web-app/dist/assets/{Card-BMw7NSaV.js → Card-BG17vsX0.js} +1 -1
  60. package/web-app/dist/assets/{HomePage-QyvNpyFv.js → HomePage-BMSQ7Apj.js} +3 -3
  61. package/web-app/dist/assets/{LoginPage-CG_DkANw.js → LoginPage-aH_6iolg.js} +1 -1
  62. package/web-app/dist/assets/{NotFoundPage-CHBJTLTi.js → NotFoundPage-Di8cNtB1.js} +1 -1
  63. package/web-app/dist/assets/ProjectPage-BtRssmw9.js +285 -0
  64. package/web-app/dist/assets/ProjectsPage-B-FTFagc.js +6 -0
  65. package/web-app/dist/assets/{SettingsPage-Dq-c6kXj.js → SettingsPage-DIJPBla4.js} +1 -1
  66. package/web-app/dist/assets/TeamsPage--19fNX7w.js +36 -0
  67. package/web-app/dist/assets/TemplatesPage-ChUQNOOv.js +11 -0
  68. package/web-app/dist/assets/TerminalOutput-Dwrzecyl.js +31 -0
  69. package/web-app/dist/assets/activity-BNRWeu9N.js +6 -0
  70. package/web-app/dist/assets/{arrow-left-Dw9yRwL8.js → arrow-left-Ce6g1_YE.js} +1 -1
  71. package/web-app/dist/assets/circle-alert-LIndawHL.js +11 -0
  72. package/web-app/dist/assets/clock-Bpj4VPlP.js +6 -0
  73. package/web-app/dist/assets/{external-link-DGtaQZrg.js → external-link-BhhdF0iQ.js} +1 -1
  74. package/web-app/dist/assets/folder-open-CM2LgfxI.js +11 -0
  75. package/web-app/dist/assets/index-8-KpWWq7.css +1 -0
  76. package/web-app/dist/assets/index-kPDW4e_b.js +236 -0
  77. package/web-app/dist/assets/lock-sAk3Xe54.js +16 -0
  78. package/web-app/dist/assets/search-CR-2i9by.js +6 -0
  79. package/web-app/dist/assets/server-DuFh4ymA.js +26 -0
  80. package/web-app/dist/assets/trash-2-BmkkT8V_.js +11 -0
  81. package/web-app/dist/index.html +2 -2
  82. package/web-app/server.py +1345 -55
  83. package/web-app/dist/assets/Badge-BFLpnFZM.js +0 -6
  84. package/web-app/dist/assets/Button-BYY9clv_.js +0 -16
  85. package/web-app/dist/assets/ProjectPage-q65bhy76.js +0 -217
  86. package/web-app/dist/assets/ProjectsPage-d4mY9ewI.js +0 -21
  87. package/web-app/dist/assets/TemplatesPage-BEpY-p-Q.js +0 -1
  88. package/web-app/dist/assets/TerminalOutput-CFy7MnPO.js +0 -51
  89. package/web-app/dist/assets/clock-D4pcK_Eq.js +0 -11
  90. package/web-app/dist/assets/index-BnNomb7B.js +0 -196
  91. package/web-app/dist/assets/index-D452pFGl.css +0 -1
package/autonomy/loki CHANGED
@@ -126,6 +126,7 @@ SANDBOX_SH="$SKILL_DIR/autonomy/sandbox.sh"
126
126
  EMIT_SH="$SKILL_DIR/events/emit.sh"
127
127
  LEARNING_EMIT_SH="$SKILL_DIR/learning/emit.sh"
128
128
  LOKI_DIR=".loki"
129
+ export LOKI_DIR
129
130
 
130
131
  # Anonymous usage telemetry
131
132
  PROJECT_DIR="$SKILL_DIR"
@@ -400,7 +401,7 @@ show_help() {
400
401
  echo " quick \"task\" Quick single-task mode (lightweight, 3 iterations max)"
401
402
  echo " monitor [path] Monitor Docker Compose services with auto-fix (v6.67.0)"
402
403
  echo " demo Run interactive demo (~60s simulated session)"
403
- echo " init [name] Project scaffolding with 22 PRD templates"
404
+ echo " init [name] Project scaffolding with 21 PRD templates"
404
405
  echo " issue <url|num> [DEPRECATED] Use 'loki run' instead"
405
406
  echo " watch [prd] Auto-rerun on PRD file changes (v6.33.0)"
406
407
  echo " export <format> Export session data: json|markdown|csv|timeline (v6.0.0)"
@@ -1092,6 +1093,24 @@ cmd_start() {
1092
1093
  --outcome success \
1093
1094
  --context "{\"provider\":\"$effective_provider\",\"prd_path\":\"${prd_file:-}\"}"
1094
1095
 
1096
+ # Pre-flight: check that the provider CLI is installed
1097
+ if ! command -v "$effective_provider" &>/dev/null; then
1098
+ echo -e "${RED}Error: Provider CLI '$effective_provider' is not installed.${NC}"
1099
+ echo ""
1100
+ echo "Install it first:"
1101
+ case "$effective_provider" in
1102
+ claude) echo " npm install -g @anthropic-ai/claude-code" ;;
1103
+ codex) echo " npm install -g @openai/codex" ;;
1104
+ gemini) echo " npm install -g @google/gemini-cli" ;;
1105
+ cline) echo " npm install -g @anthropic-ai/cline" ;;
1106
+ aider) echo " pip install aider-chat" ;;
1107
+ *) echo " Check the provider documentation for installation." ;;
1108
+ esac
1109
+ echo ""
1110
+ echo "Check your environment: loki doctor"
1111
+ exit 1
1112
+ fi
1113
+
1095
1114
  exec "$RUN_SH" "${args[@]}"
1096
1115
  }
1097
1116
 
@@ -1350,18 +1369,22 @@ cmd_stop() {
1350
1369
  pkill -9 -f "loki-run-" 2>/dev/null || true
1351
1370
 
1352
1371
  # Mark session.json as stopped (skill-invoked sessions)
1372
+ # BUG-ST-008: Atomic session.json update via temp file + mv (matches run.sh)
1353
1373
  if [ -f "$LOKI_DIR/session.json" ]; then
1354
- python3 -c "
1355
- import json, sys, os
1374
+ _LOKI_SESSION_FILE="$LOKI_DIR/session.json" python3 -c "
1375
+ import json, os, tempfile
1376
+ sf = os.environ['_LOKI_SESSION_FILE']
1356
1377
  try:
1357
- p = os.path.join(sys.argv[1], 'session.json')
1358
- with open(p, 'r+') as f:
1378
+ with open(sf) as f:
1359
1379
  d = json.load(f)
1360
- d['status'] = 'stopped'
1361
- f.seek(0); f.truncate()
1380
+ d['status'] = 'stopped'
1381
+ sd = os.path.dirname(sf)
1382
+ fd, tmp = tempfile.mkstemp(dir=sd, suffix='.json')
1383
+ with os.fdopen(fd, 'w') as f:
1362
1384
  json.dump(d, f)
1363
- except: pass
1364
- " "$LOKI_DIR" 2>/dev/null || true
1385
+ os.replace(tmp, sf)
1386
+ except (json.JSONDecodeError, OSError): pass
1387
+ " 2>/dev/null || true
1365
1388
  fi
1366
1389
 
1367
1390
  # Clean up control files
@@ -1738,12 +1761,15 @@ cmd_status() {
1738
1761
  # Context window usage (token tracking)
1739
1762
  if [ -f "$LOKI_DIR/state/context-usage.json" ]; then
1740
1763
  local ctx_used ctx_total
1741
- ctx_total=$(python3 -c "import json; d=json.load(open('$LOKI_DIR/state/context-usage.json')); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
1742
- ctx_used=$(python3 -c "import json; d=json.load(open('$LOKI_DIR/state/context-usage.json')); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
1764
+ ctx_total=$(_LOKI_CTX_FILE="$LOKI_DIR/state/context-usage.json" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
1765
+ ctx_used=$(_LOKI_CTX_FILE="$LOKI_DIR/state/context-usage.json" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
1743
1766
  if type context_gauge &>/dev/null; then
1744
1767
  context_gauge "$ctx_used" "$ctx_total" "Context"
1745
1768
  else
1746
- local ctx_pct=$((ctx_used * 100 / ctx_total))
1769
+ local ctx_pct=0
1770
+ if [ "$ctx_total" -gt 0 ] 2>/dev/null; then
1771
+ ctx_pct=$((ctx_used * 100 / ctx_total))
1772
+ fi
1747
1773
  echo -e "${CYAN}Context:${NC} ${ctx_pct}% (${ctx_used} / ${ctx_total} tokens)"
1748
1774
  fi
1749
1775
  fi
@@ -3151,6 +3177,22 @@ cmd_web() {
3151
3177
  status)
3152
3178
  cmd_web_status
3153
3179
  ;;
3180
+ logs)
3181
+ shift || true
3182
+ local log_lines="${1:-100}"
3183
+ local log_file="${PURPLE_LAB_STATE_DIR}/logs/purple-lab.log"
3184
+ if [ ! -f "$log_file" ]; then
3185
+ log_file="${LOKI_DIR}/purple-lab/logs/purple-lab.log"
3186
+ fi
3187
+ if [ ! -f "$log_file" ]; then
3188
+ echo -e "${YELLOW}No Purple Lab log file found${NC}"
3189
+ return 0
3190
+ fi
3191
+ echo -e "${BOLD}Purple Lab Logs (last $log_lines lines)${NC}"
3192
+ echo -e "${DIM}Use 'loki web logs <N>' to show more/fewer lines${NC}"
3193
+ echo ""
3194
+ tail -n "$log_lines" "$log_file"
3195
+ ;;
3154
3196
  --help|-h|help)
3155
3197
  cmd_web_help
3156
3198
  ;;
@@ -3171,6 +3213,7 @@ cmd_web_help() {
3171
3213
  echo " start Start Purple Lab (default)"
3172
3214
  echo " stop Stop Purple Lab server"
3173
3215
  echo " status Show Purple Lab server status"
3216
+ echo " logs Show Purple Lab server logs"
3174
3217
  echo " help Show this help"
3175
3218
  echo ""
3176
3219
  echo "Options (for start):"
@@ -3335,8 +3378,10 @@ cmd_web_start() {
3335
3378
 
3336
3379
  # Wait for server to be ready
3337
3380
  local retries=0
3381
+ local server_ready=false
3338
3382
  while [ $retries -lt 15 ]; do
3339
3383
  if curl -s "http://${PURPLE_LAB_DEFAULT_HOST}:${port}/api/session/status" > /dev/null 2>&1; then
3384
+ server_ready=true
3340
3385
  break
3341
3386
  fi
3342
3387
  sleep 0.5
@@ -3351,21 +3396,33 @@ cmd_web_start() {
3351
3396
  fi
3352
3397
 
3353
3398
  local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}"
3354
- echo -e "${GREEN}Purple Lab running at: $url${NC} (PID: $pid)"
3399
+
3400
+ if [ "$server_ready" = true ]; then
3401
+ echo -e "${GREEN}Purple Lab running at: $url${NC} (PID: $pid)"
3402
+ else
3403
+ echo -e "${YELLOW}Purple Lab starting at: $url${NC} (PID: $pid)"
3404
+ echo "Server may still be loading. Refresh the browser if it does not load immediately."
3405
+ fi
3355
3406
  echo -e "Logs: $log_file"
3356
3407
  echo -e "Stop with: ${CYAN}loki web stop${NC}"
3357
3408
 
3358
- # Open browser
3409
+ # Open browser only after server is confirmed ready
3359
3410
  if [ "$open_browser" = true ]; then
3360
- echo ""
3361
- if command -v open &> /dev/null; then
3362
- open "$url"
3363
- elif command -v xdg-open &> /dev/null; then
3364
- xdg-open "$url"
3365
- elif command -v start &> /dev/null; then
3366
- start "$url"
3411
+ if [ "$server_ready" = true ]; then
3412
+ echo ""
3413
+ if command -v open &> /dev/null; then
3414
+ open "$url"
3415
+ elif command -v xdg-open &> /dev/null; then
3416
+ xdg-open "$url"
3417
+ elif command -v start &> /dev/null; then
3418
+ start "$url"
3419
+ else
3420
+ echo "Please open in browser: $url"
3421
+ fi
3367
3422
  else
3368
- echo "Please open in browser: $url"
3423
+ echo ""
3424
+ echo -e "${YELLOW}Browser not opened because server is not ready yet.${NC}"
3425
+ echo "Open manually when ready: $url"
3369
3426
  fi
3370
3427
  fi
3371
3428
  }
@@ -3388,13 +3445,20 @@ cmd_web_stop() {
3388
3445
  local pid
3389
3446
  pid=$(cat "$pid_file" 2>/dev/null)
3390
3447
  if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
3391
- kill "$pid" 2>/dev/null
3392
- sleep 1
3393
- if kill -0 "$pid" 2>/dev/null; then
3394
- kill -9 "$pid" 2>/dev/null
3448
+ # Verify the PID belongs to a Purple Lab/Python process (PID recycling guard)
3449
+ local pid_cmd
3450
+ pid_cmd=$(ps -p "$pid" -o comm= 2>/dev/null || true)
3451
+ if [[ "$pid_cmd" == *python* ]] || [[ "$pid_cmd" == *uvicorn* ]] || [[ "$pid_cmd" == *Purple* ]]; then
3452
+ kill "$pid" 2>/dev/null
3453
+ sleep 1
3454
+ if kill -0 "$pid" 2>/dev/null; then
3455
+ kill -9 "$pid" 2>/dev/null
3456
+ fi
3457
+ echo -e "${GREEN}Purple Lab stopped (PID: $pid)${NC}"
3458
+ stopped=true
3459
+ else
3460
+ echo -e "${YELLOW}PID $pid is not a Purple Lab process (found: $pid_cmd), skipping${NC}"
3395
3461
  fi
3396
- echo -e "${GREEN}Purple Lab stopped (PID: $pid)${NC}"
3397
- stopped=true
3398
3462
  fi
3399
3463
  rm -f "$PURPLE_LAB_PID_FILE"
3400
3464
  fi
@@ -3513,7 +3577,13 @@ cmd_web_stop() {
3513
3577
 
3514
3578
  cmd_web_status() {
3515
3579
  local port
3516
- port=$(cat "${LOKI_DIR}/purple-lab/port" 2>/dev/null || echo "$PURPLE_LAB_DEFAULT_PORT")
3580
+ port=$(cat "${PURPLE_LAB_STATE_DIR}/port" 2>/dev/null || cat "${LOKI_DIR}/purple-lab/port" 2>/dev/null || echo "$PURPLE_LAB_DEFAULT_PORT")
3581
+
3582
+ # Resolve log file path (check home-based state dir first, then CWD-based)
3583
+ local log_path="${PURPLE_LAB_STATE_DIR}/logs/purple-lab.log"
3584
+ if [ ! -f "$log_path" ]; then
3585
+ log_path="${LOKI_DIR}/purple-lab/logs/purple-lab.log"
3586
+ fi
3517
3587
 
3518
3588
  # Check PID file
3519
3589
  if [ -f "$PURPLE_LAB_PID_FILE" ]; then
@@ -3523,7 +3593,7 @@ cmd_web_status() {
3523
3593
  echo -e "${GREEN}Purple Lab is running${NC}"
3524
3594
  echo " PID: $pid"
3525
3595
  echo " URL: http://${PURPLE_LAB_DEFAULT_HOST}:${port}"
3526
- echo " Logs: ${LOKI_DIR}/purple-lab/logs/purple-lab.log"
3596
+ echo " Logs: $log_path"
3527
3597
  return 0
3528
3598
  fi
3529
3599
  rm -f "$PURPLE_LAB_PID_FILE"
@@ -5031,6 +5101,22 @@ cmd_export() {
5031
5101
  esac
5032
5102
  }
5033
5103
 
5104
+ # Guard: check if output file exists and warn before overwriting
5105
+ _export_check_overwrite() {
5106
+ local output="$1"
5107
+ if [ -n "$output" ] && [ -f "$output" ]; then
5108
+ echo -e "${YELLOW}Warning: File already exists: $output${NC}"
5109
+ echo -n "Overwrite? [y/N] "
5110
+ local reply
5111
+ read -r reply
5112
+ case "$reply" in
5113
+ [yY]|[yY][eE][sS]) return 0 ;;
5114
+ *) echo "Export cancelled."; return 1 ;;
5115
+ esac
5116
+ fi
5117
+ return 0
5118
+ }
5119
+
5034
5120
  _export_json() {
5035
5121
  local output="$1"
5036
5122
 
@@ -5102,6 +5188,7 @@ EXPORT_JSON
5102
5188
  echo -e "${RED}Error: Output path must not contain '..'${NC}"
5103
5189
  return 1
5104
5190
  fi
5191
+ _export_check_overwrite "$output" || return 0
5105
5192
  echo "$json_output" > "$output"
5106
5193
  echo -e "${GREEN}Exported to $output${NC}"
5107
5194
  else
@@ -5149,6 +5236,7 @@ MDEOF
5149
5236
  echo -e "${RED}Error: Output path must not contain '..'${NC}"
5150
5237
  return 1
5151
5238
  fi
5239
+ _export_check_overwrite "$output" || return 0
5152
5240
  echo "$md_output" > "$output"
5153
5241
  echo -e "${GREEN}Exported to $output${NC}"
5154
5242
  else
@@ -5199,6 +5287,7 @@ EXPORT_CSV
5199
5287
  echo -e "${RED}Error: Output path must not contain '..'${NC}"
5200
5288
  return 1
5201
5289
  fi
5290
+ _export_check_overwrite "$output" || return 0
5202
5291
  echo "$csv_output" > "$output"
5203
5292
  echo -e "${GREEN}Exported to $output${NC}"
5204
5293
  else
@@ -5252,6 +5341,7 @@ EXPORT_TIMELINE
5252
5341
  echo -e "${RED}Error: Output path must not contain '..'${NC}"
5253
5342
  return 1
5254
5343
  fi
5344
+ _export_check_overwrite "$output" || return 0
5255
5345
  echo "$timeline_output" > "$output"
5256
5346
  echo -e "${GREEN}Exported to $output${NC}"
5257
5347
  else
@@ -5399,7 +5489,15 @@ cmd_config_set() {
5399
5489
  fi
5400
5490
  ;;
5401
5491
  *)
5402
- echo -e "${YELLOW}Warning: Unknown key '$key' - storing anyway${NC}"
5492
+ echo -e "${RED}Unknown configuration key: '$key'${NC}"
5493
+ echo ""
5494
+ echo "Valid keys: maxTier, provider, issue.provider, blind_validation,"
5495
+ echo " adversarial_testing, spawn_timeout, spawn_retries, budget,"
5496
+ echo " model.planning, model.development, model.fast,"
5497
+ echo " notify.slack, notify.discord"
5498
+ echo ""
5499
+ echo "Run 'loki config' for details on each key."
5500
+ return 1
5403
5501
  ;;
5404
5502
  esac
5405
5503
 
@@ -5470,7 +5568,8 @@ cmd_config_get() {
5470
5568
  return 0
5471
5569
  fi
5472
5570
 
5473
- LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" \
5571
+ local result
5572
+ result=$(LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" \
5474
5573
  python3 << 'GET_CONFIG'
5475
5574
  import json, os
5476
5575
  cfg_file = os.environ["LOKI_CFG_FILE"]
@@ -5490,7 +5589,11 @@ for part in parts:
5490
5589
 
5491
5590
  print(current if not isinstance(current, dict) else json.dumps(current, indent=2))
5492
5591
  GET_CONFIG
5493
- unset LOKI_CFG_FILE LOKI_CFG_KEY
5592
+ ) 2>/dev/null || {
5593
+ echo -e "${RED}Error reading config key '$key'${NC}"
5594
+ return 1
5595
+ }
5596
+ echo "$result"
5494
5597
  }
5495
5598
 
5496
5599
  cmd_config_show() {
@@ -5873,6 +5976,40 @@ cmd_doctor() {
5873
5976
  doctor_check "Gemini CLI" gemini optional || true
5874
5977
  doctor_check "Cline CLI" cline optional || true
5875
5978
  doctor_check "Aider CLI" aider optional || true
5979
+
5980
+ # Check if at least one provider is installed
5981
+ local _any_provider=false
5982
+ for _dp in claude codex gemini cline aider; do
5983
+ command -v "$_dp" &>/dev/null && _any_provider=true && break
5984
+ done
5985
+ if ! $_any_provider; then
5986
+ echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
5987
+ echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
5988
+ fail_count=$((fail_count + 1))
5989
+ fi
5990
+ echo ""
5991
+
5992
+ echo -e "${CYAN}API Keys:${NC}"
5993
+ # Note: CLI tools use their own login sessions outside Docker/K8s.
5994
+ # This section helps first-time users verify they can authenticate.
5995
+ if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
5996
+ echo -e " ${GREEN}PASS${NC} ANTHROPIC_API_KEY is set"
5997
+ pass_count=$((pass_count + 1))
5998
+ elif command -v claude &>/dev/null; then
5999
+ echo -e " ${DIM} -- ${NC} ANTHROPIC_API_KEY not set (Claude CLI uses its own login)"
6000
+ fi
6001
+ if [ -n "${OPENAI_API_KEY:-}" ]; then
6002
+ echo -e " ${GREEN}PASS${NC} OPENAI_API_KEY is set"
6003
+ pass_count=$((pass_count + 1))
6004
+ elif command -v codex &>/dev/null; then
6005
+ echo -e " ${DIM} -- ${NC} OPENAI_API_KEY not set (Codex CLI uses its own login)"
6006
+ fi
6007
+ if [ -n "${GOOGLE_API_KEY:-${GEMINI_API_KEY:-}}" ]; then
6008
+ echo -e " ${GREEN}PASS${NC} GOOGLE_API_KEY is set"
6009
+ pass_count=$((pass_count + 1))
6010
+ elif command -v gemini &>/dev/null; then
6011
+ echo -e " ${DIM} -- ${NC} GOOGLE_API_KEY not set (Gemini CLI uses its own login)"
6012
+ fi
5876
6013
  echo ""
5877
6014
 
5878
6015
  echo -e "${CYAN}Skills:${NC}"
@@ -6187,7 +6324,37 @@ for name, info in result.items():
6187
6324
 
6188
6325
  # Show recent logs
6189
6326
  cmd_logs() {
6190
- local lines="${1:-50}"
6327
+ local lines="50"
6328
+ local follow=false
6329
+
6330
+ # Parse arguments
6331
+ while [[ $# -gt 0 ]]; do
6332
+ case "$1" in
6333
+ --tail|-n) lines="$2"; shift 2 ;;
6334
+ --tail=*) lines="${1#--tail=}"; shift ;;
6335
+ -n=*) lines="${1#-n=}"; shift ;;
6336
+ --follow|-f) follow=true; shift ;;
6337
+ --all|-a) lines="+1"; shift ;;
6338
+ --help|-h)
6339
+ echo "Usage: loki logs [options]"
6340
+ echo ""
6341
+ echo "Options:"
6342
+ echo " --tail, -n N Show last N lines (default: 50)"
6343
+ echo " --all, -a Show all lines"
6344
+ echo " --follow, -f Follow log output (like tail -f)"
6345
+ echo " --help, -h Show this help"
6346
+ return 0
6347
+ ;;
6348
+ *)
6349
+ # Legacy: treat plain number as line count
6350
+ if [[ "$1" =~ ^[0-9]+$ ]]; then
6351
+ lines="$1"
6352
+ fi
6353
+ shift
6354
+ ;;
6355
+ esac
6356
+ done
6357
+
6191
6358
  local log_file="$LOKI_DIR/logs/session.log"
6192
6359
 
6193
6360
  if [ ! -f "$log_file" ]; then
@@ -6195,9 +6362,16 @@ cmd_logs() {
6195
6362
  exit 0
6196
6363
  fi
6197
6364
 
6198
- echo -e "${BOLD}Recent Logs (last $lines lines)${NC}"
6199
- echo ""
6200
- tail -n "$lines" "$log_file"
6365
+ if [ "$follow" = true ]; then
6366
+ echo -e "${BOLD}Following logs (Ctrl+C to stop)${NC}"
6367
+ echo ""
6368
+ tail -f "$log_file"
6369
+ else
6370
+ echo -e "${BOLD}Recent Logs (last $lines lines)${NC}"
6371
+ echo -e "${DIM}Use --all to show all lines, --follow to stream${NC}"
6372
+ echo ""
6373
+ tail -n "$lines" "$log_file"
6374
+ fi
6201
6375
  }
6202
6376
 
6203
6377
  # API server management (delegates to unified FastAPI dashboard server)
@@ -6997,8 +7171,10 @@ cmd_quick() {
6997
7171
  echo ""
6998
7172
 
6999
7173
  # Create quick PRD from task description
7174
+ # BUG-PU-005: Use unique filename to prevent race conditions when
7175
+ # multiple simultaneous `loki quick` commands run in the same project
7000
7176
  mkdir -p "$LOKI_DIR"
7001
- local quick_prd="$LOKI_DIR/quick-prd.md"
7177
+ local quick_prd="$LOKI_DIR/quick-prd-$$.md"
7002
7178
  cat > "$quick_prd" << QPRDEOF
7003
7179
  # Quick Task
7004
7180
 
@@ -7046,6 +7222,25 @@ QPRDEOF
7046
7222
  # Emit event
7047
7223
  emit_event session cli quick_start "task=$(echo "$task_desc" | head -c 100)"
7048
7224
 
7225
+ # Pre-flight: check that the provider CLI is installed
7226
+ local _quick_provider="${LOKI_PROVIDER:-claude}"
7227
+ if ! command -v "$_quick_provider" &>/dev/null; then
7228
+ echo -e "${RED}Error: Provider CLI '$_quick_provider' is not installed.${NC}"
7229
+ echo ""
7230
+ echo "Install your AI provider CLI first:"
7231
+ case "$_quick_provider" in
7232
+ claude) echo " npm install -g @anthropic-ai/claude-code" ;;
7233
+ codex) echo " npm install -g @openai/codex" ;;
7234
+ gemini) echo " npm install -g @google/gemini-cli" ;;
7235
+ cline) echo " npm install -g @anthropic-ai/cline" ;;
7236
+ aider) echo " pip install aider-chat" ;;
7237
+ *) echo " Check the provider documentation for installation." ;;
7238
+ esac
7239
+ echo ""
7240
+ echo "Then verify: loki doctor"
7241
+ exit 1
7242
+ fi
7243
+
7049
7244
  # Run the orchestrator with quick settings
7050
7245
  exec "$RUN_SH" "$quick_prd"
7051
7246
  }
@@ -7364,7 +7559,7 @@ cmd_init() {
7364
7559
  local list_mode=false
7365
7560
  local json_mode=false
7366
7561
 
7367
- # 22 built-in template names (order: simple, standard, complex)
7562
+ # 21 built-in template names (order: simple, standard, complex)
7368
7563
  local TEMPLATE_NAMES=(
7369
7564
  simple-todo-app
7370
7565
  static-landing-page
@@ -7384,7 +7579,6 @@ cmd_init() {
7384
7579
  npm-library
7385
7580
  microservice
7386
7581
  mobile-app
7387
- saas-app
7388
7582
  saas-starter
7389
7583
  e-commerce
7390
7584
  ai-chatbot
@@ -7411,7 +7605,6 @@ cmd_init() {
7411
7605
  npm-library) echo "npm Library" ;;
7412
7606
  microservice) echo "Microservice" ;;
7413
7607
  mobile-app) echo "Mobile App" ;;
7414
- saas-app) echo "SaaS Application" ;;
7415
7608
  saas-starter) echo "SaaS Starter Kit" ;;
7416
7609
  e-commerce) echo "E-Commerce Store" ;;
7417
7610
  ai-chatbot) echo "AI Chatbot (RAG)" ;;
@@ -7469,7 +7662,7 @@ cmd_init() {
7469
7662
  echo " .gitignore Git ignore rules (with git init)"
7470
7663
  echo ""
7471
7664
  echo "Options:"
7472
- echo " --template, -t TYPE Template name (e.g., saas-app, cli-tool)"
7665
+ echo " --template, -t TYPE Template name (e.g., saas-starter, cli-tool)"
7473
7666
  echo " --no-git Skip git init"
7474
7667
  echo " --stdout Print PRD to stdout instead of writing files"
7475
7668
  echo " --list List all available templates"
@@ -7478,7 +7671,7 @@ cmd_init() {
7478
7671
  echo " --help, -h Show this help"
7479
7672
  echo ""
7480
7673
  echo "Examples:"
7481
- echo " loki init my-saas --template saas-app Create my-saas/ with SaaS PRD"
7674
+ echo " loki init my-saas --template saas-starter Create my-saas/ with SaaS PRD"
7482
7675
  echo " loki init --template cli-tool Scaffold current dir with CLI PRD"
7483
7676
  echo " loki init my-app Create my-app/ (prompts for template)"
7484
7677
  echo " loki init --list Show all $template_count templates"
@@ -7748,11 +7941,17 @@ ENDGITIGNORE
7748
7941
  echo -e "${RED}Directory already exists: $target_dir${NC}"
7749
7942
  exit 1
7750
7943
  fi
7751
- mkdir -p "$target_dir"
7944
+ if ! mkdir -p "$target_dir"; then
7945
+ echo -e "${RED}Failed to create directory: $target_dir${NC}"
7946
+ exit 1
7947
+ fi
7752
7948
  fi
7753
7949
 
7754
7950
  # Create .loki config directory
7755
- mkdir -p "$target_dir/.loki"
7951
+ if ! mkdir -p "$target_dir/.loki"; then
7952
+ echo -e "${RED}Failed to create .loki directory in: $target_dir${NC}"
7953
+ exit 1
7954
+ fi
7756
7955
 
7757
7956
  # Write files
7758
7957
  echo "$prd_content" > "$target_dir/prd.md"
@@ -7799,6 +7998,25 @@ ENDGITIGNORE
7799
7998
  echo -e " 1. Review and edit: ${BOLD}prd.md${NC}"
7800
7999
  echo -e " 2. Run: ${BOLD}loki start prd.md${NC}"
7801
8000
  fi
8001
+
8002
+ # Check if an AI provider CLI is available
8003
+ local _has_provider=false
8004
+ for _pcli in claude codex gemini cline aider; do
8005
+ if command -v "$_pcli" &>/dev/null; then
8006
+ _has_provider=true
8007
+ break
8008
+ fi
8009
+ done
8010
+ if ! $_has_provider; then
8011
+ echo ""
8012
+ echo -e "${YELLOW}Note: No AI provider CLI detected.${NC}"
8013
+ echo " Install at least one before running 'loki start':"
8014
+ echo " npm install -g @anthropic-ai/claude-code (recommended)"
8015
+ echo " npm install -g @openai/codex"
8016
+ echo " npm install -g @google/gemini-cli"
8017
+ echo ""
8018
+ echo " Then verify your setup: ${BOLD}loki doctor${NC}"
8019
+ fi
7802
8020
  }
7803
8021
 
7804
8022
  # Dogfooding statistics
@@ -9166,8 +9384,8 @@ for f in data.get('frictions', []):
9166
9384
  fi
9167
9385
  echo -e "${BOLD}Healing Report${NC}"
9168
9386
  echo ""
9169
- echo " Friction map: $(python3 -c "import json; print(len(json.load(open('$heal_dir/friction-map.json')).get('frictions', [])))" 2>/dev/null || echo '0') points"
9170
- echo " Failure modes: $(python3 -c "import json; print(len(json.load(open('$heal_dir/failure-modes.json')).get('modes', [])))" 2>/dev/null || echo '0') cataloged"
9387
+ echo " Friction map: $(_HEAL_FILE="$heal_dir/friction-map.json" python3 -c "import json, os; print(len(json.load(open(os.environ['_HEAL_FILE'])).get('frictions', [])))" 2>/dev/null || echo '0') points"
9388
+ echo " Failure modes: $(_HEAL_FILE="$heal_dir/failure-modes.json" python3 -c "import json, os; print(len(json.load(open(os.environ['_HEAL_FILE'])).get('modes', [])))" 2>/dev/null || echo '0') cataloged"
9171
9389
  echo " Institutional knowledge: $(wc -l < "$heal_dir/institutional-knowledge.md" 2>/dev/null || echo '0') lines"
9172
9390
  echo " Characterization tests: $(find "$heal_dir/characterization-tests/" -name "*.json" 2>/dev/null | wc -l | tr -d ' ') tests"
9173
9391
  echo ""
@@ -9200,6 +9418,21 @@ for f in data.get('frictions', []):
9200
9418
  local heal_dir="$codebase_path/.loki/healing"
9201
9419
  mkdir -p "$heal_dir"/{behavioral-baseline,characterization-tests}
9202
9420
 
9421
+ # BUG-HEAL-004: Source migration hooks for healing enforcement
9422
+ local hooks_file
9423
+ hooks_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hooks/migration-hooks.sh"
9424
+ if [[ -f "$hooks_file" ]]; then
9425
+ # shellcheck source=hooks/migration-hooks.sh
9426
+ source "$hooks_file"
9427
+ load_migration_hook_config "$codebase_path"
9428
+ fi
9429
+
9430
+ # Export healing environment variables for hooks
9431
+ export LOKI_HEAL_MODE="true"
9432
+ export LOKI_HEAL_PHASE="$phase"
9433
+ export LOKI_HEAL_STRICT="$strict"
9434
+ export LOKI_CODEBASE_PATH="$codebase_path"
9435
+
9203
9436
  # Initialize healing state files if they don't exist
9204
9437
  [[ ! -f "$heal_dir/friction-map.json" ]] && echo '{"frictions":[]}' > "$heal_dir/friction-map.json"
9205
9438
  [[ ! -f "$heal_dir/failure-modes.json" ]] && echo '{"modes":[]}' > "$heal_dir/failure-modes.json"
@@ -9223,6 +9456,20 @@ with open('$heal_dir/healing-progress.json', 'w') as f:
9223
9456
  " || true
9224
9457
  fi
9225
9458
 
9459
+ # BUG-HEAL-004: Validate phase gate when resuming from a previous phase
9460
+ if [ "$do_resume" = "true" ] && [[ -f "$heal_dir/healing-progress.json" ]] && type hook_healing_phase_gate &>/dev/null; then
9461
+ local prev_phase
9462
+ prev_phase=$(python3 -c "import json; print(json.load(open('$heal_dir/healing-progress.json')).get('current_phase', 'archaeology'))" 2>/dev/null || echo "archaeology")
9463
+ if [[ "$prev_phase" != "$phase" ]]; then
9464
+ local gate_result
9465
+ if ! gate_result=$(hook_healing_phase_gate "$prev_phase" "$phase" 2>&1); then
9466
+ echo -e "${RED}Phase gate check failed:${NC}"
9467
+ echo " $gate_result"
9468
+ return 1
9469
+ fi
9470
+ fi
9471
+ fi
9472
+
9226
9473
  emit_event healing cli start "phase=$phase" "codebase=$codebase_path" "strict=$strict" 2>/dev/null || true
9227
9474
 
9228
9475
  echo -e "${BOLD}Legacy System Healing${NC}"
@@ -9329,6 +9576,12 @@ except Exception: pass
9329
9576
  # shellcheck disable=SC2086
9330
9577
  (cd "$codebase_path" && aider --message "$heal_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || heal_exit=$?
9331
9578
  ;;
9579
+ # BUG-HEAL-003: Unknown provider should error, not silently succeed
9580
+ *)
9581
+ echo -e "${RED}Error: Unknown provider: $provider${NC}"
9582
+ echo "Supported providers: claude, codex, gemini, cline, aider"
9583
+ return 1
9584
+ ;;
9332
9585
  esac
9333
9586
 
9334
9587
  emit_event healing cli complete "phase=$phase" "exit=$heal_exit" 2>/dev/null || true
@@ -9852,7 +10105,7 @@ print(json.loads(p.read_text()).get('port', 7373) if p.exists() else 7373)
9852
10105
  local sched_file=".loki/triggers/schedules.json"
9853
10106
  if [[ -f "$sched_file" ]]; then
9854
10107
  local count
9855
- count=$(python3 -c "import json; d=json.load(open('$sched_file')); print(len(d) if isinstance(d,list) else len(d.get('schedules',[])))" 2>/dev/null || echo "?")
10108
+ count=$(_SCHED_FILE="$sched_file" python3 -c "import json, os; d=json.load(open(os.environ['_SCHED_FILE'])); print(len(d) if isinstance(d,list) else len(d.get('schedules',[])))" 2>/dev/null || echo "?")
9856
10109
  echo "Schedules configured: $count"
9857
10110
  else
9858
10111
  echo "Schedules configured: 0"
@@ -10165,7 +10418,7 @@ with open('$failover_file', 'w') as f: json.dump(d, f, indent=2)
10165
10418
 
10166
10419
  local chain_providers="claude,codex,gemini"
10167
10420
  if [ -f "$failover_file" ]; then
10168
- chain_providers=$(python3 -c "import json; print(','.join(json.load(open('$failover_file')).get('chain', ['claude','codex','gemini'])))" 2>/dev/null || echo "claude,codex,gemini")
10421
+ chain_providers=$(_FAILOVER_FILE="$failover_file" python3 -c "import json, os; print(','.join(json.load(open(os.environ['_FAILOVER_FILE'])).get('chain', ['claude','codex','gemini'])))" 2>/dev/null || echo "claude,codex,gemini")
10169
10422
  fi
10170
10423
 
10171
10424
  local IFS=','
@@ -11477,9 +11730,10 @@ cmd_worktree() {
11477
11730
  local status="running"
11478
11731
  local stream_name=""
11479
11732
 
11480
- # Check for merge signal
11481
- if [[ "$branch" == loki-parallel-* ]]; then
11482
- stream_name="${branch#loki-parallel-}"
11733
+ # Check for merge signal (branches named parallel-<stream> by run.sh)
11734
+ if [[ "$branch" == parallel-* ]] || [[ "$branch" == loki-parallel-* ]]; then
11735
+ stream_name="${branch#parallel-}"
11736
+ stream_name="${stream_name#loki-}"
11483
11737
  if [ -f ".loki/signals/MERGE_REQUESTED_${stream_name}" ]; then
11484
11738
  status="merge-ready"
11485
11739
  elif [ -f ".loki/signals/WORKTREE_FAILED_${stream_name}" ]; then
@@ -11536,13 +11790,27 @@ except Exception as e:
11536
11790
  MERGE_VALIDATE_PY
11537
11791
  then
11538
11792
  echo -e "${RED}Invalid or corrupted merge signal file${NC}"
11539
- exit 1
11793
+ return 1
11540
11794
  fi
11541
11795
 
11542
11796
  local branch
11543
- branch=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['branch'])")
11797
+ branch=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['branch'])" 2>/dev/null)
11544
11798
  local worktree_path
11545
- worktree_path=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['worktree'])")
11799
+ worktree_path=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['worktree'])" 2>/dev/null)
11800
+
11801
+ # Validate branch was extracted successfully
11802
+ if [ -z "$branch" ]; then
11803
+ echo -e "${RED}Could not extract branch name from merge signal${NC}"
11804
+ return 1
11805
+ fi
11806
+
11807
+ # Verify branch exists before attempting merge
11808
+ if ! git rev-parse --verify "$branch" &>/dev/null; then
11809
+ echo -e "${RED}Branch '$branch' does not exist${NC}"
11810
+ echo "The worktree may have been cleaned up already."
11811
+ rm -f "$signal_file"
11812
+ return 1
11813
+ fi
11546
11814
 
11547
11815
  if git merge --no-ff "$branch" -m "merge($name): auto-merge from parallel worktree"; then
11548
11816
  echo -e "${GREEN}Merge successful${NC}"
@@ -11563,14 +11831,50 @@ MERGE_VALIDATE_PY
11563
11831
  local removed=0
11564
11832
  local main_wt
11565
11833
  main_wt=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
11566
- while read -r line; do
11567
- local wt="${line#worktree }"
11568
- if [ -n "$wt" ] && [ "$wt" != "$main_wt" ]; then
11569
- echo " Removing: $wt"
11570
- git worktree remove "$wt" --force 2>/dev/null || true
11571
- removed=$((removed + 1))
11572
- fi
11573
- done < <(git worktree list --porcelain 2>/dev/null | grep "^worktree ")
11834
+ # BUG-PU-001: Track branches to clean up after worktree removal
11835
+ local branches_to_delete=()
11836
+ local current_wt_clean=""
11837
+ local current_branch_clean=""
11838
+ while IFS= read -r line; do
11839
+ case "$line" in
11840
+ "worktree "*)
11841
+ current_wt_clean="${line#worktree }"
11842
+ ;;
11843
+ "branch "*)
11844
+ current_branch_clean="${line#branch refs/heads/}"
11845
+ if [ -n "$current_wt_clean" ] && [ "$current_wt_clean" != "$main_wt" ]; then
11846
+ # Kill any running session in this worktree before removing
11847
+ local wt_pid_file="$current_wt_clean/.loki/loki.pid"
11848
+ if [ -f "$wt_pid_file" ]; then
11849
+ local wt_pid
11850
+ wt_pid=$(cat "$wt_pid_file" 2>/dev/null)
11851
+ if [ -n "$wt_pid" ] && kill -0 "$wt_pid" 2>/dev/null; then
11852
+ echo " Stopping session in $current_wt_clean (PID: $wt_pid)..."
11853
+ kill "$wt_pid" 2>/dev/null || true
11854
+ sleep 1
11855
+ kill -0 "$wt_pid" 2>/dev/null && kill -9 "$wt_pid" 2>/dev/null || true
11856
+ fi
11857
+ fi
11858
+ echo " Removing: $current_wt_clean"
11859
+ git worktree remove "$current_wt_clean" --force 2>/dev/null || true
11860
+ removed=$((removed + 1))
11861
+ # Queue branch for deletion (can only delete after worktree is gone)
11862
+ if [[ "$current_branch_clean" == loki-parallel-* ]] || \
11863
+ [[ "$current_branch_clean" == parallel-* ]]; then
11864
+ branches_to_delete+=("$current_branch_clean")
11865
+ fi
11866
+ fi
11867
+ current_wt_clean=""
11868
+ current_branch_clean=""
11869
+ ;;
11870
+ esac
11871
+ done <<< "$(git worktree list --porcelain 2>/dev/null)"
11872
+ # Prune worktree metadata for any already-removed directories
11873
+ git worktree prune 2>/dev/null || true
11874
+ # Clean up orphaned parallel branches
11875
+ for branch in "${branches_to_delete[@]}"; do
11876
+ git branch -D "$branch" 2>/dev/null || true
11877
+ done
11574
11878
  rm -f .loki/signals/MERGE_REQUESTED_* .loki/signals/WORKTREE_FAILED_* 2>/dev/null || true
11575
11879
  echo -e "${GREEN}Cleanup complete ($removed worktrees removed)${NC}"
11576
11880
  ;;
@@ -11755,7 +12059,7 @@ cmd_audit() {
11755
12059
  if [ ! -f "$audit_file" ]; then
11756
12060
  echo -e "${YELLOW}No audit log found at $audit_file${NC}"
11757
12061
  echo "Agent action auditing records entries during loki sessions."
11758
- exit 0
12062
+ return 0
11759
12063
  fi
11760
12064
  local lines="${2:-50}"
11761
12065
  echo -e "${BOLD}Agent Audit Log${NC} (last $lines entries)"
@@ -11781,7 +12085,7 @@ except: print(f' {sys.argv[1]}')
11781
12085
  count)
11782
12086
  if [ ! -f "$audit_file" ]; then
11783
12087
  echo -e "${YELLOW}No audit log found${NC}"
11784
- exit 0
12088
+ return 0
11785
12089
  fi
11786
12090
  echo -e "${BOLD}Agent Action Counts${NC}"
11787
12091
  echo "---"
@@ -11823,15 +12127,15 @@ print(f' {\"TOTAL\":25s} {sum(counts.values())}')
11823
12127
  echo " --preset NAME Compliance preset (default|healthcare|fintech|government)"
11824
12128
  echo " --export Save report to .loki/quality/report-{date}.json"
11825
12129
  echo ""
11826
- exit 0
12130
+ return 0
11827
12131
  ;;
11828
- *) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
12132
+ *) echo -e "${RED}Unknown option: $1${NC}"; return 1 ;;
11829
12133
  esac
11830
12134
  done
11831
12135
 
11832
12136
  case "$preset" in
11833
12137
  default|healthcare|fintech|government) ;;
11834
- *) echo -e "${RED}Error: Invalid preset '$preset'. Must be one of: default, healthcare, fintech, government${NC}"; exit 1 ;;
12138
+ *) echo -e "${RED}Error: Invalid preset '$preset'. Must be one of: default, healthcare, fintech, government${NC}"; return 1 ;;
11835
12139
  esac
11836
12140
 
11837
12141
  local port="${LOKI_DASHBOARD_PORT:-57374}"
@@ -11848,12 +12152,12 @@ print(f' {\"TOTAL\":25s} {sum(counts.values())}')
11848
12152
  if [ -z "$http_code" ] || [ "$http_code" = "000" ]; then
11849
12153
  echo -e "${RED}Error: Could not connect to dashboard API at http://${host}:${port}${NC}"
11850
12154
  echo "Make sure the dashboard is running: loki serve"
11851
- exit 1
12155
+ return 1
11852
12156
  fi
11853
12157
  if [ "$http_code" -ge 400 ] 2>/dev/null; then
11854
12158
  echo -e "${RED}Error: Dashboard API returned HTTP $http_code${NC}"
11855
12159
  [ -n "$response" ] && echo "$response"
11856
- exit 1
12160
+ return 1
11857
12161
  fi
11858
12162
 
11859
12163
  if ! command -v python3 &>/dev/null; then
@@ -12294,7 +12598,7 @@ if count == 0:
12294
12598
  *)
12295
12599
  echo -e "${RED}Unknown type: $type${NC}"
12296
12600
  echo "Valid types: patterns, mistakes, successes, all"
12297
- exit 1
12601
+ return 1
12298
12602
  ;;
12299
12603
  esac
12300
12604
  ;;
@@ -12317,11 +12621,13 @@ if count == 0:
12317
12621
  _search_py="python3.12"
12318
12622
  fi
12319
12623
 
12320
- # Try vector search first, fall back to keyword
12321
- $_search_py << PYEOF
12624
+ # BUG-PU-010: Pass query via environment variable instead of embedding
12625
+ # directly in heredoc to prevent shell/Python injection via triple-quotes
12626
+ export LOKI_MEM_QUERY="$query"
12627
+ $_search_py << 'PYEOF'
12322
12628
  import os, json, sys, glob
12323
12629
 
12324
- query = """$query"""
12630
+ query = os.environ.get('LOKI_MEM_QUERY', '')
12325
12631
  results = []
12326
12632
 
12327
12633
  # Keyword search across all memory files
@@ -12450,7 +12756,7 @@ PYEOF
12450
12756
  *)
12451
12757
  echo -e "${RED}Unknown type: $type${NC}"
12452
12758
  echo "Valid types: patterns, mistakes, successes"
12453
- exit 1
12759
+ return 1
12454
12760
  ;;
12455
12761
  esac
12456
12762
  fi
@@ -12886,7 +13192,7 @@ except Exception as e:
12886
13192
  if ls -1 .loki/memory/vectors/*.npz 2>/dev/null | head -1 >/dev/null 2>&1; then
12887
13193
  for f in .loki/memory/vectors/*.npz; do
12888
13194
  if [ -f "$f" ]; then
12889
- count=$(python3 -c "import numpy as np; d=np.load('$f'); print(len(d['ids']))" 2>/dev/null || echo "error")
13195
+ count=$(_NPZ_FILE="$f" python3 -c "import numpy as np, os; d=np.load(os.environ['_NPZ_FILE']); print(len(d['ids']))" 2>/dev/null || echo "error")
12890
13196
  echo " $(basename "$f"): $count vectors"
12891
13197
  fi
12892
13198
  done
@@ -14837,6 +15143,18 @@ cmd_telemetry() {
14837
15143
  local endpoint="${LOKI_OTEL_ENDPOINT:-}"
14838
15144
  if [ -n "$endpoint" ] && [ "$persistently_disabled" = false ]; then
14839
15145
  echo -e " Endpoint: ${GREEN}$endpoint${NC}"
15146
+ # BUG-PU-004: Actually test connectivity to the endpoint instead of
15147
+ # failing silently when Jaeger/collector is down
15148
+ local health_url="${endpoint}/v1/traces"
15149
+ local health_code
15150
+ health_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$health_url" 2>/dev/null) || health_code="000"
15151
+ if [ "$health_code" = "000" ]; then
15152
+ echo -e " Reachable: ${RED}NO (connection failed - is the collector running?)${NC}"
15153
+ elif [ "$health_code" -ge 400 ] 2>/dev/null && [ "$health_code" -ne 405 ]; then
15154
+ echo -e " Reachable: ${YELLOW}HTTP $health_code (may not be accepting traces)${NC}"
15155
+ else
15156
+ echo -e " Reachable: ${GREEN}YES${NC}"
15157
+ fi
14840
15158
  elif [ "$persistently_disabled" = true ]; then
14841
15159
  echo -e " Endpoint: ${YELLOW}ignored (opted out)${NC}"
14842
15160
  else
@@ -14861,7 +15179,7 @@ try {
14861
15179
  local config_file=".loki/config.json"
14862
15180
  if [ -f "$config_file" ]; then
14863
15181
  local saved_endpoint
14864
- saved_endpoint=$(python3 -c "import json; print(json.load(open('$config_file')).get('otel_endpoint',''))" 2>/dev/null || echo "")
15182
+ saved_endpoint=$(_CFG_FILE="$config_file" python3 -c "import json, os; print(json.load(open(os.environ['_CFG_FILE'])).get('otel_endpoint',''))" 2>/dev/null || echo "")
14865
15183
  if [ -n "$saved_endpoint" ]; then
14866
15184
  echo -e " Saved: $saved_endpoint"
14867
15185
  fi
@@ -14886,7 +15204,9 @@ try {
14886
15204
  mkdir -p .loki
14887
15205
  local config_file=".loki/config.json"
14888
15206
  if [ -f "$config_file" ]; then
14889
- LOKI_TELEM_CFG="$config_file" LOKI_TELEM_ENDPOINT="$endpoint" \
15207
+ # BUG-PU-011: Use separate if/then/fi to prevent python3 failure
15208
+ # from falling through to else and overwriting config
15209
+ if ! LOKI_TELEM_CFG="$config_file" LOKI_TELEM_ENDPOINT="$endpoint" \
14890
15210
  python3 << 'TELEM_ENABLE_PY'
14891
15211
  import json, os
14892
15212
  cfg = os.environ['LOKI_TELEM_CFG']
@@ -14897,6 +15217,10 @@ config['otel_endpoint'] = ep
14897
15217
  with open(cfg, 'w') as f:
14898
15218
  json.dump(config, f, indent=2)
14899
15219
  TELEM_ENABLE_PY
15220
+ then
15221
+ echo -e "${YELLOW}Warning: Could not update existing config, recreating${NC}"
15222
+ echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
15223
+ fi
14900
15224
  else
14901
15225
  echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
14902
15226
  fi
@@ -15771,17 +16095,20 @@ _context_show() {
15771
16095
  local ctx_file="$loki_dir/state/context-usage.json"
15772
16096
  if [ -f "$ctx_file" ]; then
15773
16097
  local total_tokens used_tokens input_tokens output_tokens cache_tokens
15774
- total_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
15775
- used_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
15776
- input_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('input_tokens', 0))" 2>/dev/null || echo "0")
15777
- output_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('output_tokens', 0))" 2>/dev/null || echo "0")
15778
- cache_tokens=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('cache_read_tokens', 0))" 2>/dev/null || echo "0")
16098
+ total_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
16099
+ used_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
16100
+ input_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('input_tokens', 0))" 2>/dev/null || echo "0")
16101
+ output_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('output_tokens', 0))" 2>/dev/null || echo "0")
16102
+ cache_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('cache_read_tokens', 0))" 2>/dev/null || echo "0")
15779
16103
 
15780
16104
  # Display gauge
15781
16105
  if type context_gauge &>/dev/null; then
15782
16106
  context_gauge "$used_tokens" "$total_tokens" "Window"
15783
16107
  else
15784
- local pct=$((used_tokens * 100 / total_tokens))
16108
+ local pct=0
16109
+ if [ "$total_tokens" -gt 0 ] 2>/dev/null; then
16110
+ pct=$((used_tokens * 100 / total_tokens))
16111
+ fi
15785
16112
  echo -e " ${CYAN}Context:${NC} ${pct}% used (${used_tokens} / ${total_tokens} tokens)"
15786
16113
  fi
15787
16114
  echo ""
@@ -15802,8 +16129,8 @@ _context_show() {
15802
16129
  echo ""
15803
16130
  echo -e " ${BOLD}Cost${NC}"
15804
16131
  local budget_used budget_limit
15805
- budget_used=$(python3 -c "import json; d=json.load(open('$budget_file')); print(round(d.get('budget_used', 0), 4))" 2>/dev/null || echo "0")
15806
- budget_limit=$(python3 -c "import json; d=json.load(open('$budget_file')); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
16132
+ budget_used=$(_BUDGET_FILE="$budget_file" python3 -c "import json, os; d=json.load(open(os.environ['_BUDGET_FILE'])); print(round(d.get('budget_used', 0), 4))" 2>/dev/null || echo "0")
16133
+ budget_limit=$(_BUDGET_FILE="$budget_file" python3 -c "import json, os; d=json.load(open(os.environ['_BUDGET_FILE'])); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
15807
16134
 
15808
16135
  if [ "$budget_limit" != "0" ]; then
15809
16136
  echo -e " ${DIM}Spent:${NC} \$${budget_used} / \$${budget_limit}"
@@ -16431,7 +16758,15 @@ for a in agents:
16431
16758
  return 1
16432
16759
  fi
16433
16760
 
16434
- local full_prompt="$persona $prompt"
16761
+ # BUG-PU-003: Separate persona from user prompt with clear delimiter
16762
+ # so the AI provider can distinguish role instruction from task
16763
+ local full_prompt="You are acting as the following specialist agent:
16764
+
16765
+ ${persona}
16766
+
16767
+ ---
16768
+
16769
+ USER TASK: ${prompt}"
16435
16770
  echo -e "${BOLD}Running as: $agent_type${NC}"
16436
16771
  echo -e "${DIM}Persona: ${persona:0:80}...${NC}"
16437
16772
  echo ""
@@ -16508,13 +16843,15 @@ for a in agents:
16508
16843
  cmd_start "$prd"
16509
16844
  else
16510
16845
  # Treat as inline prompt - create temp PRD
16511
- local tmp_prd
16512
- tmp_prd=$(mktemp /tmp/loki-agent-prd-XXXXXX.md)
16846
+ # BUG-PU-003: Use .loki/ directory instead of /tmp so run.sh
16847
+ # can find it after exec replaces this process. The temp file
16848
+ # in /tmp was never cleaned because cmd_start uses exec.
16849
+ mkdir -p "$LOKI_DIR"
16850
+ local tmp_prd="$LOKI_DIR/agent-prd-$$.md"
16513
16851
  echo "# Agent Task: $agent_type" > "$tmp_prd"
16514
16852
  echo "" >> "$tmp_prd"
16515
16853
  echo "$prd" >> "$tmp_prd"
16516
16854
  cmd_start "$tmp_prd"
16517
- rm -f "$tmp_prd"
16518
16855
  fi
16519
16856
  ;;
16520
16857
 
@@ -17183,7 +17520,7 @@ except: pass
17183
17520
  {
17184
17521
  "project": {
17185
17522
  "name": "$project_name",
17186
- "description": $(python3 -c "import json; print(json.dumps('$project_description'))" 2>/dev/null || echo "\"$project_description\""),
17523
+ "description": $(_DESC="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_DESC','')))" 2>/dev/null || echo "\"$project_description\""),
17187
17524
  "version": "$project_version",
17188
17525
  "path": "$target_path"
17189
17526
  },
@@ -20280,7 +20617,7 @@ cmd_share() {
20280
20617
 
20281
20618
  # Upload as gist
20282
20619
  echo "Uploading session report..."
20283
- local gist_desc="Loki Mode session report ($(date +%Y-%m-%d))"
20620
+ local gist_desc="Loki Mode session report ($(date +%Y-%m-%dT%H:%M:%S))"
20284
20621
  local gist_url
20285
20622
  gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" $visibility 2>&1)
20286
20623
  local gist_exit=$?