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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +85 -2
- package/autonomy/crash.sh +47 -21
- package/autonomy/loki +50 -27
- package/autonomy/run.sh +378 -1
- package/autonomy/spec-interrogation.sh +1 -0
- package/autonomy/telemetry.sh +28 -8
- package/bin/postinstall.js +22 -10
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_v2.py +258 -12
- package/dashboard/server.py +64 -10
- package/dashboard/telemetry.py +34 -6
- package/docs/INSTALLATION.md +10 -3
- package/docs/PRIVACY.md +82 -24
- package/loki-ts/dist/loki.js +30 -30
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
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
|
-
|
|
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
|
package/autonomy/telemetry.sh
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Anonymous usage telemetry for Loki Mode
|
|
3
|
-
#
|
|
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
|
|
11
|
-
#
|
|
12
|
-
# crash
|
|
13
|
-
#
|
|
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
|
-
|
|
24
|
-
|
|
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() {
|
package/bin/postinstall.js
CHANGED
|
@@ -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
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// crash
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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 (
|
|
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');
|