loki-mode 7.18.1 → 7.18.3

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
@@ -61,6 +61,13 @@ if [ -f "$_LOKI_SCRIPT_DIR/tui.sh" ]; then
61
61
  source "$_LOKI_SCRIPT_DIR/tui.sh"
62
62
  fi
63
63
 
64
+ # Crash-reporting helpers (provides loki_collection_enabled, used by
65
+ # cmd_telemetry status and cmd_crash). Self-guarded against double-source.
66
+ if [ -f "$_LOKI_SCRIPT_DIR/crash.sh" ]; then
67
+ # shellcheck source=crash.sh
68
+ source "$_LOKI_SCRIPT_DIR/crash.sh"
69
+ fi
70
+
64
71
  # Resolve the script's real path (handles symlinks)
65
72
  resolve_script_path() {
66
73
  local script="$1"
@@ -502,12 +509,19 @@ show_help() {
502
509
  echo ""
503
510
  echo "Usage: loki <command> [options]"
504
511
  echo ""
512
+ echo "New here? Try one of these first:"
513
+ echo " loki doctor Check your setup is ready (instant)"
514
+ echo " loki quick \"add a health endpoint\" One small task, start to finish"
515
+ echo " loki demo Build a sample todo app end to end (real run)"
516
+ echo " loki start ./prd.md Build from a spec (PRD file, GitHub issue, or no arg)"
517
+ echo " Docs: https://github.com/asklokesh/loki-mode | Report a problem: loki crash"
518
+ echo ""
505
519
  echo "Commands:"
506
520
  echo " start [PRD|ISSUE] Start Loki Mode (PRD file, issue ref, or no arg)"
507
521
  echo " run <issue> (deprecated) Alias for 'loki start <issue-ref>'"
508
522
  echo " quick \"task\" Quick single-task mode (lightweight, 3 iterations max)"
509
523
  echo " monitor [path] Monitor Docker Compose services with auto-fix (v6.67.0)"
510
- echo " demo Run interactive demo (~60s simulated session)"
524
+ echo " demo Build a sample todo app end to end (real run)"
511
525
  echo " init [name] Project scaffolding with 21 PRD templates"
512
526
  echo " issue <url|num> (deprecated) Use 'loki start <issue-ref>' instead"
513
527
  echo " watch [prd] Auto-rerun on PRD file changes (v6.33.0)"
@@ -611,7 +625,7 @@ show_help() {
611
625
  echo " loki start --bg ./prd.md # Start in background"
612
626
  echo " loki start --parallel ./prd.md # Parallel mode (git worktrees)"
613
627
  echo " loki quick \"add dark mode\" # Single-task mode (3 iters max)"
614
- echo " loki demo # 60-second interactive demo"
628
+ echo " loki demo # Build a sample todo app end to end"
615
629
  echo " loki init -t saas-starter # Scaffold from template"
616
630
  echo " loki template install <src> # Install a community PRD template"
617
631
  echo ""
@@ -13472,6 +13486,9 @@ main() {
13472
13486
  report)
13473
13487
  cmd_report "$@"
13474
13488
  ;;
13489
+ crash)
13490
+ cmd_crash "$@"
13491
+ ;;
13475
13492
  share)
13476
13493
  cmd_share "$@"
13477
13494
  ;;
@@ -17943,6 +17960,201 @@ for line in sys.stdin:
17943
17960
  }
17944
17961
 
17945
17962
  # Telemetry management (v6.7.0)
17963
+ cmd_crash() {
17964
+ # Phase 0: local-only crash report inspection. NO network egress.
17965
+ #
17966
+ # PARITY: the user-facing strings here MUST match the TS canonical command
17967
+ # at loki-ts/src/commands/crash.ts byte-for-byte (ignoring color codes).
17968
+ # The whole subcommand body is implemented in one Python helper so the
17969
+ # behavior (filename-id resolution, "-" for missing fields, JSON shape,
17970
+ # URL encoding, exit codes) cannot drift from the TS route.
17971
+ #
17972
+ # Subcommands:
17973
+ # loki crash list .loki/crash/*.json reports
17974
+ # loki crash show <id> pretty-print one scrubbed report
17975
+ # loki crash submit [<id>] print the scrubbed payload + a prefilled
17976
+ # GitHub issue URL for MANUAL submission
17977
+ _LOKI_CRASH_DIR=".loki/crash" _LOKI_CRASH_SUB="${1-}" _LOKI_CRASH_ARG="${2-}" \
17978
+ python3 -c '
17979
+ import json, os, sys, urllib.parse
17980
+
17981
+ # Colors: honor NO_COLOR (https://no-color.org) exactly like the TS route
17982
+ # (loki-ts/src/util/colors.ts). Codes match autonomy/loki:25-32 byte-for-byte.
17983
+ _no_color = len(os.environ.get("NO_COLOR", "")) > 0
17984
+ def _c(code):
17985
+ return "" if _no_color else code
17986
+ RED = _c("\x1b[0;31m")
17987
+ GREEN = _c("\x1b[0;32m")
17988
+ YELLOW = _c("\x1b[1;33m")
17989
+ CYAN = _c("\x1b[0;36m")
17990
+ BOLD = _c("\x1b[1m")
17991
+ NC = _c("\x1b[0m")
17992
+
17993
+ ISSUE_BASE = "https://github.com/asklokesh/loki-mode/issues/new"
17994
+ CRASH_DIR = os.environ["_LOKI_CRASH_DIR"]
17995
+ SUB = os.environ.get("_LOKI_CRASH_SUB", "")
17996
+ ARG = os.environ.get("_LOKI_CRASH_ARG", "")
17997
+
17998
+ HELP = (
17999
+ BOLD + "loki crash" + NC + " - inspect and manually submit local crash reports\n"
18000
+ "\n"
18001
+ "Usage: loki crash [subcommand] [args]\n"
18002
+ "\n"
18003
+ "Subcommands:\n"
18004
+ " (none) List crash reports in .loki/crash/\n"
18005
+ " show <id> Pretty-print one scrubbed crash report\n"
18006
+ " submit [<id>] Print the scrubbed payload and a prefilled GitHub\n"
18007
+ " issue URL for manual submission\n"
18008
+ "\n"
18009
+ "Crash reports are anonymous, scrubbed, and stored locally only. Nothing is\n"
18010
+ "sent automatically in this version. See docs/PRIVACY.md.\n"
18011
+ )
18012
+
18013
+ def s(v):
18014
+ return "-" if v is None else str(v)
18015
+
18016
+ def pad(text, w):
18017
+ return text if len(text) >= w else text + " " * (w - len(text))
18018
+
18019
+ def report_ids():
18020
+ if not os.path.isdir(CRASH_DIR):
18021
+ return []
18022
+ try:
18023
+ names = [e for e in os.listdir(CRASH_DIR)
18024
+ if e.endswith(".json") and os.path.isfile(os.path.join(CRASH_DIR, e))]
18025
+ except OSError:
18026
+ return []
18027
+ return sorted(n[:-len(".json")] for n in names)
18028
+
18029
+ def _safe_id(rid):
18030
+ # Reject path traversal: ids are bare filenames (no separators, no "..",
18031
+ # no leading separator). This guards the direct filename lookup; the
18032
+ # fingerprint-resolution loop only ever passes real listdir ids, which
18033
+ # always pass. Mirrors the identical rule in loki-ts crash.ts.
18034
+ if rid is None or rid == "":
18035
+ return False
18036
+ if "/" in rid or "\\" in rid or ".." in rid:
18037
+ return False
18038
+ if rid[0] in ("/", "\\"):
18039
+ return False
18040
+ return True
18041
+
18042
+ def read_report(rid):
18043
+ if not _safe_id(rid):
18044
+ return None
18045
+ p = os.path.join(CRASH_DIR, rid + ".json")
18046
+ if not os.path.exists(p):
18047
+ return None
18048
+ try:
18049
+ with open(p) as f:
18050
+ return json.load(f)
18051
+ except Exception:
18052
+ return {}
18053
+
18054
+ def resolve_report(arg):
18055
+ direct = read_report(arg)
18056
+ if direct is not None:
18057
+ return (arg, direct)
18058
+ for rid in report_ids():
18059
+ r = read_report(rid)
18060
+ if r is not None and str(r.get("fingerprint", "")) == arg:
18061
+ return (rid, r)
18062
+ return None
18063
+
18064
+ def dumps(obj):
18065
+ return json.dumps(obj, indent=2, ensure_ascii=False)
18066
+
18067
+ def list_crashes():
18068
+ ids = report_ids()
18069
+ if not ids:
18070
+ sys.stdout.write(YELLOW + "No crash reports found." + NC +
18071
+ " Nothing has been captured in .loki/crash/.\n")
18072
+ return 0
18073
+ sys.stdout.write(pad("ID", 40) + " " + pad("CAPTURED_AT", 22) + " ERROR_CLASS\n")
18074
+ for rid in ids:
18075
+ d = read_report(rid) or {}
18076
+ fp = s(d.get("fingerprint"))
18077
+ captured_at = s(d.get("captured_at"))
18078
+ error_class = s(d.get("error_class"))
18079
+ visible_id = fp if fp != "-" else rid
18080
+ sys.stdout.write(pad(visible_id, 40) + " " + pad(captured_at, 22) + " " + error_class + "\n")
18081
+ sys.stdout.write(
18082
+ "\n" + str(len(ids)) + " report(s). Run \x27loki crash show <id>\x27 to inspect, "
18083
+ "\x27loki crash submit\x27 to get a prefilled GitHub issue URL.\n")
18084
+ return 0
18085
+
18086
+ def show_crash(rid):
18087
+ if not rid:
18088
+ sys.stderr.write(RED + "Missing crash id." + NC + " Use \x27loki crash\x27 to list reports.\n")
18089
+ return 2
18090
+ resolved = resolve_report(rid)
18091
+ if resolved is None:
18092
+ sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
18093
+ sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
18094
+ return 1
18095
+ sys.stdout.write(dumps(resolved[1]) + "\n")
18096
+ return 0
18097
+
18098
+ def issue_url(report):
18099
+ error_class = s(report.get("error_class"))
18100
+ fp = s(report.get("fingerprint"))
18101
+ short_fp = fp[:12] if fp != "-" else "unknown"
18102
+ title = "crash: " + error_class + " (" + short_fp + ")"
18103
+ payload = dumps(report)
18104
+ body = "\n".join([
18105
+ "Anonymous crash report captured by Loki Mode (scrubbed, whitelist-only).",
18106
+ "",
18107
+ "Scrubbed payload:",
18108
+ "```json",
18109
+ payload,
18110
+ "```",
18111
+ "",
18112
+ "Nothing was sent automatically. This issue is submitted manually by me.",
18113
+ ])
18114
+ q = urllib.parse.urlencode({"title": title, "body": body})
18115
+ return ISSUE_BASE + "?" + q
18116
+
18117
+ def submit_crash(rid):
18118
+ if rid:
18119
+ target = resolve_report(rid)
18120
+ if target is None:
18121
+ sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
18122
+ sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
18123
+ return 1
18124
+ else:
18125
+ ids = report_ids()
18126
+ if not ids:
18127
+ sys.stdout.write(YELLOW + "No crash reports found." + NC + " Nothing to submit.\n")
18128
+ return 0
18129
+ last_id = ids[-1]
18130
+ target = (last_id, read_report(last_id) or {})
18131
+ sys.stdout.write(BOLD + "Scrubbed payload (this is the ENTIRE report):" + NC + "\n")
18132
+ sys.stdout.write(dumps(target[1]) + "\n\n")
18133
+ sys.stdout.write(YELLOW + "Nothing is sent automatically in this version." + NC +
18134
+ " Loki Mode never transmits crash data on its own.\n")
18135
+ sys.stdout.write("To submit manually, open this prefilled GitHub issue and review it first:\n\n")
18136
+ sys.stdout.write(" " + CYAN + issue_url(target[1]) + NC + "\n\n")
18137
+ sys.stdout.write(GREEN + "The payload above is exactly what the URL contains." + NC + "\n")
18138
+ sys.stdout.write("See docs/PRIVACY.md for what is and is not collected.\n")
18139
+ return 0
18140
+
18141
+ if SUB == "":
18142
+ sys.exit(list_crashes())
18143
+ elif SUB in ("--help", "-h", "help"):
18144
+ sys.stdout.write(HELP)
18145
+ sys.exit(0)
18146
+ elif SUB == "show":
18147
+ sys.exit(show_crash(ARG))
18148
+ elif SUB == "submit":
18149
+ sys.exit(submit_crash(ARG))
18150
+ else:
18151
+ sys.stderr.write(RED + "Unknown crash subcommand: " + SUB + NC + "\n")
18152
+ sys.stdout.write(HELP)
18153
+ sys.exit(2)
18154
+ '
18155
+ return $?
18156
+ }
18157
+
17946
18158
  cmd_telemetry() {
17947
18159
  local subcommand="${1:-status}"
17948
18160
  shift 2>/dev/null || true
@@ -18007,6 +18219,25 @@ try {
18007
18219
  echo -e " Saved: $saved_endpoint"
18008
18220
  fi
18009
18221
  fi
18222
+
18223
+ # Unified collection state (PostHog usage telemetry + crash reporting).
18224
+ # loki_collection_enabled is the single source of truth (crash.sh).
18225
+ echo ""
18226
+ if type loki_collection_enabled &>/dev/null; then
18227
+ if loki_collection_enabled; then
18228
+ echo -e " Collection: ${GREEN}enabled${NC} (anonymous diagnostics; turn off with: loki telemetry off)"
18229
+ else
18230
+ echo -e " Collection: ${YELLOW}disabled${NC} (re-enable with: loki telemetry on)"
18231
+ fi
18232
+ fi
18233
+
18234
+ # Count of pending local crash reports (.loki/crash/*.json).
18235
+ local crash_dir=".loki/crash"
18236
+ local pending=0
18237
+ if [ -d "$crash_dir" ]; then
18238
+ pending=$(find "$crash_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null | wc -l | tr -d '[:space:]')
18239
+ fi
18240
+ echo -e " Crash reports pending: ${pending:-0} (see: loki crash)"
18010
18241
  echo ""
18011
18242
  ;;
18012
18243
 
@@ -18074,26 +18305,28 @@ TELEM_DISABLE_PY
18074
18305
  echo " Unset LOKI_OTEL_ENDPOINT in your shell for immediate effect."
18075
18306
  ;;
18076
18307
 
18077
- stop)
18078
- # Persistent opt-out across all sessions
18308
+ stop|off)
18309
+ # Persistent opt-out across all sessions. This is the unified
18310
+ # opt-out: it gates BOTH PostHog usage telemetry AND crash reporting.
18079
18311
  local global_config="${HOME}/.loki/config"
18080
18312
  mkdir -p "${HOME}/.loki"
18081
18313
 
18082
18314
  # Remove existing TELEMETRY_DISABLED line if present, then add
18315
+ # (idempotent).
18083
18316
  if [ -f "$global_config" ]; then
18084
18317
  grep -v "^TELEMETRY_DISABLED=" "$global_config" > "${global_config}.tmp" 2>/dev/null || true
18085
18318
  mv "${global_config}.tmp" "$global_config"
18086
18319
  fi
18087
18320
  echo "TELEMETRY_DISABLED=true" >> "$global_config"
18088
18321
 
18089
- echo -e "${BOLD}Telemetry permanently disabled${NC}"
18322
+ echo -e "${BOLD}Telemetry and crash reporting disabled${NC}"
18090
18323
  echo ""
18091
18324
  echo " Opt-out saved to: $global_config"
18092
18325
  echo " This persists across all sessions and new runs."
18093
- echo " Run 'loki telemetry start' to re-enable."
18326
+ echo " Run 'loki telemetry on' to re-enable."
18094
18327
  ;;
18095
18328
 
18096
- start)
18329
+ start|on)
18097
18330
  # Remove persistent opt-out
