loki-mode 5.46.0 → 5.48.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/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  [![HumanEval](https://img.shields.io/badge/HumanEval-98.17%25%20Pass%401-brightgreen)](benchmarks/results/)
14
14
  [![SWE-bench](https://img.shields.io/badge/SWE--bench-99.67%25%20Patch%20Gen-brightgreen)](benchmarks/results/)
15
15
 
16
- **Current Version: v5.46.0**
16
+ **Current Version: v5.47.0**
17
17
 
18
18
  **[Autonomi](https://www.autonomi.dev/)** | **[Documentation](https://www.autonomi.dev/docs)** | **[GitHub](https://github.com/asklokesh/loki-mode)**
19
19
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v5.46.0
6
+ # Loki Mode v5.48.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -262,4 +262,4 @@ The following features are documented in skill modules but not yet fully automat
262
262
  | Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
263
263
  | Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
264
264
 
265
- **v5.46.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
265
+ **v5.48.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.46.0
1
+ 5.48.0
@@ -395,8 +395,9 @@ _install_node_deps() {
395
395
  _install_python_deps() {
396
396
  local dir="$1"
397
397
  if [ -f "$dir/requirements.txt" ]; then
398
- log_step "App Runner: installing Python dependencies (background)..."
399
- (cd "$dir" && pip install -r requirements.txt >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
398
+ log_step "App Runner: installing Python dependencies..."
399
+ (cd "$dir" && pip install -r requirements.txt >> "$_APP_RUNNER_DIR/app.log" 2>&1) || \
400
+ log_warn "App Runner: pip install failed, app may not start"
400
401
  fi
401
402
  }
402
403
 
@@ -425,14 +426,18 @@ app_runner_start() {
425
426
  _rotate_app_log
426
427
 
427
428
  # Start the process in a new process group
428
- (cd "$dir" && setsid bash -c "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
429
+ if command -v setsid >/dev/null 2>&1; then
430
+ (cd "$dir" && setsid bash -c "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
431
+ else
432
+ (cd "$dir" && bash -c "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
433
+ fi
429
434
  _APP_RUNNER_PID=$!
430
435
 
431
436
  # Write PID file
432
437
  echo "$_APP_RUNNER_PID" > "$_APP_RUNNER_DIR/app.pid"
433
438
 
434
439
  # Capture initial git diff hash for change detection
435
- _GIT_DIFF_HASH=$(cd "$dir" && git diff --stat 2>/dev/null | md5sum 2>/dev/null | awk '{print $1}' || echo "none")
440
+ _GIT_DIFF_HASH=$(cd "$dir" && git diff --stat 2>/dev/null | (md5sum 2>/dev/null || md5 -r 2>/dev/null) | awk '{print $1}' || echo "none")
436
441
 
437
442
  # Brief pause for process to initialize
438
443
  sleep 2
@@ -557,7 +562,7 @@ app_runner_should_restart() {
557
562
 
558
563
  # Get current git diff hash
559
564
  local current_hash
560
- current_hash=$(cd "$dir" && git diff --stat 2>/dev/null | md5sum 2>/dev/null | awk '{print $1}' || echo "none")
565
+ current_hash=$(cd "$dir" && git diff --stat 2>/dev/null | (md5sum 2>/dev/null || md5 -r 2>/dev/null) | awk '{print $1}' || echo "none")
561
566
 
562
567
  # No change
563
568
  if [ "$current_hash" = "$_GIT_DIFF_HASH" ]; then
@@ -631,7 +636,7 @@ app_runner_watchdog() {
631
636
  # Clear PID and restart
632
637
  rm -f "$_APP_RUNNER_DIR/app.pid"
633
638
  _APP_RUNNER_PID=""
634
- app_runner_start
639
+ app_runner_start || log_warn "App Runner: auto-restart failed"
635
640
  }
636
641
 
637
642
  #===============================================================================
@@ -32,7 +32,7 @@ from pathlib import Path
32
32
 
33
33
  # Allowed characters in check paths and patterns (security: prevent injection)
34
34
  _SAFE_PATH_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?]+$')
35
- _SAFE_PATTERN_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?|\\()+^$\s]+$')
35
+ _SAFE_PATTERN_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?|\\()+^$\s:=<>@#"\'`,;!&%]+$')
36
36
 
37
37
 
38
38
  def _validate_path(path: str, project_dir: str) -> str:
@@ -170,7 +170,8 @@ def run_check(check: dict, project_dir: str, timeout: int) -> dict:
170
170
  elif check_type == "http_check":
171
171
  path = check.get("path", "/")
172
172
  # Validate path is safe
173
- if path and not _SAFE_PATH_RE.match(path.lstrip("/")):
173
+ stripped = path.lstrip("/")
174
+ if stripped and not _SAFE_PATH_RE.match(stripped):
174
175
  result["passed"] = None
175
176
  result["output"] = f"Unsafe path rejected: {path!r}"
176
177
  else:
@@ -461,6 +461,124 @@ try:
461
461
  except: print('Results unavailable')
462
462
  " >> "$evidence_file" 2>/dev/null || echo "Playwright data unavailable" >> "$evidence_file"
463
463
  fi
464
+
465
+ # Add hard gate status
466
+ if [ -f "$COUNCIL_STATE_DIR/gate-block.json" ]; then
467
+ echo "" >> "$evidence_file"
468
+ echo "## Hard Gate Status: BLOCKED" >> "$evidence_file"
469
+ echo "Critical checklist items are failing. Completion is blocked until resolved." >> "$evidence_file"
470
+ cat "$COUNCIL_STATE_DIR/gate-block.json" >> "$evidence_file"
471
+ fi
472
+ }
473
+
474
+ #===============================================================================
475
+ # Council Reverify Checklist - Re-run checklist before evaluation
476
+ #===============================================================================
477
+
478
+ # Re-verify checklist before council evaluation to ensure fresh data
479
+ council_reverify_checklist() {
480
+ if type checklist_verify &>/dev/null && [ -f ".loki/checklist/checklist.json" ]; then
481
+ log_info "[Council] Re-verifying checklist before evaluation..."
482
+ checklist_verify 2>/dev/null || true
483
+ fi
484
+ }
485
+
486
+ #===============================================================================
487
+ # Council Checklist Hard Gate - Block completion on critical failures
488
+ #===============================================================================
489
+
490
+ # Council hard gate: blocks completion if critical checklist items are failing
491
+ # Returns 0 if gate passes (ok to complete), 1 if gate blocks (critical failures exist)
492
+ council_checklist_gate() {
493
+ local results_file=".loki/checklist/verification-results.json"
494
+ local waivers_file=".loki/checklist/waivers.json"
495
+
496
+ # No checklist = no gate (backwards compatible)
497
+ if [ ! -f "$results_file" ]; then
498
+ return 0
499
+ fi
500
+
501
+ # Check for critical failures, excluding waived items
502
+ local gate_result
503
+ gate_result=$(_RESULTS_FILE="$results_file" _WAIVERS_FILE="$waivers_file" python3 -c "
504
+ import json, sys, os
505
+
506
+ results_file = os.environ['_RESULTS_FILE']
507
+ waivers_file = os.environ.get('_WAIVERS_FILE', '')
508
+
509
+ try:
510
+ with open(results_file) as f:
511
+ results = json.load(f)
512
+ except (json.JSONDecodeError, IOError, KeyError):
513
+ print('PASS')
514
+ sys.exit(0)
515
+
516
+ # Load waivers
517
+ waived_ids = set()
518
+ if waivers_file and os.path.exists(waivers_file):
519
+ try:
520
+ with open(waivers_file) as f:
521
+ waivers = json.load(f)
522
+ waived_ids = {w['item_id'] for w in waivers.get('waivers', []) if w.get('active', True)}
523
+ except (json.JSONDecodeError, KeyError):
524
+ pass
525
+
526
+ # Find critical failures not waived
527
+ critical_failures = []
528
+ for cat in results.get('categories', []):
529
+ for item in cat.get('items', []):
530
+ if item.get('priority') == 'critical' and item.get('status') == 'failing':
531
+ if item.get('id') not in waived_ids:
532
+ critical_failures.append(item.get('title', item.get('id', 'unknown')))
533
+
534
+ if critical_failures:
535
+ print('BLOCK:' + '|'.join(critical_failures[:5]))
536
+ sys.exit(0)
537
+ else:
538
+ print('PASS')
539
+ sys.exit(0)
540
+ " 2>/dev/null || echo "PASS")
541
+
542
+ if [[ "$gate_result" == BLOCK:* ]]; then
543
+ local failures="${gate_result#BLOCK:}"
544
+ log_warn "[Council] Hard gate BLOCKED: critical checklist failures: ${failures//|/, }"
545
+
546
+ # Write gate block to council state (atomic write via temp file)
547
+ local gate_file="$COUNCIL_STATE_DIR/gate-block.json"
548
+ local gate_tmp="${gate_file}.tmp"
549
+ local timestamp
550
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
551
+ local failures_json
552
+ failures_json=$(_FAILURES="$failures" python3 -c "
553
+ import json, os
554
+ items = os.environ['_FAILURES'].split('|')
555
+ print(json.dumps(items))
556
+ " 2>/dev/null || echo '[]')
557
+ local critical_count
558
+ critical_count=$(_FAILURES="$failures" python3 -c "
559
+ import os
560
+ print(len(os.environ['_FAILURES'].split('|')))
561
+ " 2>/dev/null || echo '0')
562
+ cat > "$gate_tmp" << GATE_EOF
563
+ {
564
+ "status": "blocked",
565
+ "blocked": true,
566
+ "blocked_at": "$timestamp",
567
+ "iteration": ${ITERATION_COUNT:-0},
568
+ "reason": "critical_checklist_failures",
569
+ "critical_failures": $critical_count,
570
+ "failures": $failures_json
571
+ }
572
+ GATE_EOF
573
+ mv "$gate_tmp" "$gate_file"
574
+ return 1
575
+ fi
576
+
577
+ # Gate passes
578
+ if [ -f "$COUNCIL_STATE_DIR/gate-block.json" ]; then
579
+ rm -f "$COUNCIL_STATE_DIR/gate-block.json"
580
+ fi
581
+ return 0
464
582
  }
465
583
 
466
584
  #===============================================================================
@@ -1019,6 +1137,15 @@ council_evaluate() {
1019
1137
 
1020
1138
  log_info "Running council evaluation pipeline (round $ITERATION_COUNT)..."
1021
1139
 
1140
+ # Phase 4: Re-verify checklist for fresh data
1141
+ council_reverify_checklist
1142
+
1143
+ # Phase 4: Hard gate check - block if critical checklist items failing
1144
+ if ! council_checklist_gate; then
1145
+ log_info "[Council] Completion blocked by checklist hard gate"
1146
+ return 1 # CONTINUE - can't complete with critical failures
1147
+ fi
1148
+
1022
1149
  # Step 1: Aggregate votes from all members
1023
1150
  local aggregate_result
1024
1151
  aggregate_result=$(council_aggregate_votes)
@@ -1082,8 +1209,8 @@ council_should_stop() {
1082
1209
  return 1 # Not time to check yet
1083
1210
  fi
1084
1211
 
1085
- # Run the council vote
1086
- if council_vote; then
1212
+ # Run the council evaluation (includes hard gate + aggregate votes + devil's advocate)
1213
+ if council_evaluate; then
1087
1214
  log_header "COMPLETION COUNCIL: PROJECT APPROVED"
1088
1215
  log_info "The council has determined this project is complete."
1089
1216
 
package/autonomy/loki CHANGED
@@ -820,6 +820,14 @@ cmd_resume() {
820
820
 
821
821
  # Show current status
822
822
  cmd_status() {
823
+ # Check for --json flag
824
+ while [[ $# -gt 0 ]]; do
825
+ case "$1" in
826
+ --json) cmd_status_json; return $? ;;
827
+ *) shift ;;
828
+ esac
829
+ done
830
+
823
831
  require_jq
824
832
 
825
833
  if [ ! -d "$LOKI_DIR" ]; then
@@ -3123,7 +3131,7 @@ cmd_api() {
3123
3131
  if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
3124
3132
  uvicorn_args="$uvicorn_args --ssl-certfile ${LOKI_TLS_CERT} --ssl-keyfile ${LOKI_TLS_KEY}"
3125
3133
  fi
3126
- LOKI_DIR="$LOKI_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
3134
+ LOKI_DIR="$LOKI_DIR" PYTHONPATH="$SKILL_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
3127
3135
  local new_pid=$!
3128
3136
  echo "$new_pid" > "$pid_file"
3129
3137
 
File without changes
File without changes
@@ -158,7 +158,9 @@ checklist_summary() {
158
158
  return 0
159
159
  fi
160
160
 
161
- _CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE" python3 -c "
161
+ _CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE" \
162
+ _CHECKLIST_WAIVERS="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json" \
163
+ python3 -c "
162
164
  import json, sys, os
163
165
  try:
164
166
  fpath = os.environ.get('_CHECKLIST_RESULTS', '')
@@ -168,18 +170,39 @@ try:
168
170
  verified = s.get('verified', 0)
169
171
  failing = s.get('failing', 0)
170
172
  pending = s.get('pending', 0)
173
+
174
+ # Load waivers
175
+ waived_ids = set()
176
+ waivers_path = os.environ.get('_CHECKLIST_WAIVERS', '')
177
+ if waivers_path and os.path.exists(waivers_path):
178
+ try:
179
+ with open(waivers_path) as wf:
180
+ wdata = json.load(wf)
181
+ for w in wdata.get('waivers', []):
182
+ if w.get('active', True):
183
+ waived_ids.add(w['item_id'])
184
+ except Exception:
185
+ pass
186
+
187
+ # Count waived items and adjust failing list
188
+ waived_count = 0
171
189
  if total == 0:
172
190
  print('')
173
191
  else:
174
192
  failing_items = []
175
193
  for cat in data.get('categories', []):
176
194
  for item in cat.get('items', []):
195
+ item_id = item.get('id', '')
196
+ if item_id in waived_ids:
197
+ waived_count += 1
198
+ continue
177
199
  if item.get('status') == 'failing' and item.get('priority') in ('critical', 'major'):
178
200
  failing_items.append(item.get('title', item.get('id', '?')))
179
201
  detail = ''
180
202
  if failing_items:
181
203
  detail = ' FAILING: ' + ', '.join(failing_items[:5])
182
- print(f'{verified}/{total} verified, {failing} failing, {pending} pending.{detail}')
204
+ waived_str = f', {waived_count} waived' if waived_count > 0 else ''
205
+ print(f'{verified}/{total} verified, {failing} failing{waived_str}, {pending} pending.{detail}')
183
206
  except Exception:
184
207
  print('', file=sys.stderr)
185
208
  " 2>/dev/null || echo ""
@@ -221,3 +244,141 @@ except Exception:
221
244
  " 2>/dev/null || echo "Checklist data unavailable"
222
245
  } >> "${evidence_file:-/dev/stdout}"
223
246
  }
247
+
248
+ #===============================================================================
249
+ # Waiver Support (Phase 4)
250
+ #===============================================================================
251
+
252
+ # Load waivers from .loki/checklist/waivers.json
253
+ # Returns waived item IDs (one per line) to stdout
254
+ checklist_waiver_load() {
255
+ local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
256
+ if [ ! -f "$waivers_file" ]; then
257
+ return 0
258
+ fi
259
+ _WAIVERS_FILE="$waivers_file" python3 -c "
260
+ import json, sys, os
261
+ try:
262
+ waivers_file = os.environ['_WAIVERS_FILE']
263
+ with open(waivers_file) as f:
264
+ waivers = json.load(f)
265
+ for w in waivers.get('waivers', []):
266
+ if w.get('active', True):
267
+ print(w['item_id'])
268
+ except Exception:
269
+ pass
270
+ " 2>/dev/null || true
271
+ }
272
+
273
+ # Add a waiver for a checklist item
274
+ # Usage: checklist_waiver_add <item_id> <reason> [waived_by]
275
+ checklist_waiver_add() {
276
+ local item_id="${1:?item_id required}"
277
+ local reason="${2:?reason required}"
278
+ local waived_by="${3:-manual}"
279
+ local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
280
+ local timestamp
281
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
282
+
283
+ _WAIVERS_FILE="$waivers_file" python3 -c "
284
+ import json, os, sys
285
+
286
+ waivers_file = os.environ['_WAIVERS_FILE']
287
+ item_id = sys.argv[1]
288
+ reason = sys.argv[2]
289
+ waived_by = sys.argv[3]
290
+ timestamp = sys.argv[4]
291
+
292
+ # Load existing or create new
293
+ waivers = {'waivers': []}
294
+ if os.path.exists(waivers_file):
295
+ try:
296
+ with open(waivers_file) as f:
297
+ waivers = json.load(f)
298
+ except (json.JSONDecodeError, IOError):
299
+ pass
300
+
301
+ # Check for duplicate
302
+ for w in waivers.get('waivers', []):
303
+ if w.get('item_id') == item_id and w.get('active', True):
304
+ print(f'Waiver already exists for {item_id}')
305
+ sys.exit(0)
306
+
307
+ # Add new waiver
308
+ waivers.setdefault('waivers', []).append({
309
+ 'item_id': item_id,
310
+ 'reason': reason,
311
+ 'waived_by': waived_by,
312
+ 'waived_at': timestamp,
313
+ 'active': True
314
+ })
315
+
316
+ # Atomic write
317
+ tmp = waivers_file + '.tmp'
318
+ with open(tmp, 'w') as f:
319
+ json.dump(waivers, f, indent=2)
320
+ os.replace(tmp, waivers_file)
321
+ print(f'Waiver added for {item_id}')
322
+ " "$item_id" "$reason" "$waived_by" "$timestamp" 2>/dev/null
323
+ }
324
+
325
+ # Remove (deactivate) a waiver for a checklist item
326
+ # Usage: checklist_waiver_remove <item_id>
327
+ checklist_waiver_remove() {
328
+ local item_id="${1:?item_id required}"
329
+ local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
330
+
331
+ if [ ! -f "$waivers_file" ]; then
332
+ echo "No waivers file found"
333
+ return 1
334
+ fi
335
+
336
+ _WAIVERS_FILE="$waivers_file" python3 -c "
337
+ import json, os, sys
338
+
339
+ waivers_file = os.environ['_WAIVERS_FILE']
340
+ item_id = sys.argv[1]
341
+
342
+ with open(waivers_file) as f:
343
+ waivers = json.load(f)
344
+
345
+ found = False
346
+ for w in waivers.get('waivers', []):
347
+ if w.get('item_id') == item_id and w.get('active', True):
348
+ w['active'] = False
349
+ found = True
350
+
351
+ if not found:
352
+ print(f'No active waiver found for {item_id}')
353
+ sys.exit(1)
354
+
355
+ tmp = waivers_file + '.tmp'
356
+ with open(tmp, 'w') as f:
357
+ json.dump(waivers, f, indent=2)
358
+ os.replace(tmp, waivers_file)
359
+ print(f'Waiver removed for {item_id}')
360
+ " "$item_id" 2>/dev/null
361
+ }
362
+
363
+ # List all active waivers
364
+ checklist_waiver_list() {
365
+ local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
366
+
367
+ if [ ! -f "$waivers_file" ]; then
368
+ echo "No waivers configured"
369
+ return 0
370
+ fi
371
+
372
+ _WAIVERS_FILE="$waivers_file" python3 -c "
373
+ import json, os
374
+ waivers_file = os.environ['_WAIVERS_FILE']
375
+ with open(waivers_file) as f:
376
+ waivers = json.load(f)
377
+ active = [w for w in waivers.get('waivers', []) if w.get('active', True)]
378
+ if not active:
379
+ print('No active waivers')
380
+ else:
381
+ for w in active:
382
+ print(f\" {w['item_id']}: {w.get('reason', 'no reason')} (by {w.get('waived_by', 'unknown')} at {w.get('waived_at', '?')})\")
383
+ " 2>/dev/null
384
+ }
package/autonomy/run.sh CHANGED
@@ -698,7 +698,7 @@ print(json.dumps(event))
698
698
  if [ -z "$json_event" ]; then
699
699
  # Escape quotes and special chars for JSON
700
700
  local escaped_data
701
- escaped_data=$(echo "$event_data" | sed 's/"/\\"/g' | tr -d '\n')
701
+ escaped_data=$(printf '%s' "$event_data" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n')
702
702
  json_event="{\"timestamp\":\"$timestamp\",\"type\":\"$event_type\",\"data\":\"$escaped_data\"}"
703
703
  fi
704
704
 
@@ -733,8 +733,8 @@ emit_event_json() {
733
733
  if [[ "$value" =~ ^[0-9]+$ ]] || [[ "$value" =~ ^(true|false|null)$ ]]; then
734
734
  json_data+="\"$key\":$value"
735
735
  else
736
- # Escape quotes in value
737
- value=$(echo "$value" | sed 's/"/\\"/g')
736
+ # Escape backslashes, quotes, and special chars in value
737
+ value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
738
738
  json_data+="\"$key\":\"$value\""
739
739
  fi
740
740
  shift
@@ -3511,8 +3511,10 @@ update_agents_state() {
3511
3511
 
3512
3512
  agents_json="${agents_json}]"
3513
3513
 
3514
- # Write aggregated data
3515
- echo "$agents_json" > "$output_file"
3514
+ # Write aggregated data (atomic via temp file + mv)
3515
+ local tmp_file="${output_file}.tmp.$$"
3516
+ echo "$agents_json" > "$tmp_file"
3517
+ mv -f "$tmp_file" "$output_file" 2>/dev/null || rm -f "$tmp_file"
3516
3518
  }
3517
3519
 
3518
3520
  #===============================================================================
@@ -5874,7 +5876,7 @@ save_state() {
5874
5876
  "status": "$status",
5875
5877
  "lastExitCode": $exit_code,
5876
5878
  "lastRun": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
5877
- "prdPath": "${PRD_PATH:-}",
5879
+ "prdPath": "$(printf '%s' "${PRD_PATH:-}" | sed 's/\\/\\\\/g; s/"/\\"/g')",
5878
5880
  "pid": $$,
5879
5881
  "maxRetries": $MAX_RETRIES,
5880
5882
  "baseWait": $BASE_WAIT
@@ -6843,7 +6845,9 @@ check_human_intervention() {
6843
6845
  if [ -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED" ]; then
6844
6846
  log_info "Council force-review requested from dashboard"
6845
6847
  rm -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED"
6846
- if type council_vote &>/dev/null && council_vote; then
6848
+ if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
6849
+ log_info "Council force-review: blocked by checklist hard gate"
6850
+ elif type council_vote &>/dev/null && council_vote; then
6847
6851
  log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
6848
6852
  # BUG #17 fix: Write COMPLETED marker, generate council report, and
6849
6853
  # run memory consolidation (matching the normal council approval path
@@ -7452,6 +7456,9 @@ main() {
7452
7456
  audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
7453
7457
 
7454
7458
  # Cleanup
7459
+ if type app_runner_cleanup &>/dev/null; then
7460
+ app_runner_cleanup
7461
+ fi
7455
7462
  stop_dashboard
7456
7463
  stop_status_monitor
7457
7464
  rm -f .loki/loki.pid 2>/dev/null
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.46.0"
10
+ __version__ = "5.48.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: