loki-mode 7.47.0 → 7.49.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/run.sh CHANGED
@@ -2751,6 +2751,113 @@ with os.fdopen(fd, 'w') as f:
2751
2751
  os.replace(tmp, out)
2752
2752
  " 2>/dev/null || true
2753
2753
 
2754
+ # ---- P3-5: run manifest / bill-of-materials: .loki/loki-run.json ----------
2755
+ # Best-effort, auditable + reproducible record of this run. Emitted from
2756
+ # build_completion_summary because that fires on EVERY terminal path
2757
+ # (complete / max_iterations / stopped / intervention / failed), including the
2758
+ # intervention/stopped calls that bypass emit_completion_summary. The whole
2759
+ # block is wrapped so a failure can NEVER abort the run.
2760
+ {
2761
+ # Portable sha256 (macOS shasum, Linux sha256sum). Echoes empty on miss.
2762
+ _loki_sha256() {
2763
+ local _f="$1"
2764
+ [ -f "$_f" ] || { printf ''; return 0; }
2765
+ if command -v shasum >/dev/null 2>&1; then
2766
+ shasum -a 256 "$_f" 2>/dev/null | awk '{print $1}'
2767
+ elif command -v sha256sum >/dev/null 2>&1; then
2768
+ sha256sum "$_f" 2>/dev/null | awk '{print $1}'
2769
+ else
2770
+ printf ''
2771
+ fi
2772
+ }
2773
+
2774
+ local _loki_ver
2775
+ _loki_ver="$(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "unknown")"
2776
+ local _spec_path="${PRD_PATH:-}"
2777
+ [ -z "$_spec_path" ] && _spec_path="none"
2778
+ local _spec_hash=""
2779
+ [ "$_spec_path" != "none" ] && _spec_hash="$(_loki_sha256 "$_spec_path")"
2780
+
2781
+ # Evidence files we reference + hash (existing artifacts, not new ones).
2782
+ local _ev_tests="$loki_dir/quality/test-results.json"
2783
+ local _ev_cov="$loki_dir/quality/coverage.json"
2784
+ local _ev_completion="$loki_dir/state/completion.json"
2785
+
2786
+ _LOKI_RM_OUT="$loki_dir/loki-run.json" \
2787
+ _LOKI_RM_VERSION="$_loki_ver" \
2788
+ _LOKI_RM_SPEC_PATH="$_spec_path" \
2789
+ _LOKI_RM_SPEC_HASH="$_spec_hash" \
2790
+ _LOKI_RM_PROVIDER="${PROVIDER_NAME:-${LOKI_PROVIDER:-claude}}" \
2791
+ _LOKI_RM_TIER="${CURRENT_TIER:-unknown}" \
2792
+ _LOKI_RM_OUTCOME="$outcome" \
2793
+ _LOKI_RM_BRANCH="$branch" \
2794
+ _LOKI_RM_START_SHA="$start_sha" \
2795
+ _LOKI_RM_HEAD_SHA="$head_sha" \
2796
+ _LOKI_RM_ITERS="${ITERATION_COUNT:-0}" \
2797
+ _LOKI_RM_TS="$ts" \
2798
+ _LOKI_RM_EV_TESTS="$_ev_tests" \
2799
+ _LOKI_RM_EV_TESTS_HASH="$(_loki_sha256 "$_ev_tests")" \
2800
+ _LOKI_RM_EV_COV="$_ev_cov" \
2801
+ _LOKI_RM_EV_COV_HASH="$(_loki_sha256 "$_ev_cov")" \
2802
+ _LOKI_RM_EV_COMPLETION="$_ev_completion" \
2803
+ _LOKI_RM_NODE="$(node --version 2>/dev/null || echo '')" \
2804
+ _LOKI_RM_PYTHON="$(python3 --version 2>&1 | awk '{print $2}' || echo '')" \
2805
+ _LOKI_RM_GIT="$(git --version 2>/dev/null | awk '{print $3}' || echo '')" \
2806
+ _LOKI_RM_BUN="$(bun --version 2>/dev/null || echo '')" \
2807
+ python3 -c "
2808
+ import json, os, tempfile
2809
+ out=os.environ['_LOKI_RM_OUT']
2810
+ def s(k): return os.environ.get(k,'')
2811
+ def i(k):
2812
+ try: return int(os.environ.get(k,'0'))
2813
+ except (TypeError, ValueError): return 0
2814
+ def ev(path_k, hash_k=None):
2815
+ p=s(path_k)
2816
+ rec={'path': p, 'exists': bool(p) and os.path.isfile(p)}
2817
+ if hash_k:
2818
+ h=s(hash_k)
2819
+ if h: rec['sha256']=h
2820
+ return rec
2821
+ manifest={
2822
+ 'schema': 'loki-run-manifest/v1',
2823
+ 'loki_version': s('_LOKI_RM_VERSION'),
2824
+ 'timestamp': s('_LOKI_RM_TS'),
2825
+ 'outcome': s('_LOKI_RM_OUTCOME'),
2826
+ 'iterations': i('_LOKI_RM_ITERS'),
2827
+ 'provider': s('_LOKI_RM_PROVIDER'),
2828
+ # Tier cycles per RARV iteration (R/A/R/V); this is the LAST tier set, not
2829
+ # the only tier used. Recorded honestly as last_tier, not a single 'model'.
2830
+ 'last_tier': s('_LOKI_RM_TIER'),
2831
+ 'spec': {
2832
+ 'path': s('_LOKI_RM_SPEC_PATH'),
2833
+ 'sha256': s('_LOKI_RM_SPEC_HASH') or None,
2834
+ },
2835
+ 'git': {
2836
+ 'branch': s('_LOKI_RM_BRANCH'),
2837
+ 'start_sha': s('_LOKI_RM_START_SHA') or None,
2838
+ 'head_sha': s('_LOKI_RM_HEAD_SHA') or None,
2839
+ },
2840
+ 'tool_versions': {
2841
+ 'node': s('_LOKI_RM_NODE') or None,
2842
+ 'python': s('_LOKI_RM_PYTHON') or None,
2843
+ 'git': s('_LOKI_RM_GIT') or None,
2844
+ 'bun': s('_LOKI_RM_BUN') or None,
2845
+ },
2846
+ 'evidence': {
2847
+ 'test_results': ev('_LOKI_RM_EV_TESTS','_LOKI_RM_EV_TESTS_HASH'),
2848
+ 'coverage': ev('_LOKI_RM_EV_COV','_LOKI_RM_EV_COV_HASH'),
2849
+ 'completion': ev('_LOKI_RM_EV_COMPLETION'),
2850
+ },
2851
+ }
2852
+ d=os.path.dirname(out)
2853
+ fd, tmp=tempfile.mkstemp(dir=d, suffix='.json')
2854
+ with os.fdopen(fd,'w') as f:
2855
+ json.dump(manifest, f, indent=2)
2856
+ os.replace(tmp, out)
2857
+ " 2>/dev/null || true
2858
+ unset -f _loki_sha256 2>/dev/null || true
2859
+ } || true
2860
+
2754
2861
  # ---- Short strings for the desktop notification --------------------------