18098
18331
  local global_config="${HOME}/.loki/config"
18099
18332
  if [ -f "$global_config" ]; then
@@ -18101,9 +18334,10 @@ TELEM_DISABLE_PY
18101
18334
  mv "${global_config}.tmp" "$global_config"
18102
18335
  fi
18103
18336
 
18104
- echo -e "${BOLD}Telemetry opt-out removed${NC}"
18337
+ echo -e "${BOLD}Telemetry and crash reporting re-enabled${NC}"
18105
18338
  echo ""
18106
- echo " Telemetry can now be enabled with 'loki telemetry enable [endpoint]'."
18339
+ echo " Anonymous diagnostics collection is on again."
18340
+ echo " OTEL tracing can be configured with 'loki telemetry enable [endpoint]'."
18107
18341
  ;;
18108
18342
 
18109
18343
  --help|-h|help)
@@ -18112,11 +18346,13 @@ TELEM_DISABLE_PY
18112
18346
  echo "Usage: loki telemetry <command>"
18113
18347
  echo ""
18114
18348
  echo "Commands:"
18115
- echo " status Show current telemetry config"
18349
+ echo " status Show OTEL config + collection state + pending crash reports"
18116
18350
  echo " enable [endpoint] Enable OTEL (default: http://localhost:4318)"
18117
18351
  echo " disable Disable OTEL for current project"
18118
- echo " stop Permanently opt out of telemetry across all sessions"
18119
- echo " start Remove persistent opt-out, re-enable telemetry"
18352
+ echo " off Opt out of all anonymous diagnostics (telemetry + crash)"
18353
+ echo " on Re-enable anonymous diagnostics"
18354
+ echo " stop Alias for 'off' (permanent opt-out across all sessions)"
18355
+ echo " start Alias for 'on' (remove persistent opt-out)"
18120
18356
  echo ""
18121
18357
  ;;
18122
18358
  *)
package/autonomy/run.sh CHANGED
@@ -651,6 +651,15 @@ if [ -f "$TELEMETRY_SCRIPT" ]; then
651
651
  source "$TELEMETRY_SCRIPT"
652
652
  fi
653
653
 
654
+ # Crash-reporting helpers (Phase 0: local-only, zero egress).
655
+ # Provides loki_collection_enabled (unified opt-out), loki_crash_capture,
656
+ # loki_crash_friction, loki_show_disclosure_once.
657
+ CRASH_SCRIPT="$SCRIPT_DIR/crash.sh"
658
+ if [ -f "$CRASH_SCRIPT" ]; then
659
+ # shellcheck source=crash.sh
660
+ source "$CRASH_SCRIPT"
661
+ fi
662
+
654
663
 
