loki-mode 5.32.2 → 5.33.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/autonomy/loki CHANGED
@@ -873,8 +873,8 @@ cmd_status() {
873
873
  # Check budget
874
874
  if [ -f "$LOKI_DIR/metrics/budget.json" ]; then
875
875
  local budget_limit budget_used budget_remaining
876
- budget_limit=$(python3 -c "import json; d=json.load(open('$LOKI_DIR/metrics/budget.json')); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
877
- budget_used=$(python3 -c "import json; d=json.load(open('$LOKI_DIR/metrics/budget.json')); print(round(d.get('budget_used', 0), 2))" 2>/dev/null || echo "0")
876
+ budget_limit=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
877
+ budget_used=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); print(round(d.get('budget_used', 0), 2))" 2>/dev/null || echo "0")
878
878
  if [ "$budget_limit" != "0" ]; then
879
879
  echo -e "${CYAN}Budget:${NC} \$$budget_used / \$$budget_limit"
880
880
  fi
@@ -3109,8 +3109,10 @@ send_slack_notification() {
3109
3109
  project_name=$(basename "$(pwd)")
3110
3110
  local timestamp
3111
3111
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')
3112
- local message_escaped
3112
+ local message_escaped event_escaped project_escaped
3113
3113
  message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3114
+ event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3115
+ project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3114
3116
 
3115
3117
  # Build Slack payload with blocks for better formatting
3116
3118
  local payload
@@ -3121,7 +3123,7 @@ send_slack_notification() {
3121
3123
  "type": "header",
3122
3124
  "text": {
3123
3125
  "type": "plain_text",
3124
- "text": "Loki Mode: $event_type"
3126
+ "text": "Loki Mode: $event_escaped"
3125
3127
  }
3126
3128
  },
3127
3129
  {
@@ -3136,7 +3138,7 @@ send_slack_notification() {
3136
3138
  "elements": [
3137
3139
  {
3138
3140
  "type": "mrkdwn",
3139
- "text": "*Project:* $project_name | *Time:* $timestamp"
3141
+ "text": "*Project:* $project_escaped | *Time:* $timestamp"
3140
3142
  }
3141
3143
  ]
3142
3144
  }
@@ -3165,8 +3167,10 @@ send_discord_notification() {
3165
3167
  project_name=$(basename "$(pwd)")
3166
3168
  local timestamp
3167
3169
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')
3168
- local message_escaped
3170
+ local message_escaped event_escaped project_escaped
3169
3171
  message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3172
+ event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3173
+ project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3170
3174
 
3171
3175
  # Build Discord embed payload
3172
3176
  local payload
@@ -3174,11 +3178,11 @@ send_discord_notification() {
3174
3178
  {
3175
3179
  "embeds": [
3176
3180
  {
3177
- "title": "Loki Mode: $event_type",
3181
+ "title": "Loki Mode: $event_escaped",
3178
3182
  "description": "$message_escaped",
3179
3183
  "color": 5814783,
3180
3184
  "footer": {
3181
- "text": "Project: $project_name | $timestamp"
3185
+ "text": "Project: $project_escaped | $timestamp"
3182
3186
  }
3183
3187
  }
3184
3188
  ]
@@ -3206,19 +3210,22 @@ send_webhook_notification() {
3206
3210
  project_name=$(basename "$(pwd)")
3207
3211
  local timestamp
3208
3212
  timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
3209
- local message_escaped
3213
+ local message_escaped event_escaped project_escaped cwd_escaped
3210
3214
  message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3215
+ event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3216
+ project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3217
+ cwd_escaped=$(printf '%s' "$(pwd)" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
3211
3218
 
3212
3219
  # Build generic JSON payload
3213
3220
  local payload
3214
3221
  payload=$(cat <<EOF
3215
3222
  {
3216
3223
  "source": "loki-mode",
3217
- "event": "$event_type",
3224
+ "event": "$event_escaped",
3218
3225
  "message": "$message_escaped",
3219
- "project": "$project_name",
3226
+ "project": "$project_escaped",
3220
3227
  "timestamp": "$timestamp",
3221
- "cwd": "$(pwd)"
3228
+ "cwd": "$cwd_escaped"
3222
3229
  }
3223
3230
  EOF
3224
3231
  )
@@ -3405,12 +3412,13 @@ SESSEOF
3405
3412
 
3406
3413
  # Mark session stopped
3407
3414
  if [ -f "$LOKI_DIR/session.json" ]; then
3408
- python3 -c "
3409
- import json
3410
- with open('$LOKI_DIR/session.json', 'r') as f:
3415
+ LOKI_SESSION_FILE="$LOKI_DIR/session.json" python3 -c "
3416
+ import json, os
3417
+ p = os.environ['LOKI_SESSION_FILE']
3418
+ with open(p, 'r') as f:
3411
3419
  d = json.load(f)
3412
3420
  d['status'] = 'stopped'
3413
- with open('$LOKI_DIR/session.json', 'w') as f:
3421
+ with open(p, 'w') as f:
3414
3422
  json.dump(d, f, indent=2)
3415
3423
  " 2>/dev/null || true
3416
3424
  fi
@@ -4169,11 +4177,11 @@ cmd_memory() {
4169
4177
  case "$1" in
4170
4178
  --limit|-l)
4171
4179
  limit="${2:-20}"
4172
- shift 2
4180
+ if [ $# -ge 2 ]; then shift 2; else shift; fi
4173
4181
  ;;
4174
4182
  --project|-p)
4175
4183
  project="${2:-}"
4176
- shift 2
4184
+ if [ $# -ge 2 ]; then shift 2; else shift; fi
4177
4185
  ;;
4178
4186
  *)
4179
4187
  shift
@@ -4364,11 +4372,11 @@ for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
4364
4372
  export)
4365
4373
  local output="${2:-learnings-export.json}"
4366
4374
 
4367
- python3 -c "
4375
+ LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
4368
4376
  import json
4369
4377
  import os
4370
4378
 
4371
- learnings_dir = '$learnings_dir'
4379
+ learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
4372
4380
  result = {'patterns': [], 'mistakes': [], 'successes': []}
4373
4381
 
4374
4382
  for category in ['patterns', 'mistakes', 'successes']:
@@ -4392,12 +4400,12 @@ print(f'Exported to $output')
4392
4400
  echo -e "${BOLD}Learning Statistics${NC}"
4393
4401
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
4394
4402
 
4395
- python3 -c "
4403
+ LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
4396
4404
  import json
4397
4405
  import os
4398
4406
  from collections import Counter
4399
4407
 
4400
- learnings_dir = '$learnings_dir'
4408
+ learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
4401
4409
  projects = Counter()
4402
4410
  categories = Counter()
4403
4411
 
@@ -4431,13 +4439,13 @@ for proj, count in projects.most_common(10):
4431
4439
  echo -e "${BOLD}Deduplicating Learnings${NC}"
4432
4440
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
4433
4441
 
4434
- python3 -c "
4442
+ LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
4435
4443
  import json
4436
4444
  import os
4437
4445
  import hashlib
4438
4446
  from datetime import datetime, timezone
4439
4447
 
4440
- learnings_dir = '$learnings_dir'
4448
+ learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
4441
4449
 
4442
4450
  def dedupe_file(filepath):
4443
4451
  '''Deduplicate a JSONL file, keeping first occurrence of each unique description'''
@@ -4526,13 +4534,14 @@ except Exception as e:
4526
4534
  consolidate)
4527
4535
  # Run consolidation pipeline
4528
4536
  local hours="${2:-24}"
4529
- python3 -c "
4537
+ LOKI_HOURS="$hours" python3 -c "
4538
+ import os
4530
4539
  try:
4531
4540
  from memory.consolidation import ConsolidationPipeline
4532
4541
  from memory.storage import MemoryStorage
4533
4542
  storage = MemoryStorage('.loki/memory')
4534
4543
  pipeline = ConsolidationPipeline(storage)
4535
- result = pipeline.consolidate(since_hours=$hours)
4544
+ result = pipeline.consolidate(since_hours=int(os.environ.get('LOKI_HOURS', '24')))
4536
4545
  print('Consolidation complete:')
4537
4546
  print(f' Patterns created: {result.patterns_created}')
4538
4547
  print(f' Patterns merged: {result.patterns_merged}')
@@ -5326,9 +5335,9 @@ cmd_council() {
5326
5335
  fi
5327
5336
 
5328
5337
  if [ -f "$council_dir/state.json" ]; then
5329
- python3 -c "
5330
- import json
5331
- with open('$council_dir/state.json') as f:
5338
+ LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
5339
+ import json, os
5340
+ with open(os.environ['LOKI_COUNCIL_STATE']) as f:
5332
5341
  state = json.load(f)
5333
5342
  print(f\"Enabled: {state.get('initialized', False)}\")
5334
5343
  print(f\"Total votes: {state.get('total_votes', 0)}\")
@@ -5354,9 +5363,9 @@ else:
5354
5363
  echo -e "${CYAN}=== Council Decision Log ===${NC}"
5355
5364
 
5356
5365
  if [ -f "$council_dir/state.json" ]; then
5357
- python3 -c "
5358
- import json
5359
- with open('$council_dir/state.json') as f:
5366
+ LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
5367
+ import json, os
5368
+ with open(os.environ['LOKI_COUNCIL_STATE']) as f:
5360
5369
  state = json.load(f)
5361
5370
  verdicts = state.get('verdicts', [])
5362
5371
  if not verdicts:
package/autonomy/run.sh CHANGED
@@ -1067,14 +1067,16 @@ import_github_issues() {
1067
1067
  local pending_file=".loki/queue/pending.json"
1068
1068
  local task_count=0
1069
1069
 
1070
- # Ensure pending.json exists with correct format for GitHub import
1070
+ # BUG #14 fix: Normalize to bare [] format (consistent with init_loki_dir
1071
+ # and all other queue consumers). Previously used {"tasks":[]} wrapper here
1072
+ # but bare [] everywhere else, causing format mismatch.
1071
1073
  if [ ! -f "$pending_file" ]; then
1072
- echo '{"tasks":[]}' > "$pending_file"
1073
- elif jq -e 'type == "array"' "$pending_file" &>/dev/null; then
1074
- # Normalize bare array format to {"tasks":[...]} for GitHub import compatibility
1074
+ echo '[]' > "$pending_file"
1075
+ elif jq -e 'type == "object"' "$pending_file" &>/dev/null; then
1076
+ # Normalize {"tasks":[...]} wrapper to bare array
1075
1077
  local _tmp_normalize
1076
1078
  _tmp_normalize=$(mktemp)
1077
- jq '{tasks: .}' "$pending_file" > "$_tmp_normalize" && mv "$_tmp_normalize" "$pending_file"
1079
+ jq 'if type == "object" then .tasks // [] else . end' "$pending_file" > "$_tmp_normalize" && mv "$_tmp_normalize" "$pending_file"
1078
1080
  rm -f "$_tmp_normalize"
1079
1081
  fi
1080
1082
 
@@ -1094,8 +1096,8 @@ import_github_issues() {
1094
1096
  url=$(echo "$issue" | jq -r '.url')
1095
1097
  labels=$(echo "$issue" | jq -c '[.labels[].name]')
1096
1098
 
1097
- # Check if task already exists
1098
- if jq -e ".tasks[] | select(.github_issue == $number)" "$pending_file" &>/dev/null; then
1099
+ # Check if task already exists (bare array format)
1100
+ if jq -e ".[] | select(.github_issue == $number)" "$pending_file" &>/dev/null; then
1099
1101
  log_info "Issue #$number already imported, skipping"
1100
1102
  continue
1101
1103
  fi
@@ -1137,10 +1139,10 @@ import_github_issues() {
1137
1139
  created_at: $created
1138
1140
  }')
1139
1141
 
1140
- # Append to pending.json with temp file cleanup on error
1142
+ # Append to pending.json (bare array format) with temp file cleanup on error
1141
1143
  local temp_file
1142
1144
  temp_file=$(mktemp)
1143
- if jq ".tasks += [$task_json]" "$pending_file" > "$temp_file" && mv "$temp_file" "$pending_file"; then
1145
+ if jq ". += [$task_json]" "$pending_file" > "$temp_file" && mv "$temp_file" "$pending_file"; then
1144
1146
  log_info "Imported issue #$number: $title"
1145
1147
  task_count=$((task_count + 1))
1146
1148
  else
@@ -1293,8 +1295,8 @@ export_tasks_to_github() {
1293
1295
  return 0
1294
1296
  fi
1295
1297
 
1296
- # Export non-GitHub tasks as issues
1297
- jq -c '.tasks[] | select(.source != "github")' "$pending_file" 2>/dev/null | while read -r task; do
1298
+ # Export non-GitHub tasks as issues (handles both bare array and wrapper formats)
1299
+ jq -c 'if type == "object" then .tasks // [] else . end | .[] | select(.source != "github")' "$pending_file" 2>/dev/null | while read -r task; do
1298
1300
  local title desc
1299
1301
  title=$(echo "$task" | jq -r '.title')
1300
1302
  desc=$(echo "$task" | jq -r '.description // ""')
@@ -2415,13 +2417,20 @@ write_dashboard_state() {
2415
2417
  local project_path=$(pwd)
2416
2418
  local _tmp_state="${output_file}.tmp"
2417
2419
 
2420
+ # BUG #49 fix: Escape project path/name for JSON to handle special chars
2421
+ # (spaces, quotes, backslashes in directory names)
2422
+ local project_name_escaped
2423
+ local project_path_escaped
2424
+ project_name_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g')
2425
+ project_path_escaped=$(printf '%s' "$project_path" | sed 's/\\/\\\\/g; s/"/\\"/g')
2426
+
2418
2427
  cat > "$_tmp_state" << EOF
2419
2428
  {
2420
2429
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
2421
2430
  "version": "$version",
2422
2431
  "project": {
2423
- "name": "$project_name",
2424
- "path": "$project_path"
2432
+ "name": "$project_name_escaped",
2433
+ "path": "$project_path_escaped"
2425
2434
  },
2426
2435
  "mode": "$mode",
2427
2436
  "provider": "${PROVIDER_NAME:-claude}",
@@ -4665,12 +4674,12 @@ build_prompt() {
4665
4674
  fi
4666
4675
 
4667
4676
  # Human directive injection (from HUMAN_INPUT.md)
4677
+ # NOTE: Do NOT unset LOKI_HUMAN_INPUT here - build_prompt runs in a subshell
4678
+ # (command substitution) so unset would not affect the parent shell.
4679
+ # The caller (run_autonomous) clears it after consuming the prompt.
4668
4680
  local human_directive=""
4669
4681
  if [ -n "${LOKI_HUMAN_INPUT:-}" ]; then
4670
4682
  human_directive="HUMAN_DIRECTIVE (PRIORITY): $LOKI_HUMAN_INPUT Execute this directive BEFORE continuing normal tasks."
4671
- # Clear after consumption so it doesn't repeat every iteration
4672
- unset LOKI_HUMAN_INPUT
4673
- rm -f "${TARGET_DIR:-.}/.loki/HUMAN_INPUT.md"
4674
4683
  fi
4675
4684
 
4676
4685
  # Queue task injection (from dashboard or API)
@@ -4795,6 +4804,15 @@ run_autonomous() {
4795
4804
 
4796
4805
  local prompt=$(build_prompt $retry "$prd_path" $ITERATION_COUNT)
4797
4806
 
4807
+ # BUG #5 fix: Clear LOKI_HUMAN_INPUT in the parent shell after build_prompt
4808
+ # consumed it. build_prompt runs in a subshell (command substitution), so
4809
+ # any unset inside it does not affect the parent. Clear here to prevent
4810
+ # the same directive from repeating every iteration.
4811
+ if [ -n "${LOKI_HUMAN_INPUT:-}" ]; then
4812
+ unset LOKI_HUMAN_INPUT
4813
+ rm -f "${TARGET_DIR:-.}/.loki/HUMAN_INPUT.md"
4814
+ fi
4815
+
4798
4816
  echo ""
4799
4817
  log_header "Attempt $((retry + 1)) of $MAX_RETRIES"
4800
4818
  log_info "Prompt: $prompt"
@@ -5234,11 +5252,19 @@ check_human_intervention() {
5234
5252
  local loki_dir="${TARGET_DIR:-.}/.loki"
5235
5253
 
5236
5254
  # Check for PAUSE file
5255
+ # BUG #4 fix: Check handle_pause return value before deleting PAUSE file.
5256
+ # handle_pause returns 1 if STOP was requested during the pause, so we must
5257
+ # propagate that as return 2 (stop) instead of always returning 1 (continue).
5237
5258
  if [ -f "$loki_dir/PAUSE" ]; then
5238
5259
  log_warn "PAUSE file detected - pausing execution"
5239
5260
  notify_intervention_needed "Execution paused via PAUSE file"
5240
5261
  handle_pause
5262
+ local pause_result=$?
5241
5263
  rm -f "$loki_dir/PAUSE"
5264
+ if [ "$pause_result" -eq 1 ]; then
5265
+ # STOP was requested during pause
5266
+ return 2
5267
+ fi
5242
5268
  return 1
5243
5269
  fi
5244
5270
 
@@ -5287,8 +5313,13 @@ check_human_intervention() {
5287
5313
  rm -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED"
5288
5314
  if type council_vote &>/dev/null && council_vote; then
5289
5315
  log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
5290
- # Complete the missing steps: COMPLETED marker, memory consolidation, report
5291
- touch "$loki_dir/COMPLETED"
5316
+ # BUG #17 fix: Write COMPLETED marker, generate council report, and
5317
+ # run memory consolidation (matching the normal council approval path
5318
+ # in council_should_stop).
5319
+ echo "Council force-review approved at iteration $ITERATION_COUNT on $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$loki_dir/COMPLETED"
5320
+ if type council_write_report &>/dev/null; then
5321
+ council_write_report
5322
+ fi
5292
5323
  log_info "Running memory consolidation..."
5293
5324
  run_memory_consolidation
5294
5325
  notify_all_complete
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.32.2"
10
+ __version__ = "5.33.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -56,10 +56,13 @@ app = FastAPI(
56
56
  version="1.0.0"
57
57
  )
58
58
 
59
- # CORS middleware for dashboard frontend
59
+ # CORS middleware for dashboard frontend - restricted to localhost by default.
60
+ # Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
61
+ _cors_default = "http://localhost:57374,http://127.0.0.1:57374"
62
+ _cors_origins = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default).split(",")
60
63
  app.add_middleware(
61
64
  CORSMiddleware,
62
- allow_origins=["*"],
65
+ allow_origins=[o.strip() for o in _cors_origins if o.strip()],
63
66
  allow_credentials=True,
64
67
  allow_methods=["*"],
65
68
  allow_headers=["*"],
@@ -511,4 +514,5 @@ async def asyncio_sleep(seconds: float):
511
514
  if __name__ == "__main__":
512
515
  import uvicorn
513
516
  port = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
514
- uvicorn.run(app, host="0.0.0.0", port=port)
517
+ host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
518
+ uvicorn.run(app, host=host, port=port)
@@ -209,9 +209,10 @@ app = FastAPI(
209
209
  lifespan=lifespan,
210
210
  )
211
211
 
212
- # Add CORS middleware - allow_origins=* is safe because server binds to
213
- # 127.0.0.1 by default. Set LOKI_DASHBOARD_HOST to override if LAN access needed.
214
- _cors_origins = os.environ.get("CORS_ALLOWED_ORIGINS", "*").split(",")
212
+ # Add CORS middleware - restricted to localhost by default.
213
+ # Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
214
+ _cors_default = "http://localhost:57374,http://127.0.0.1:57374"
215
+ _cors_origins = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default).split(",")
215
216
  app.add_middleware(
216
217
  CORSMiddleware,
217
218
  allow_origins=[o.strip() for o in _cors_origins if o.strip()],
@@ -2046,6 +2047,7 @@ async def get_agents():
2046
2047
  @app.post("/api/agents/{agent_id}/kill")
2047
2048
  async def kill_agent(agent_id: str):
2048
2049
  """Kill a specific agent by ID."""
2050
+ agent_id = _sanitize_agent_id(agent_id)
2049
2051
  agents_file = _get_loki_dir() / "state" / "agents.json"
2050
2052
  if not agents_file.exists():
2051
2053
  raise HTTPException(404, "No agents file found")
@@ -2296,7 +2298,7 @@ def run_server(host: str = None, port: int = None) -> None:
2296
2298
  """Run the dashboard server."""
2297
2299
  import uvicorn
2298
2300
  if host is None:
2299
- # Default to localhost-only; CORS * is safe since not exposed to LAN
2301
+ # Default to localhost-only for security
2300
2302
  host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
2301
2303
  if port is None:
2302
2304
  port = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
@@ -3750,7 +3750,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
3750
3750
  ${t}
3751
3751
  </div>
3752
3752
  </div>
3753
- `,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("time-range-select");e&&e.addEventListener("change",r=>this._setFilter("timeRange",r.target.value));let t=this.shadowRoot.getElementById("signal-type-select");t&&t.addEventListener("change",r=>this._setFilter("signalType",r.target.value));let a=this.shadowRoot.getElementById("source-select");a&&a.addEventListener("change",r=>this._setFilter("source",r.target.value));let i=this.shadowRoot.getElementById("refresh-btn");i&&i.addEventListener("click",()=>this._loadData());let s=this.shadowRoot.getElementById("close-detail");s&&s.addEventListener("click",()=>this._closeDetail()),this.shadowRoot.querySelectorAll(".list-item").forEach(r=>{r.addEventListener("click",()=>{let n=r.dataset.type,d=r.dataset.id,p=this._findItemData(n,d);p&&this._selectMetric(n,p)}),r.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),r.click())})})}_findItemData(e,t){if(!this._metrics?.aggregation)return null;switch(e){case"preference":return this._metrics.aggregation.preferences?.find(a=>a.preference_key===t);case"error_pattern":return this._metrics.aggregation.error_patterns?.find(a=>a.error_type===t);case"success_pattern":return this._metrics.aggregation.success_patterns?.find(a=>a.pattern_name===t);case"tool_efficiency":return this._metrics.aggregation.tool_efficiencies?.find(a=>a.tool_name===t);default:return null}}};customElements.get("loki-learning-dashboard")||customElements.define("loki-learning-dashboard",H);var me=[{id:"overview",label:"Overview"},{id:"decisions",label:"Decision Log"},{id:"convergence",label:"Convergence"},{id:"agents",label:"Agents"}],O=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._activeTab="overview",this._pollInterval=null,this._councilState=null,this._verdicts=[],this._convergence=[],this._agents=[],this._selectedAgent=null,this._lastDataHash=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:e})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),3e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadData(),this._pollInterval=setInterval(()=>this._loadData(),3e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null)}async _loadData(){try{let[t,a,i,s]=await Promise.allSettled([this._api._get("/api/council/state"),this._api._get("/api/council/verdicts"),this._api._get("/api/council/convergence"),this._api._get("/api/agents")]);t.status==="fulfilled"&&(this._councilState=t.value),a.status==="fulfilled"&&(this._verdicts=a.value.verdicts||[]),i.status==="fulfilled"&&(this._convergence=i.value.dataPoints||[]),s.status==="fulfilled"&&(this._agents=Array.isArray(s.value)?s.value:[]),this._error=null}catch(t){this._error=t.message}let e=JSON.stringify({s:this._councilState,v:this._verdicts,c:this._convergence,a:this._agents,e:this._error});e!==this._lastDataHash&&(this._lastDataHash=e,this.render())}async _forceReview(){try{await this._api._post("/api/council/force-review"),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"force-review"},bubbles:!0}))}catch(e){this._error=`Failed to force review: ${e.message}`,this.render()}}async _killAgent(e){if(confirm(`Kill agent ${e}?`))try{await this._api._post(`/api/agents/${e}/kill`),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"kill-agent",agentId:e},bubbles:!0})),await this._loadData()}catch(t){this._error=`Failed to kill agent: ${t.message}`,this.render()}}async _pauseAgent(e){try{await this._api._post(`/api/agents/${e}/pause`),await this._loadData()}catch(t){this._error=`Failed to pause agent: ${t.message}`,this.render()}}async _resumeAgent(e){try{await this._api._post(`/api/agents/${e}/resume`),await this._loadData()}catch(t){this._error=`Failed to resume agent: ${t.message}`,this.render()}}_setTab(e){this._activeTab=e,this.render()}_selectAgent(e){this._selectedAgent=this._selectedAgent?.id===e.id?null:e,this.render()}render(){let e=this.shadowRoot;e&&(e.innerHTML=`
3753
+ `,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("time-range-select");e&&e.addEventListener("change",r=>this._setFilter("timeRange",r.target.value));let t=this.shadowRoot.getElementById("signal-type-select");t&&t.addEventListener("change",r=>this._setFilter("signalType",r.target.value));let a=this.shadowRoot.getElementById("source-select");a&&a.addEventListener("change",r=>this._setFilter("source",r.target.value));let i=this.shadowRoot.getElementById("refresh-btn");i&&i.addEventListener("click",()=>this._loadData());let s=this.shadowRoot.getElementById("close-detail");s&&s.addEventListener("click",()=>this._closeDetail()),this.shadowRoot.querySelectorAll(".list-item").forEach(r=>{r.addEventListener("click",()=>{let n=r.dataset.type,d=r.dataset.id,p=this._findItemData(n,d);p&&this._selectMetric(n,p)}),r.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),r.click())})})}_findItemData(e,t){if(!this._metrics?.aggregation)return null;switch(e){case"preference":return this._metrics.aggregation.preferences?.find(a=>a.preference_key===t);case"error_pattern":return this._metrics.aggregation.error_patterns?.find(a=>a.error_type===t);case"success_pattern":return this._metrics.aggregation.success_patterns?.find(a=>a.pattern_name===t);case"tool_efficiency":return this._metrics.aggregation.tool_efficiencies?.find(a=>a.tool_name===t);default:return null}}};customElements.get("loki-learning-dashboard")||customElements.define("loki-learning-dashboard",H);var me=[{id:"overview",label:"Overview"},{id:"decisions",label:"Decision Log"},{id:"convergence",label:"Convergence"},{id:"agents",label:"Agents"}],O=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._activeTab="overview",this._pollInterval=null,this._councilState=null,this._verdicts=[],this._convergence=[],this._agents=[],this._selectedAgent=null,this._lastDataHash=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:e})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),3e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadData(),this._pollInterval=setInterval(()=>this._loadData(),3e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null),this._pendingRaf&&(cancelAnimationFrame(this._pendingRaf),this._pendingRaf=null)}async _loadData(){try{let[t,a,i,s]=await Promise.allSettled([this._api._get("/api/council/state"),this._api._get("/api/council/verdicts"),this._api._get("/api/council/convergence"),this._api._get("/api/agents")]);t.status==="fulfilled"&&(this._councilState=t.value),a.status==="fulfilled"&&(this._verdicts=a.value.verdicts||[]),i.status==="fulfilled"&&(this._convergence=i.value.dataPoints||[]),s.status==="fulfilled"&&(this._agents=Array.isArray(s.value)?s.value:[]),this._error=null}catch(t){this._error=t.message}let e=JSON.stringify({s:this._councilState,v:this._verdicts,c:this._convergence,a:this._agents,e:this._error});e!==this._lastDataHash&&(this._lastDataHash=e,this.render())}async _forceReview(){try{await this._api._post("/api/council/force-review"),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"force-review"},bubbles:!0}))}catch(e){this._error=`Failed to force review: ${e.message}`,this.render()}}async _killAgent(e){if(confirm(`Kill agent ${e}?`))try{await this._api._post(`/api/agents/${e}/kill`),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"kill-agent",agentId:e},bubbles:!0})),await this._loadData()}catch(t){this._error=`Failed to kill agent: ${t.message}`,this.render()}}async _pauseAgent(e){try{await this._api._post(`/api/agents/${e}/pause`),await this._loadData()}catch(t){this._error=`Failed to pause agent: ${t.message}`,this.render()}}async _resumeAgent(e){try{await this._api._post(`/api/agents/${e}/resume`),await this._loadData()}catch(t){this._error=`Failed to resume agent: ${t.message}`,this.render()}}_setTab(e){this._activeTab=e,this.render()}_selectAgent(e){this._selectedAgent=this._selectedAgent?.id===e.id?null:e,this.render()}render(){let e=this.shadowRoot;e&&(this._pendingRaf&&(cancelAnimationFrame(this._pendingRaf),this._pendingRaf=null),e.innerHTML=`
3754
3754
  <style>${this.getBaseStyles()}${this._getStyles()}</style>
3755
3755
  <div class="council-dashboard">
3756
3756
  <div class="council-header">
@@ -3758,7 +3758,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
3758
3758
  <h2 class="title">Completion Council</h2>
3759
3759
  ${this._councilState?.enabled!==!1?'<span class="badge badge-active">Active</span>':'<span class="badge badge-inactive">Disabled</span>'}
3760
3760
  </div>
3761
- <button class="btn btn-primary" onclick="this.getRootNode().host._forceReview()">
3761
+ <button class="btn btn-primary" id="force-review-btn">
3762
3762
  Force Review
3763
3763
  </button>
3764
3764
  </div>
@@ -3767,7 +3767,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
3767
3767
  ${me.map(t=>`
3768
3768
  <button
3769
3769
  class="tab ${this._activeTab===t.id?"active":""}"
3770
- onclick="this.getRootNode().host._setTab('${t.id}')"
3770
+ data-tab="${t.id}"
3771
3771
  >${t.label}</button>
3772
3772
  `).join("")}
3773
3773
  </div>
@@ -3778,7 +3778,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
3778
3778
 
3779
3779
  ${this._error?`<div class="error-banner">${this._error}</div>`:""}
3780
3780
  </div>
3781
- `)}_renderTabContent(){switch(this._activeTab){case"overview":return this._renderOverview();case"decisions":return this._renderDecisions();case"convergence":return this._renderConvergence();case"agents":return this._renderAgents();default:return""}}_renderOverview(){let e=this._councilState||{},t=e.consecutive_no_change||0,a=e.done_signals||0,i=e.total_votes||0,s=e.approve_votes||0,r=this._verdicts.length>0?this._verdicts[this._verdicts.length-1]:null,n=this._agents.filter(d=>d.alive).length;return`
3781
+ `,this._attachEventListeners())}_attachEventListeners(){let e=this.shadowRoot;if(!e)return;let t=e.getElementById("force-review-btn");t&&t.addEventListener("click",()=>this._forceReview()),e.querySelectorAll(".tab[data-tab]").forEach(a=>{a.addEventListener("click",()=>this._setTab(a.dataset.tab))})}_renderTabContent(){switch(this._activeTab){case"overview":return this._renderOverview();case"decisions":return this._renderDecisions();case"convergence":return this._renderConvergence();case"agents":return this._renderAgents();default:return""}}_renderOverview(){let e=this._councilState||{},t=e.consecutive_no_change||0,a=e.done_signals||0,i=e.total_votes||0,s=e.approve_votes||0,r=this._verdicts.length>0?this._verdicts[this._verdicts.length-1]:null,n=this._agents.filter(d=>d.alive).length;return`
3782
3782
  <div class="overview-grid">
3783
3783
  <div class="stat-card">
3784
3784
  <div class="stat-label">Council Status</div>
@@ -3918,7 +3918,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
3918
3918
  </div>
3919
3919
  `).join("")}
3920
3920
  </div>
3921
- `;return requestAnimationFrame(()=>{let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(a=>{let i=parseInt(a.dataset.agentIndex,10),s=this._agents[i];s&&(a.addEventListener("click",()=>this._selectAgent(s)),a.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",n=>{n.stopPropagation();let d=r.dataset.action,p=r.dataset.agentId;d==="pause"?this._pauseAgent(p):d==="kill"?this._killAgent(p):d==="resume"&&this._resumeAgent(p)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_getStyles(){return`
3921
+ `;return this._pendingRaf=requestAnimationFrame(()=>{this._pendingRaf=null;let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(a=>{let i=parseInt(a.dataset.agentIndex,10),s=this._agents[i];s&&(a.addEventListener("click",()=>this._selectAgent(s)),a.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",n=>{n.stopPropagation();let d=r.dataset.action,p=r.dataset.agentId;d==="pause"?this._pauseAgent(p):d==="kill"?this._killAgent(p):d==="resume"&&this._resumeAgent(p)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_getStyles(){return`
3922
3922
  :host {
3923
3923
  display: block;
3924
3924
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
@@ -2,7 +2,7 @@
2
2
 
3
3
  Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v5.32.2
5
+ **Version:** v5.33.0
6
6
 
7
7
  ---
8
8
 
package/events/emit.sh CHANGED
@@ -25,30 +25,37 @@ mkdir -p "$EVENTS_DIR"
25
25
  TYPE="${1:-state}"
26
26
  SOURCE="${2:-cli}"
27
27
  ACTION="${3:-unknown}"
28
- if [ $# -ge 3 ]; then shift 3; else shift $#; fi
28
+ if [ "$#" -ge 3 ]; then shift 3; else shift "$#"; fi
29
29
 
30
30
  # Generate event ID and timestamp
31
31
  EVENT_ID=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')
32
32
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
33
33
 
34
+ # JSON escape helper: handles \, ", and control characters
35
+ json_escape() {
36
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | tr -d '\n'
37
+ }
38
+
34
39
  # Build payload JSON
35
- PAYLOAD="{\"action\":\"$ACTION\""
40
+ ACTION_ESC=$(json_escape "$ACTION")
41
+ PAYLOAD="{\"action\":\"$ACTION_ESC\""
36
42
  for arg in "$@"; do
37
43
  key="${arg%%=*}"
38
44
  value="${arg#*=}"
39
- # Escape special characters for JSON
40
- key_escaped=$(printf '%s' "$key" | sed 's/\\/\\\\/g; s/"/\\"/g')
41
- value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
42
- PAYLOAD="$PAYLOAD,\"$key_escaped\":\"$value\""
45
+ key_escaped=$(json_escape "$key")
46
+ value_escaped=$(json_escape "$value")
47
+ PAYLOAD="$PAYLOAD,\"$key_escaped\":\"$value_escaped\""
43
48
  done
44
49
  PAYLOAD="$PAYLOAD}"
45
50
 
46
- # Build full event JSON
51
+ # Build full event JSON (escape type/source for safe embedding)
52
+ TYPE_ESC=$(json_escape "$TYPE")
53
+ SOURCE_ESC=$(json_escape "$SOURCE")
47
54
  EVENT=$(cat <<EOF
48
55
  {
49
56
  "id": "$EVENT_ID",
50
- "type": "$TYPE",
51
- "source": "$SOURCE",
57
+ "type": "$TYPE_ESC",
58
+ "source": "$SOURCE_ESC",
52
59
  "timestamp": "$TIMESTAMP",
53
60
  "payload": $PAYLOAD,
54
61
  "version": "1.0"