2755
2862
  # Desktop body stays terse; full detail lives in COMPLETION.txt.
2756
2863
  _LOKI_SUMMARY_TITLE="$notify_title"
@@ -7061,6 +7168,150 @@ _loki_run_pytest_with_timeout() {
7061
7168
  (cd "$target_dir" && "${_to_cmd[@]}" pytest "$@" 2>&1)
7062
7169
  }
7063
7170
 
7171
+ # ============================================================================
7172
+ # P0-1 Fix A: real test-coverage MEASUREMENT (v7.47.0)
7173
+ #
7174
+ # enforce_test_coverage() runs the project's suite for PASS/FAIL only -- it must
7175
+ # NOT add --coverage to that run, because a missing coverage provider
7176
+ # (@vitest/coverage-v8, the `coverage` pkg, pytest-cov, cargo-llvm-cov) makes the
7177
+ # instrumented command exit nonzero for a TOOLING reason, which would flip
7178
+ # test_passed=false and BLOCK a project whose tests actually pass. That would
7179
+ # destroy the honest pass/fail pass-through. So measurement is a SEPARATE,
7180
+ # best-effort second pass that can NEVER change test_passed.
7181
+ #
7182
+ # Contract:
7183
+ # - Sets COVERAGE_MEASURED (true|false), COVERAGE_PCT (number or empty),
7184
+ # COVERAGE_TOOL (string), COVERAGE_REASON (why not measured).
7185
+ # - Tool absent / unsupported language -> measured=false, no number, NEVER block.
7186
+ # - Tests run a SECOND time here when instrumented; LOKI_COVERAGE_GATE=0 skips
7187
+ # this whole measurement pass (saves the double-run).
7188
+ #
7189
+ # Usage: measure_test_coverage <target_dir> <test_runner>
7190
+ # ============================================================================
7191
+ measure_test_coverage() {
7192
+ local target_dir="$1"
7193
+ local runner="$2"
7194
+ COVERAGE_MEASURED=false
7195
+ COVERAGE_PCT=""
7196
+ COVERAGE_TOOL="none"
7197
+ COVERAGE_REASON=""
7198
+
7199
+ local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
7200
+ local cov_dir="$target_dir/.loki/quality"
7201
+ mkdir -p "$cov_dir" 2>/dev/null || true
7202
+ # Native tool reports land on a tool-specific path so they never collide
7203
+ # with our normalized coverage.json.
7204
+ local pyc_json="$cov_dir/coverage-pytest.json"
7205
+
7206
+ case "$runner" in
7207
+ vitest|monorepo-vitest)
7208
+ COVERAGE_TOOL="vitest"
7209
+ (cd "$target_dir" && timeout "$gate_timeout" npx vitest run --coverage \
7210
+ --coverage.reporter=json-summary \
7211
+ --coverage.reportsDirectory=.loki/quality/vitest-cov >/dev/null 2>&1) || true
7212
+ local f="$target_dir/.loki/quality/vitest-cov/coverage-summary.json"
7213
+ if [ -f "$f" ]; then
7214
+ COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -c "
7215
+ import json, os, sys
7216
+ try:
7217
+ d=json.load(open(os.environ['_LOKI_COV_F']))
7218
+ print(d['total']['lines']['pct'])
7219
+ except Exception:
7220
+ sys.exit(1)
7221
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="vitest coverage-summary.json unparsable"
7222
+ else
7223
+ COVERAGE_REASON="vitest coverage provider absent (install @vitest/coverage-v8)"
7224
+ fi
7225
+ ;;
7226
+ jest)
7227
+ COVERAGE_TOOL="jest"
7228
+ (cd "$target_dir" && timeout "$gate_timeout" npx jest --coverage \
7229
+ --coverageReporters=json-summary \
7230
+ --coverageDirectory=.loki/quality/jest-cov --passWithNoTests >/dev/null 2>&1) || true
7231
+ local f="$target_dir/.loki/quality/jest-cov/coverage-summary.json"
7232
+ if [ -f "$f" ]; then
7233
+ COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -c "
7234
+ import json, os, sys
7235
+ try:
7236
+ d=json.load(open(os.environ['_LOKI_COV_F']))
7237
+ print(d['total']['lines']['pct'])
7238
+ except Exception:
7239
+ sys.exit(1)
7240
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="jest coverage-summary.json unparsable"
7241
+ else
7242
+ COVERAGE_REASON="jest coverage report absent"
7243
+ fi
7244
+ ;;
7245
+ pytest)
7246
+ COVERAGE_TOOL="pytest-cov"
7247
+ # pytest-cov is optional; only measure when the plugin is importable.
7248
+ if python3 -c "import pytest_cov" >/dev/null 2>&1; then
7249
+ rm -f "$pyc_json" 2>/dev/null || true
7250
+ _loki_run_pytest_with_timeout "$target_dir" \
7251
+ --cov --cov-report="json:$pyc_json" -q >/dev/null 2>&1 || true
7252
+ if [ -f "$pyc_json" ]; then
7253
+ COVERAGE_PCT=$(_LOKI_COV_F="$pyc_json" python3 -c "
7254
+ import json, os, sys
7255
+ try:
7256
+ d=json.load(open(os.environ['_LOKI_COV_F']))
7257
+ print(d['totals']['percent_covered'])
7258
+ except Exception:
7259
+ sys.exit(1)
7260
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="pytest coverage.json unparsable"
7261
+ else
7262
+ COVERAGE_REASON="pytest produced no coverage.json"
7263
+ fi
7264
+ else
7265
+ COVERAGE_REASON="pytest-cov not installed"
7266
+ fi
7267
+ ;;
7268
+ go-test)
7269
+ COVERAGE_TOOL="go-cover"
7270
+ local prof="$cov_dir/go-coverage.out"
7271
+ rm -f "$prof" 2>/dev/null || true
7272
+ (cd "$target_dir" && timeout "$gate_timeout" go test -coverprofile="$prof" ./... >/dev/null 2>&1) || true
7273
+ if [ -f "$prof" ]; then
7274
+ local total_line
7275
+ total_line=$(cd "$target_dir" && go tool cover -func="$prof" 2>/dev/null | tail -1)
7276
+ # "total: (statements) 87.5%"
7277
+ COVERAGE_PCT=$(printf '%s\n' "$total_line" | grep -oE '[0-9]+(\.[0-9]+)?%' | tail -1 | tr -d '%')
7278
+ if [ -n "$COVERAGE_PCT" ]; then
7279
+ COVERAGE_MEASURED=true
7280
+ else
7281
+ COVERAGE_REASON="go tool cover produced no total"
7282
+ fi
7283
+ else
7284
+ COVERAGE_REASON="go test produced no coverage profile"
7285
+ fi
7286
+ ;;
7287
+ cargo-test)
7288
+ COVERAGE_TOOL="cargo-llvm-cov"
7289
+ if cargo llvm-cov --version >/dev/null 2>&1; then
7290
+ local out
7291
+ out=$(cd "$target_dir" && timeout "$gate_timeout" cargo llvm-cov --json 2>/dev/null) || true
7292
+ if [ -n "$out" ]; then
7293
+ COVERAGE_PCT=$(_LOKI_COV_JSON="$out" python3 -c "
7294
+ import json, os, sys
7295
+ try:
7296
+ d=json.loads(os.environ['_LOKI_COV_JSON'])
7297
+ print(d['data'][0]['totals']['lines']['percent'])
7298
+ except Exception:
7299
+ sys.exit(1)
7300
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="cargo llvm-cov json unparsable"
7301
+ else
7302
+ COVERAGE_REASON="cargo llvm-cov produced no output"
7303
+ fi
7304
+ else
7305
+ COVERAGE_REASON="cargo-llvm-cov not installed"
7306
+ fi
7307
+ ;;
7308
+ *)
7309
+ COVERAGE_REASON="coverage not supported for runner '$runner'"
7310
+ ;;
7311
+ esac
7312
+ return 0
7313
+ }
7314
+
7064
7315
  enforce_test_coverage() {
7065
7316
  local loki_dir="${TARGET_DIR:-.}/.loki"
7066
7317
  local quality_dir="$loki_dir/quality"
@@ -7291,10 +7542,109 @@ TREOF
7291
7542
  # Finding #598: stamp the per-iteration freshness marker (see above).
7292
7543
  printf '%s\n' "${ITERATION_COUNT:-0}" > "$quality_dir/.test-results.iter" 2>/dev/null || true
7293
7544
 
7545
+ # ---- P0-1 Fix A: best-effort coverage MEASUREMENT (v7.47.0) --------------
7546
+ # Runs AFTER test_passed is decided. NEVER mutates test_passed (a coverage
7547
+ # tooling failure must not flip a green suite to red). Writes a normalized
7548
+ # .loki/quality/coverage.json with honest measured/pct/reason. Blocks the
7549
+ # gate (coverage_block=true) ONLY when measurable AND below threshold AND
7550
+ # LOKI_ENFORCE_COVERAGE=1. A coverage block is distinct from a tests-red
7551
+ # block: it does NOT set TESTS_FAILED and does NOT remove unit-tests.pass.
7552
+ #
7553
+ # Knob semantics (measurement is OPT-IN: it re-runs the suite instrumented,
7554
+ # so for an autonomous loop iterating many times it is off unless requested):
7555
+ # default (unset) -> skip measurement entirely (no double-run).
7556
+ # LOKI_COVERAGE_GATE=1 -> measure + record + warn, never block.
7557
+ # LOKI_ENFORCE_COVERAGE=1 -> implies measurement; measurable + below
7558
+ # LOKI_MIN_COVERAGE -> BLOCK.
7559
+ # tool absent / unsupported -> record measured:false, never block.
7560
+ local coverage_block=false
7561
+ if [ "${LOKI_COVERAGE_GATE:-0}" != "0" ] || [ "${LOKI_ENFORCE_COVERAGE:-0}" = "1" ]; then
7562
+ COVERAGE_MEASURED=false; COVERAGE_PCT=""; COVERAGE_TOOL="none"; COVERAGE_REASON=""
7563
+ measure_test_coverage "${TARGET_DIR:-.}" "$test_runner" || true
7564
+
7565
+ local cov_enforced="${LOKI_ENFORCE_COVERAGE:-0}"
7566
+ local cov_below=false
7567
+ if [ "$COVERAGE_MEASURED" = "true" ] && [ -n "$COVERAGE_PCT" ]; then
7568
+ # Float-safe compare via python3 (pct may be e.g. 87.5).
7569
+ if _LOKI_COV_PCT="$COVERAGE_PCT" _LOKI_COV_MIN="$min_coverage" python3 -c "
7570
+ import os, sys
7571
+ try:
7572
+ pct=float(os.environ['_LOKI_COV_PCT']); mn=float(os.environ['_LOKI_COV_MIN'])
7573
+ except Exception:
7574
+ sys.exit(2)
7575
+ sys.exit(0 if pct < mn else 1)
7576
+ " 2>/dev/null; then
7577
+ cov_below=true
7578
+ fi
7579
+ fi
7580
+ if [ "$COVERAGE_MEASURED" = "true" ] && [ "$cov_below" = "true" ] && [ "$cov_enforced" = "1" ]; then
7581
+ coverage_block=true
7582
+ fi
7583
+
7584
+ # Normalized coverage.json (single source of truth for coverage facts).
7585
+ _LOKI_COV_MEASURED="$COVERAGE_MEASURED" \
7586
+ _LOKI_COV_PCT="$COVERAGE_PCT" \
7587
+ _LOKI_COV_TOOL="$COVERAGE_TOOL" \
7588
+ _LOKI_COV_REASON="$COVERAGE_REASON" \
7589
+ _LOKI_COV_MIN="$min_coverage" \
7590
+ _LOKI_COV_ENFORCED="$cov_enforced" \
7591
+ _LOKI_COV_BLOCKED="$coverage_block" \
7592
+ _LOKI_COV_RUNNER="$test_runner" \
7593
+ _LOKI_COV_OUT="$quality_dir/coverage.json" \
7594
+ python3 -c "
7595
+ import json, os, tempfile
7596
+ out=os.environ['_LOKI_COV_OUT']
7597
+ measured = os.environ.get('_LOKI_COV_MEASURED','false') == 'true'
7598
+ pct_raw = os.environ.get('_LOKI_COV_PCT','')
7599
+ try:
7600
+ pct = float(pct_raw) if (measured and pct_raw != '') else None
7601
+ except ValueError:
7602
+ pct = None
7603
+ def b(v): return os.environ.get(v,'false') == 'true'
7604
+ def i(v):
7605
+ try: return int(float(os.environ.get(v,'0')))
7606
+ except (TypeError, ValueError): return 0
7607
+ rec = {
7608
+ 'measured': measured,
7609
+ 'pct': pct,
7610
+ 'tool': os.environ.get('_LOKI_COV_TOOL','none'),
7611
+ 'runner': os.environ.get('_LOKI_COV_RUNNER','none'),
7612
+ 'threshold': i('_LOKI_COV_MIN'),
7613
+ 'enforced': os.environ.get('_LOKI_COV_ENFORCED','0') == '1',
7614
+ 'blocked': b('_LOKI_COV_BLOCKED'),
7615
+ 'reason': os.environ.get('_LOKI_COV_REASON','') if not measured else '',
7616
+ 'timestamp': __import__('datetime').datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
7617
+ }
7618
+ d=os.path.dirname(out)
7619
+ fd, tmp=tempfile.mkstemp(dir=d, suffix='.json')
7620
+ with os.fdopen(fd,'w') as f:
7621
+ json.dump(rec, f, indent=2)
7622
+ os.replace(tmp, out)
7623
+ " 2>/dev/null || true
7624
+
7625
+ if [ "$COVERAGE_MEASURED" = "true" ]; then
7626
+ if [ "$coverage_block" = "true" ]; then
7627
+ log_warn "Coverage gate: ${COVERAGE_TOOL} measured ${COVERAGE_PCT}% < ${min_coverage}% (LOKI_ENFORCE_COVERAGE=1) -- BLOCK"
7628
+ elif [ "$cov_below" = "true" ]; then
7629
+ log_warn "Coverage: ${COVERAGE_TOOL} measured ${COVERAGE_PCT}% < ${min_coverage}% (warn only; set LOKI_ENFORCE_COVERAGE=1 to block)"
7630
+ else
7631
+ log_info "Coverage: ${COVERAGE_TOOL} measured ${COVERAGE_PCT}% (threshold ${min_coverage}%)"
7632
+ fi
7633
+ else
7634
+ log_info "Coverage: not measured (${COVERAGE_REASON:-unknown}); pass-through, not blocking"
7635
+ fi
7636
+ fi
7637
+
7294
7638
  if [ "$test_passed" = "true" ]; then
7295
7639
  touch "$quality_dir/unit-tests.pass"
7296
7640
  rm -f "$loki_dir/signals/TESTS_FAILED" 2>/dev/null || true
7297
7641
  log_info "Test suite gate: $test_runner passed"
7642
+ # Coverage block is distinct from tests-red: tests passed, but enforced
7643
+ # coverage is below threshold. Return nonzero to gate WITHOUT writing the
7644
+ # TESTS_FAILED signal or removing unit-tests.pass.
7645
+ if [ "$coverage_block" = "true" ]; then
7646
+ return 1
7647
+ fi
7298
7648
  return 0
7299
7649
  else
7300
7650
  rm -f "$quality_dir/unit-tests.pass"
@@ -11611,6 +11961,25 @@ build_prompt() {
11611
11961
  test_summary=$(python3 -c "import json; d=json.load(open('${TARGET_DIR:-.}/.loki/quality/test-results.json')); print(d.get('summary',''))" 2>/dev/null || echo "")
11612
11962
  [ -n "$test_summary" ] && gate_failure_context="${gate_failure_context}Tests: ${test_summary}. "
11613
11963
  fi
11964
+ # P0-1 Fix A: when a coverage block fired (LOKI_ENFORCE_COVERAGE=1 +
11965
+ # measurable + below threshold), give the agent the ACCURATE reason. The
11966
+ # generic test_coverage token plus a passing test summary would otherwise
11967
+ # read as a contradictory "fix the tests" when the tests actually passed
11968
+ # and it is coverage that is low. Surface coverage.json so the next
11969
+ # iteration writes MORE TESTS rather than chasing a phantom red suite.
11970
+ if [ -f "${TARGET_DIR:-.}/.loki/quality/coverage.json" ]; then
11971
+ local cov_summary
11972
+ cov_summary=$(_LOKI_GFC="${TARGET_DIR:-.}/.loki/quality/coverage.json" python3 -c "
11973
+ import json, os
11974
+ try:
11975
+ d=json.load(open(os.environ['_LOKI_GFC']))
11976
+ except Exception:
11977
+ raise SystemExit
11978
+ if d.get('blocked'):
11979
+ print('Coverage %s%% is below the %s%% threshold (tests PASS; add tests to raise line coverage, do not change passing assertions).' % (d.get('pct'), d.get('threshold')))
11980
+ " 2>/dev/null || echo "")
11981
+ [ -n "$cov_summary" ] && gate_failure_context="${gate_failure_context}${cov_summary} "
11982
+ fi
11614
11983
  gate_failure_context="${gate_failure_context}FIX THESE ISSUES BEFORE PROCEEDING WITH NEW WORK."
11615
11984
  fi
11616
11985
 
@@ -14282,7 +14651,15 @@ if __name__ == "__main__":
14282
14651
  local tc_count
14283
14652
  tc_count=$(track_gate_failure "test_coverage")
14284
14653
  gate_failures="${gate_failures}test_coverage,"
14285
- log_warn "Test suite gate FAILED ($tc_count consecutive) - must pass next iteration"
14654
+ # P0-1 Fix A: distinguish a coverage-only block (tests passed,
14655
+ # enforced coverage below threshold) from a genuine tests-red
14656
+ # block in the log so the operator is not misled.
14657
+ if [ -f "${TARGET_DIR:-.}/.loki/quality/coverage.json" ] && \
14658
+ python3 -c "import json,sys; sys.exit(0 if json.load(open('${TARGET_DIR:-.}/.loki/quality/coverage.json')).get('blocked') else 1)" 2>/dev/null; then
14659
+ log_warn "Test coverage gate BLOCKED ($tc_count consecutive) - tests pass but coverage below threshold (LOKI_ENFORCE_COVERAGE=1)"
14660
+ else
14661
+ log_warn "Test suite gate FAILED ($tc_count consecutive) - must pass next iteration"
14662
+ fi
14286
14663
  fi
14287
14664
  fi
14288
14665
  # BUG-ST-002: Check pause signal between quality gates (after test coverage)
@@ -280,6 +280,7 @@ spec_interrogation_classify_report() {
280
280
  # Made" section. Best-effort; runs even when no provider is available so degrade
281
281
  # still surfaces something. Usage: spec_ledger_fold_prd_observations [path]
282
282
  # ---------------------------------------------------------------------------
283
+ # shellcheck disable=SC2120 # optional [path] arg by design (see Usage above); callers pass none
283
284
  spec_ledger_fold_prd_observations() {
284
285
  local obs="${1:-${TARGET_DIR:-.}/.loki/prd-observations.md}"
285
286
  [ -f "$obs" ] || return 0
@@ -1,27 +1,47 @@
1
1
  #!/usr/bin/env bash
2
2
  # Anonymous usage telemetry for Loki Mode
3
- # Opt-out: LOKI_TELEMETRY_DISABLED=true or DO_NOT_TRACK=1
3
+ # Collection is OPT-IN and OFF by default. Nothing is sent unless the user opts
4
+ # in, so a default install never phones home (air-gapped / GDPR / FedRAMP safe).
5
+ # Opt-in: LOKI_TELEMETRY=on OR ~/.loki/config: TELEMETRY_ENABLED=true
6
+ # Opt-out (always wins): LOKI_TELEMETRY=off / LOKI_TELEMETRY_DISABLED=true /
7
+ # DO_NOT_TRACK=1 / ~/.loki/config: TELEMETRY_DISABLED=true
4
8
  # All calls are fire-and-forget, silent on failure, non-blocking
5
9
 
6
10
  LOKI_POSTHOG_HOST="${LOKI_TELEMETRY_ENDPOINT:-https://us.i.posthog.com}"
7
11
  LOKI_POSTHOG_KEY="phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
8
12
 
9
13
  _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
+ # Unified OPT-IN gate. Returns 0 (enabled) ONLY when the user opted in AND
15
+ # did not also opt out. Opt-out always wins; default is OFF. This precedence
16
+ # MUST mirror loki_collection_enabled in autonomy/crash.sh and _is_enabled in
17
+ # dashboard/telemetry.py so one model gates BOTH usage telemetry and crash
18
+ # reporting.
19
+ # 1. Any opt-out flag present -> 1 (hard kill, always wins)
20
+ # 2. Else any opt-in flag present -> 0
21
+ # 3. Else (default) -> 1 (no egress)
14
22
  local _telem_lower
15
23
  _telem_lower="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
24
+
25
+ # --- 1. Opt-out always wins ---
16
26
  [ "$_telem_lower" = "off" ] && return 1
17
27
  [ "${LOKI_TELEMETRY_DISABLED:-}" = "true" ] && return 1
18
28
  [ "${DO_NOT_TRACK:-}" = "1" ] && return 1
19
- # Persistent opt-out in ~/.loki/config
20
29
  if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_DISABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
21
30
  return 1
22
31
  fi
23
- command -v curl >/dev/null 2>&1 || return 1
24
- return 0
32
+
33
+ # --- 2. Opt-in required to enable ---
34
+ if [ "$_telem_lower" = "on" ]; then
35
+ command -v curl >/dev/null 2>&1 || return 1
36
+ return 0
37
+ fi
38
+ if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_ENABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
39
+ command -v curl >/dev/null 2>&1 || return 1
40
+ return 0
41
+ fi
42
+
43
+ # --- 3. Default: OFF ---
44
+ return 1
25
45
  }
26
46
 
27
47
  _loki_telemetry_id() {
@@ -179,23 +179,35 @@ console.log('');
179
179
  console.log('New here? Run `loki welcome` for a 30-second tour.');
180
180
  console.log('');
181
181
 
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;
182
+ // Anonymous install telemetry (fire-and-forget, silent).
183
+ // Collection is OPT-IN and OFF by default: a default `npm install` (including
184
+ // air-gapped, GDPR, and FedRAMP environments) sends NOTHING. This precedence
185
+ // mirrors loki_collection_enabled in autonomy/crash.sh, _is_enabled in
186
+ // dashboard/telemetry.py, and _loki_telemetry_enabled in autonomy/telemetry.sh.
187
+ // 1. Any opt-out flag present -> false (hard kill, always wins)
188
+ // 2. Else any opt-in flag present -> true
189
+ // 3. Else (default) -> false (no egress)
190
+ function _lokiCollectionEnabled() {
191
+ const telem = (process.env.LOKI_TELEMETRY || '').toLowerCase();
192
+ // 1. Opt-out always wins.
193
+ if (telem === 'off') return false;
194
+ if (process.env.LOKI_TELEMETRY_DISABLED === 'true') return false;
195
+ if (process.env.DO_NOT_TRACK === '1') return false;
196
+ let configEnabled = false;
190
197
  try {
191
198
  const cfg = path.join(homeDir, '.loki', 'config');
192
199
  const lines = fs.readFileSync(cfg, 'utf8').split('\n');
193
- if (lines.some((l) => l.startsWith('TELEMETRY_DISABLED=true'))) return true;
200
+ if (lines.some((l) => l.startsWith('TELEMETRY_DISABLED=true'))) return false;
201
+ if (lines.some((l) => l.startsWith('TELEMETRY_ENABLED=true'))) configEnabled = true;
194
202
  } catch {}
203
+ // 2. Opt-in required.
204
+ if (telem === 'on') return true;
205
+ if (configEnabled) return true;
206
+ // 3. Default: OFF.
195
207
  return false;
196
208
  }
197
209
  try {
198
- if (!_lokiCollectionDisabled()) {
210
+ if (_lokiCollectionEnabled()) {
199
211
  const https = require('https');
200
212
  const crypto = require('crypto');
201
213
  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.47.0"
10
+ __version__ = "7.49.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: