loki-mode 7.47.0 → 7.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/SKILL.md +2 -2
- package/VERSION +1 -1
- 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/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');
|
package/dashboard/__init__.py
CHANGED
package/dashboard/telemetry.py
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"""Anonymous usage telemetry for Loki Mode dashboard.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Collection is OPT-IN and OFF by default. Nothing is sent unless the user
|
|
4
|
+
explicitly opts in, so a default install (including air-gapped, GDPR, and
|
|
5
|
+
FedRAMP deployments) never phones home.
|
|
6
|
+
|
|
7
|
+
Opt-in (one required): LOKI_TELEMETRY=on OR ~/.loki/config: TELEMETRY_ENABLED=true
|
|
8
|
+
Opt-out (always wins): LOKI_TELEMETRY=off / LOKI_TELEMETRY_DISABLED=true /
|
|
9
|
+
DO_NOT_TRACK=1 / ~/.loki/config: TELEMETRY_DISABLED=true
|
|
10
|
+
|
|
4
11
|
All calls are fire-and-forget, silent on failure, non-blocking.
|
|
5
12
|
"""
|
|
6
13
|
|
|
@@ -19,10 +26,20 @@ _POSTHOG_KEY = "phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
|
|
|
19
26
|
|
|
20
27
|
|
|
21
28
|
def _is_enabled():
|
|
22
|
-
# Unified
|
|
23
|
-
#
|
|
24
|
-
# crash
|
|
25
|
-
|
|
29
|
+
# Unified OPT-IN gate. Collection is OFF by default; enabled ONLY when the
|
|
30
|
+
# user has opted in AND has not also opted out. This precedence MUST mirror
|
|
31
|
+
# loki_collection_enabled in autonomy/crash.sh and _loki_telemetry_enabled
|
|
32
|
+
# in autonomy/telemetry.sh so one model gates BOTH PostHog usage telemetry
|
|
33
|
+
# and crash reporting.
|
|
34
|
+
#
|
|
35
|
+
# Precedence:
|
|
36
|
+
# 1. Any opt-out flag present -> False (hard kill, always wins)
|
|
37
|
+
# 2. Else any opt-in flag present -> True
|
|
38
|
+
# 3. Else (default) -> False (no egress)
|
|
39
|
+
telem = os.environ.get("LOKI_TELEMETRY", "").lower()
|
|
40
|
+
|
|
41
|
+
# --- 1. Opt-out always wins ---
|
|
42
|
+
if telem == "off":
|
|
26
43
|
return False
|
|
27
44
|
if os.environ.get("LOKI_TELEMETRY_DISABLED") == "true":
|
|
28
45
|
return False
|
|
@@ -30,15 +47,26 @@ def _is_enabled():
|
|
|
30
47
|
return False
|
|
31
48
|
# Persistent opt-out in ~/.loki/config (matches the bash grep prefix
|
|
32
49
|
# semantics: any line beginning with TELEMETRY_DISABLED=true).
|
|
50
|
+
config_enabled = False
|
|
33
51
|
try:
|
|
34
52
|
config_path = Path.home() / ".loki" / "config"
|
|
35
53
|
if config_path.is_file():
|
|
36
54
|
for line in config_path.read_text().splitlines():
|
|
37
55
|
if line.startswith("TELEMETRY_DISABLED=true"):
|
|
38
56
|
return False
|
|
57
|
+
if line.startswith("TELEMETRY_ENABLED=true"):
|
|
58
|
+
config_enabled = True
|
|
39
59
|
except Exception:
|
|
40
60
|
pass
|
|
41
|
-
|
|
61
|
+
|
|
62
|
+
# --- 2. Opt-in required to enable ---
|
|
63
|
+
if telem == "on":
|
|
64
|
+
return True
|
|
65
|
+
if config_enabled:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
# --- 3. Default: OFF ---
|
|
69
|
+
return False
|
|
42
70
|
|
|
43
71
|
|
|
44
72
|
def _get_distinct_id():
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.48.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -114,11 +114,18 @@ faster routed commands and forward-compat with v8.0.0.
|
|
|
114
114
|
- Installs the `loki` CLI binary to your PATH (`bin/loki` shim)
|
|
115
115
|
- Subsequent `loki setup-skill` creates symlinks at `~/.claude/skills/loki-mode`, `~/.codex/skills/loki-mode`
|
|
116
116
|
|
|
117
|
-
**
|
|
117
|
+
**Anonymous telemetry is OPT-IN and OFF by default.** A default `npm install`
|
|
118
|
+
sends nothing, so air-gapped and enterprise installs are safe out of the box. To
|
|
119
|
+
opt in to anonymous diagnostics, run `loki telemetry on` or set
|
|
120
|
+
`LOKI_TELEMETRY=on`. To make opting in impossible across a fleet, bake an
|
|
121
|
+
opt-out into your base image (opt-out always wins):
|
|
118
122
|
```bash
|
|
123
|
+
# Hard-disable everywhere (belt and suspenders; opt-out always wins):
|
|
119
124
|
LOKI_TELEMETRY_DISABLED=true npm install -g loki-mode
|
|
120
125
|
# Or set DO_NOT_TRACK=1
|
|
121
126
|
```
|
|
127
|
+
See [PRIVACY.md](./PRIVACY.md) for the exact data sent and the full opt-in /
|
|
128
|
+
opt-out model.
|
|
122
129
|
|
|
123
130
|
**Update:** `npm update -g loki-mode`
|
|
124
131
|
|
|
@@ -389,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
389
396
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
390
397
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
391
398
|
-v $(pwd):/workspace -w /workspace \
|
|
392
|
-
asklokesh/loki-mode:7.
|
|
399
|
+
asklokesh/loki-mode:7.48.0 start ./my-spec.md
|
|
393
400
|
```
|
|
394
401
|
|
|
395
402
|
##### docker compose + .env (no host install)
|