655
664
 
656
665
  # 2026 Research Enhancements (minimal additions)
@@ -1743,6 +1752,11 @@ invoke_with_timeout() {
1743
1752
  done
1744
1753
 
1745
1754
  log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
1755
+ # Crash friction (retry_loop): provider spawn exhausted all retries -- a
1756
+ # clear threshold (not a single retry). Best-effort, never blocks.
1757
+ if type loki_crash_friction &>/dev/null; then
1758
+ loki_crash_friction "retry_loop" "provider spawn failed after $((max_retries+1)) attempts" >/dev/null 2>&1 || true
1759
+ fi
1746
1760
  return 124
1747
1761
  }
1748
1762
 
@@ -6107,7 +6121,11 @@ track_gate_failure() {
6107
6121
  local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
6108
6122
  mkdir -p "$(dirname "$gate_file")"
6109
6123
 
6110
- _GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
6124
+ # IMPORTANT: this function's stdout IS its return value (callers do
6125
+ # count=$(track_gate_failure ...)). Capture the count first, then do any
6126
+ # side-effects with their stdout suppressed, then echo ONLY the count.
6127
+ local count
6128
+ count=$(_GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
6111
6129
  import json, os
6112
6130
  gate_file = os.environ['_GATE_FILE']
6113
6131
  gate_name = os.environ['_GATE_NAME']
@@ -6120,7 +6138,16 @@ counts[gate_name] = counts.get(gate_name, 0) + 1
6120
6138
  with open(gate_file, 'w') as f:
6121
6139
  json.dump(counts, f, indent=2)
6122
6140
  print(counts[gate_name])
6123
- " 2>/dev/null || echo "1"
6141
+ " 2>/dev/null || echo "1")
6142
+
6143
+ # Crash friction (gate_failure): fire exactly once at the threshold (3
6144
+ # consecutive failures) so a sustained failure does not re-fire every
6145
+ # iteration. Best-effort, stdout suppressed so the count stays clean.
6146
+ if [ "${count:-0}" -eq 3 ] 2>/dev/null && type loki_crash_friction &>/dev/null; then
6147
+ loki_crash_friction "gate_failure" "gate=${gate_name} consecutive=${count}" >/dev/null 2>&1 || true
6148
+ fi
6149
+
6150
+ echo "$count"
6124
6151
  }
6125
6152
 
6126
6153
  clear_gate_failure() {
@@ -8196,6 +8223,11 @@ attempt_provider_failover() {
8196
8223
  done
8197
8224
 
8198
8225
  log_warn "Failover: all providers in chain exhausted, falling back to retry"
8226
+ # Crash friction (rate_limit_loop): a clear threshold -- every provider in
8227
+ # the failover chain is rate-limited/unhealthy. Best-effort, never blocks.
8228
+ if type loki_crash_friction &>/dev/null; then
8229
+ loki_crash_friction "rate_limit_loop" "failover chain exhausted: ${FAILOVER_CHAIN}" >/dev/null 2>&1 || true
8230
+ fi
8199
8231
  return 1
8200
8232
  }
8201
8233
 
@@ -11989,6 +12021,23 @@ if __name__ == "__main__":
11989
12021
  "status=$([[ $exit_code -eq 0 ]] && echo ok || echo error)"
11990
12022
  fi
11991
12023
 
12024
+ # Crash capture (Phase 0: local-only, best-effort, never blocks).
12025
+ # Conservative: only on a genuine non-zero failure exit. Signal-induced
12026
+ # exits (130 SIGINT / 143 SIGTERM / 137 SIGKILL) are user/operator
12027
+ # interrupts, not crashes, so we skip them. Known conservatism tradeoff:
12028
+ # this fires once per iteration on any nonzero exit, so a long, repeatedly
12029
+ # failing run can accumulate multiple local reports under .loki/crash/.
12030
+ if [ "$exit_code" -ne 0 ] 2>/dev/null && \
12031
+ [ "$exit_code" -ne 130 ] && [ "$exit_code" -ne 143 ] && [ "$exit_code" -ne 137 ] && \
12032
+ type loki_crash_capture &>/dev/null; then
12033
+ loki_crash_capture \
12034
+ "IterationError" \
12035
+ "provider exited non-zero on iteration ${ITERATION_COUNT:-?}" \
12036
+ "$([ -f "$iter_output" ] && tail -c 16384 "$iter_output" 2>/dev/null || true)" \
12037
+ "${rarv_phase:-iteration}" \
12038
+ "$exit_code"
12039
+ fi
12040
+
11992
12041
  # PRD Checklist verification on interval (v5.44.0)
11993
12042
  if type checklist_should_verify &>/dev/null && checklist_should_verify; then
11994
12043
  checklist_verify
@@ -12914,6 +12963,11 @@ main() {
12914
12963
  trap cleanup INT TERM
12915
12964
  SESSION_START_EPOCH=$(date +%s)
12916
12965
 
12966
+ # First-run disclosure (shown once, before any work; best-effort).
12967
+ if type loki_show_disclosure_once &>/dev/null; then
12968
+ loki_show_disclosure_once
12969
+ fi
12970
+
12917
12971
  echo ""
12918
12972
  echo -e "${BOLD}${BLUE}"
12919
12973
  echo " ██╗ ██████╗ ██╗ ██╗██╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗"
@@ -7,8 +7,19 @@ LOKI_POSTHOG_HOST="${LOKI_TELEMETRY_ENDPOINT:-https://us.i.posthog.com}"
7
7
  LOKI_POSTHOG_KEY="phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
8
8
 
9
9
  _loki_telemetry_enabled() {
10
+ # Unified opt-out: these checks must mirror loki_collection_enabled in
11
+ # autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
12
+ # crash reporting.
13
+ # LOKI_TELEMETRY=off (case-insensitive)
14
+ local _telem_lower
15
+ _telem_lower="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
16
+ [ "$_telem_lower" = "off" ] && return 1
10
17
  [ "${LOKI_TELEMETRY_DISABLED:-}" = "true" ] && return 1
11
18
  [ "${DO_NOT_TRACK:-}" = "1" ] && return 1
19
+ # Persistent opt-out in ~/.loki/config
20
+ if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_DISABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
21
+ return 1
22
+ fi
12
23
  command -v curl >/dev/null 2>&1 || return 1
13
24
  return 0
14
25
  }
package/bin/loki CHANGED
@@ -116,11 +116,13 @@ fi
116
116
  # Two-token routes (provider show/list, memory list/index) match on the first
117
117
  # token only; the Bun dispatcher handles subcommand routing internally.
118
118
  case "${1:-}" in
119
- version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki)
119
+ version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki|crash)
120
120
  # v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
121
121
  # v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
122
122
  # v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
123
123
  # R4: trust added (visible trust trajectory; Bun route, bash fallback).
124
+ # crash added (Crash Reporting Phase 0: inspect/manually submit local
125
+ # scrubbed crash reports; Bun route, bash fallback).
124
126
  #
125
127
  # v7.8.2: emit the cli_command product-analytics event for Bun-routed
126
128
  # commands. The bash CLI fires this from autonomy/loki main(), but the
@@ -180,8 +180,22 @@ console.log('New here? Run `loki welcome` for a 30-second tour.');
180
180
  console.log('');
181
181
 
182
182
  // Anonymous install telemetry (fire-and-forget, silent)
183
+ // Unified opt-out: these checks mirror loki_collection_enabled in
184
+ // autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
185
+ // crash reporting.
186
+ function _lokiCollectionDisabled() {
187
+ if ((process.env.LOKI_TELEMETRY || '').toLowerCase() === 'off') return true;
188
+ if (process.env.LOKI_TELEMETRY_DISABLED === 'true') return true;
189
+ if (process.env.DO_NOT_TRACK === '1') return true;
190
+ try {
191
+ const cfg = path.join(homeDir, '.loki', 'config');
192
+ const lines = fs.readFileSync(cfg, 'utf8').split('\n');
193
+ if (lines.some((l) => l.startsWith('TELEMETRY_DISABLED=true'))) return true;
194
+ } catch {}
195
+ return false;
196
+ }
183
197
  try {
184
- if (process.env.LOKI_TELEMETRY_DISABLED !== 'true' && process.env.DO_NOT_TRACK !== '1') {
198
+ if (!_lokiCollectionDisabled()) {
185
199
  const https = require('https');
186
200
  const crypto = require('crypto');
187
201
  const idFile = path.join(homeDir, '.loki-telemetry-id');
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.18.1"
10
+ __version__ = "7.18.3"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -19,10 +19,25 @@ _POSTHOG_KEY = "phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
19
19
 
20
20
 
21
21
  def _is_enabled():
22
+ # Unified opt-out: these checks must mirror loki_collection_enabled in
23
+ # autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
24
+ # crash reporting.
25
+ if os.environ.get("LOKI_TELEMETRY", "").lower() == "off":
26
+ return False
22
27
  if os.environ.get("LOKI_TELEMETRY_DISABLED") == "true":
23
28
  return False
24
29
  if os.environ.get("DO_NOT_TRACK") == "1":
25
30
  return False
31
+ # Persistent opt-out in ~/.loki/config (matches the bash grep prefix
32
+ # semantics: any line beginning with TELEMETRY_DISABLED=true).
33
+ try:
34
+ config_path = Path.home() / ".loki" / "config"
35
+ if config_path.is_file():
36
+ for line in config_path.read_text().splitlines():
37
+ if line.startswith("TELEMETRY_DISABLED=true"):
38
+ return False
39
+ except Exception:
40
+ pass
26
41
  return True
27
42
 
28
43