loki-mode 7.49.0 → 7.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/prd-analyzer.py +215 -1
- package/autonomy/prd-checklist.sh +315 -0
- package/autonomy/run.sh +350 -3
- package/autonomy/spec-interrogation.sh +224 -4
- package/autonomy/spec.sh +25 -16
- package/autonomy/verify.sh +108 -26
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +202 -21
- package/docs/INSTALLATION.md +2 -2
- package/docs/siem-integration.md +102 -0
- package/loki-ts/dist/loki.js +231 -230
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +203 -0
- package/mcp/tests/test_lsp_proxy.py +169 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/invariant-checks.md +109 -0
- package/src/audit/crosslink.js +413 -0
- package/src/audit/index.js +32 -0
- package/src/observability/siem-export.js +424 -0
- package/src/policies/cost.js +270 -1
package/autonomy/run.sh
CHANGED
|
@@ -1480,7 +1480,40 @@ check_policy() {
|
|
|
1480
1480
|
elif [ $exit_code -eq 2 ]; then
|
|
1481
1481
|
log_warn "Policy requires APPROVAL: $result"
|
|
1482
1482
|
audit_agent_action "policy_approval_required" "Policy requires approval" "enforcement=$enforcement_point"
|
|
1483
|
-
#
|
|
1483
|
+
# P3-3 (v7.51.0): honor the approval requirement when the operator has
|
|
1484
|
+
# opted into enforcement. This is OPT-IN and changes NOTHING for existing
|
|
1485
|
+
# users: the wait fires only when staged autonomy is on
|
|
1486
|
+
# (LOKI_STAGED_AUTONOMY=true) or the explicit
|
|
1487
|
+
# LOKI_POLICY_APPROVAL_ENFORCE=1 knob is set. Otherwise it stays advisory
|
|
1488
|
+
# (log + proceed), preserving the historical default behavior. The wait
|
|
1489
|
+
# reuses the same .loki/signals/ file-signal mechanism as staged-autonomy
|
|
1490
|
+
# plan approval (check_staged_autonomy), extended with a reject arm so an
|
|
1491
|
+
# operator can deny (deny == policy DENIED == return 1).
|
|
1492
|
+
if [ "$STAGED_AUTONOMY" = "true" ] || [ "${LOKI_POLICY_APPROVAL_ENFORCE:-0}" = "1" ]; then
|
|
1493
|
+
local _approve_sig=".loki/signals/POLICY_APPROVED"
|
|
1494
|
+
local _reject_sig=".loki/signals/POLICY_REJECTED"
|
|
1495
|
+
log_warn "Policy enforcement: waiting for approval at enforcement point '$enforcement_point'."
|
|
1496
|
+
log_warn " Approve: create $_approve_sig | Reject: create $_reject_sig"
|
|
1497
|
+
audit_agent_action "policy_approval_wait" "Waiting for policy approval signal" "enforcement=$enforcement_point"
|
|
1498
|
+
while [ ! -f "$_approve_sig" ] && [ ! -f "$_reject_sig" ]; do
|
|
1499
|
+
sleep 5
|
|
1500
|
+
done
|
|
1501
|
+
if [ -f "$_reject_sig" ]; then
|
|
1502
|
+
rm -f "$_reject_sig" "$_approve_sig" 2>/dev/null || true
|
|
1503
|
+
log_error "Policy REJECTED by operator at enforcement point '$enforcement_point'"
|
|
1504
|
+
audit_agent_action "policy_approval_rejected" "Operator rejected policy approval" "enforcement=$enforcement_point"
|
|
1505
|
+
emit_event_json "policy_denied" \
|
|
1506
|
+
"enforcement=$enforcement_point" \
|
|
1507
|
+
"result=operator_rejected"
|
|
1508
|
+
return 1
|
|
1509
|
+
fi
|
|
1510
|
+
rm -f "$_approve_sig" 2>/dev/null || true
|
|
1511
|
+
log_info "Policy approved by operator at enforcement point '$enforcement_point'; continuing."
|
|
1512
|
+
audit_agent_action "policy_approval_granted" "Operator approved policy" "enforcement=$enforcement_point"
|
|
1513
|
+
return 0
|
|
1514
|
+
fi
|
|
1515
|
+
# Default (no staged autonomy, no enforce knob): advisory only -- log and
|
|
1516
|
+
# proceed. This preserves the historical behavior for existing users.
|
|
1484
1517
|
return 0
|
|
1485
1518
|
fi
|
|
1486
1519
|
return 0
|
|
@@ -7038,6 +7071,130 @@ enforce_static_analysis() {
|
|
|
7038
7071
|
}
|
|
7039
7072
|
fi
|
|
7040
7073
|
|
|
7074
|
+
# C / C++ (P1-6: cppcheck is a standalone static analyzer that needs no
|
|
7075
|
+
# build system, headers, or compile flags, so it does not false-block on
|
|
7076
|
+
# missing includes the way a per-file `clang` compile would. The exit gate
|
|
7077
|
+
# fires only on `error` severity; style/warning/portability findings on WIP
|
|
7078
|
+
# code do not block. When cppcheck is absent we pass through honestly
|
|
7079
|
+
# (log, no block) rather than silently skipping.)
|
|
7080
|
+
local cfiles
|
|
7081
|
+
cfiles=$(echo "$changed_files" | grep -E '\.(c|cc|cpp|cxx|h|hpp|hxx)$' || true)
|
|
7082
|
+
if [ -n "$cfiles" ]; then
|
|
7083
|
+
local cabs=""
|
|
7084
|
+
for f in $cfiles; do
|
|
7085
|
+
[ -f "${TARGET_DIR:-.}/$f" ] && cabs="$cabs ${TARGET_DIR:-.}/$f"
|
|
7086
|
+
done
|
|
7087
|
+
if [ -n "$cabs" ]; then
|
|
7088
|
+
if command -v cppcheck &>/dev/null; then
|
|
7089
|
+
total_checked=$((total_checked + $(echo "$cabs" | wc -w)))
|
|
7090
|
+
# Default cppcheck reports ONLY error severity, so with
|
|
7091
|
+
# --error-exitcode=2 the gate returns 2 exclusively on an
|
|
7092
|
+
# error-severity finding. We deliberately do NOT pass
|
|
7093
|
+
# --enable=warning: that would make warning/style/portability
|
|
7094
|
+
# findings on incomplete WIP code block the iteration (verified:
|
|
7095
|
+
# a deref-then-null-check warning returns 2 under --enable=warning
|
|
7096
|
+
# but 0 under the default ruleset). Error severity only = honest
|
|
7097
|
+
# parity with the TS/shell `-S error` gates above.
|
|
7098
|
+
local cpp_out cpp_rc=0
|
|
7099
|
+
# shellcheck disable=SC2086
|
|
7100
|
+
cpp_out=$(cppcheck --quiet --error-exitcode=2 $cabs 2>&1) || cpp_rc=$?
|
|
7101
|
+
if [ "$cpp_rc" -eq 2 ]; then
|
|
7102
|
+
findings=$((findings + 1))
|
|
7103
|
+
details="${details}cppcheck (error severity): $(echo "$cpp_out" | tail -3 | tr '\n' ' '). "
|
|
7104
|
+
fi
|
|
7105
|
+
else
|
|
7106
|
+
log_info "Static analysis: cppcheck not on PATH, skipping C/C++ check (pass-through)"
|
|
7107
|
+
fi
|
|
7108
|
+
fi
|
|
7109
|
+
fi
|
|
7110
|
+
|
|
7111
|
+
# Kotlin (P1-6: ktlint and detekt are standalone, build-system-free linters.
|
|
7112
|
+
# Prefer ktlint; fall back to detekt. Absent -> honest pass-through.)
|
|
7113
|
+
#
|
|
7114
|
+
# ADVISORY ONLY (not blocking): unlike cppcheck/checkstyle which expose an
|
|
7115
|
+
# error-vs-style severity distinction, ktlint is a pure formatter -- every
|
|
7116
|
+
# finding it reports is a style/formatting issue and it exits nonzero on ANY
|
|
7117
|
+
# violation, with no CLI mode to fail only on error severity. detekt's failure
|
|
7118
|
+
# threshold is config-driven (maxIssues) and its findings are code smells, not
|
|
7119
|
+
# compiler errors; there is no stable CLI flag to fail only on error severity.
|
|
7120
|
+
# Per the gate principle (a new-language arm must NOT block on style/formatting,
|
|
7121
|
+
# consistent with cppcheck's error-exitcode-only and the JS/TS/Py `-S error`
|
|
7122
|
+
# gates), we run these linters as ADVISORY: report findings via log_warn and
|
|
7123
|
+
# the details string, but do NOT increment `findings` (no BLOCK). This avoids
|
|
7124
|
+
# false-blocking a WIP build on formatting. Absent -> honest pass-through.
|
|
7125
|
+
local kt_files
|
|
7126
|
+
kt_files=$(echo "$changed_files" | grep -E '\.(kt|kts)$' || true)
|
|
7127
|
+
if [ -n "$kt_files" ]; then
|
|
7128
|
+
local kt_abs=""
|
|
7129
|
+
for f in $kt_files; do
|
|
7130
|
+
[ -f "${TARGET_DIR:-.}/$f" ] && kt_abs="$kt_abs ${TARGET_DIR:-.}/$f"
|
|
7131
|
+
done
|
|
7132
|
+
if [ -n "$kt_abs" ]; then
|
|
7133
|
+
if command -v ktlint &>/dev/null; then
|
|
7134
|
+
total_checked=$((total_checked + $(echo "$kt_abs" | wc -w)))
|
|
7135
|
+
local kt_out
|
|
7136
|
+
# shellcheck disable=SC2086
|
|
7137
|
+
kt_out=$(cd "${TARGET_DIR:-.}" && ktlint $kt_files 2>&1) || {
|
|
7138
|
+
# Advisory: ktlint reports only style/formatting; warn, do not block.
|
|
7139
|
+
details="${details}ktlint advisory (style, non-blocking): $(echo "$kt_out" | tail -3 | tr '\n' ' '). "
|
|
7140
|
+
log_warn "Static analysis: ktlint reported style findings (advisory, non-blocking)"
|
|
7141
|
+
}
|
|
7142
|
+
elif command -v detekt &>/dev/null; then
|
|
7143
|
+
total_checked=$((total_checked + $(echo "$kt_abs" | wc -w)))
|
|
7144
|
+
local dt_out dt_input
|
|
7145
|
+
dt_input=$(echo "$kt_files" | tr ' \n' ',,' | sed 's/,*$//;s/^,*//')
|
|
7146
|
+
dt_out=$(cd "${TARGET_DIR:-.}" && detekt --input "$dt_input" 2>&1) || {
|
|
7147
|
+
# Advisory: detekt threshold is config-driven, findings are code
|
|
7148
|
+
# smells (no error-severity-only CLI mode); warn, do not block.
|
|
7149
|
+
details="${details}detekt advisory (code smell, non-blocking): $(echo "$dt_out" | tail -3 | tr '\n' ' '). "
|
|
7150
|
+
log_warn "Static analysis: detekt reported findings (advisory, non-blocking)"
|
|
7151
|
+
}
|
|
7152
|
+
else
|
|
7153
|
+
log_info "Static analysis: ktlint/detekt not on PATH, skipping Kotlin check (pass-through)"
|
|
7154
|
+
fi
|
|
7155
|
+
fi
|
|
7156
|
+
fi
|
|
7157
|
+
|
|
7158
|
+
# Java (P1-6: checkstyle is a pure static linter that needs no compile or
|
|
7159
|
+
# classpath, but it REQUIRES a config file. A per-file `javac` would
|
|
7160
|
+
# false-block on unresolved imports/classpath the way per-file tsc did, so
|
|
7161
|
+
# Java is gated on checkstyle-with-config only. Without a config we pass
|
|
7162
|
+
# through honestly. C# is deferred: roslyn analyzers and `dotnet build` need
|
|
7163
|
+
# a full project + restore, which cannot be auto-detected cleanly per-file.)
|
|
7164
|
+
local java_files
|
|
7165
|
+
java_files=$(echo "$changed_files" | grep -E '\.java$' || true)
|
|
7166
|
+
if [ -n "$java_files" ]; then
|
|
7167
|
+
local java_abs=""
|
|
7168
|
+
for f in $java_files; do
|
|
7169
|
+
[ -f "${TARGET_DIR:-.}/$f" ] && java_abs="$java_abs ${TARGET_DIR:-.}/$f"
|
|
7170
|
+
done
|
|
7171
|
+
if [ -n "$java_abs" ]; then
|
|
7172
|
+
local _cs_config=""
|
|
7173
|
+
for cfg in checkstyle.xml .checkstyle.xml config/checkstyle/checkstyle.xml google_checks.xml sun_checks.xml; do
|
|
7174
|
+
if [ -f "${TARGET_DIR:-.}/$cfg" ]; then _cs_config="${TARGET_DIR:-.}/$cfg"; break; fi
|
|
7175
|
+
done
|
|
7176
|
+
if command -v checkstyle &>/dev/null && [ -n "$_cs_config" ]; then
|
|
7177
|
+
total_checked=$((total_checked + $(echo "$java_abs" | wc -w)))
|
|
7178
|
+
local cs_out
|
|
7179
|
+
# checkstyle's exit code equals the count of audit events at
|
|
7180
|
+
# severity=error; warning/info violations are printed but do NOT
|
|
7181
|
+
# bump the exit code (verified against checkstyle CLI behavior).
|
|
7182
|
+
# So a nonzero exit means error-severity findings only -- this is
|
|
7183
|
+
# already error-gated like cppcheck (--error-exitcode) and the
|
|
7184
|
+
# JS/TS/Py `-S error` gates, and does NOT block on style/warning.
|
|
7185
|
+
# Whether a given rule is error vs warning is the user's explicit
|
|
7186
|
+
# choice in their checkstyle config, which we respect.
|
|
7187
|
+
# shellcheck disable=SC2086
|
|
7188
|
+
cs_out=$(cd "${TARGET_DIR:-.}" && checkstyle -c "$_cs_config" $java_files 2>&1) || {
|
|
7189
|
+
findings=$((findings + 1))
|
|
7190
|
+
details="${details}checkstyle (error severity): $(echo "$cs_out" | tail -3 | tr '\n' ' '). "
|
|
7191
|
+
}
|
|
7192
|
+
else
|
|
7193
|
+
log_info "Static analysis: checkstyle+config not available, skipping Java check (pass-through)"
|
|
7194
|
+
fi
|
|
7195
|
+
fi
|
|
7196
|
+
fi
|
|
7197
|
+
|
|
7041
7198
|
# Write results
|
|
7042
7199
|
cat > "$quality_dir/static-analysis.json" << SAFEOF
|
|
7043
7200
|
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","files_checked":$total_checked,"findings":$findings,"summary":"$details","pass":$([ $findings -eq 0 ] && echo "true" || echo "false")}
|
|
@@ -7633,6 +7790,55 @@ os.replace(tmp, out)
|
|
|
7633
7790
|
else
|
|
7634
7791
|
log_info "Coverage: not measured (${COVERAGE_REASON:-unknown}); pass-through, not blocking"
|
|
7635
7792
|
fi
|
|
7793
|
+
else
|
|
7794
|
+
# P3-5/coverage-honesty (v7.51.0): measurement is OPT-IN (it re-runs the
|
|
7795
|
+
# suite instrumented, which would double every test run -- a UX
|
|
7796
|
+
# regression for an autonomous loop). At default-off we deliberately do
|
|
7797
|
+
# NOT measure, but we STILL write a coverage fact so the run manifest /
|
|
7798
|
+
# reproducibility record always has one honest coverage shape. This is
|
|
7799
|
+
# the "missing-artifact" fix, not a hollow gate: measured=false, pct=null,
|
|
7800
|
+
# blocked=false, with an explicit reason. ZERO runtime (no instrumented
|
|
7801
|
+
# re-run). Reuses the EXACT python3 writer + schema used at default-on so
|
|
7802
|
+
# consumers see a single shape. Single-pass, never blocks.
|
|
7803
|
+
_LOKI_COV_MEASURED="false" \
|
|
7804
|
+
_LOKI_COV_PCT="" \
|
|
7805
|
+
_LOKI_COV_TOOL="none" \
|
|
7806
|
+
_LOKI_COV_REASON="not requested (set LOKI_COVERAGE_GATE=1 to measure)" \
|
|
7807
|
+
_LOKI_COV_MIN="$min_coverage" \
|
|
7808
|
+
_LOKI_COV_ENFORCED="0" \
|
|
7809
|
+
_LOKI_COV_BLOCKED="false" \
|
|
7810
|
+
_LOKI_COV_RUNNER="$test_runner" \
|
|
7811
|
+
_LOKI_COV_OUT="$quality_dir/coverage.json" \
|
|
7812
|
+
python3 -c "
|
|
7813
|
+
import json, os, tempfile
|
|
7814
|
+
out=os.environ['_LOKI_COV_OUT']
|
|
7815
|
+
measured = os.environ.get('_LOKI_COV_MEASURED','false') == 'true'
|
|
7816
|
+
pct_raw = os.environ.get('_LOKI_COV_PCT','')
|
|
7817
|
+
try:
|
|
7818
|
+
pct = float(pct_raw) if (measured and pct_raw != '') else None
|
|
7819
|
+
except ValueError:
|
|
7820
|
+
pct = None
|
|
7821
|
+
def b(v): return os.environ.get(v,'false') == 'true'
|
|
7822
|
+
def i(v):
|
|
7823
|
+
try: return int(float(os.environ.get(v,'0')))
|
|
7824
|
+
except (TypeError, ValueError): return 0
|
|
7825
|
+
rec = {
|
|
7826
|
+
'measured': measured,
|
|
7827
|
+
'pct': pct,
|
|
7828
|
+
'tool': os.environ.get('_LOKI_COV_TOOL','none'),
|
|
7829
|
+
'runner': os.environ.get('_LOKI_COV_RUNNER','none'),
|
|
7830
|
+
'threshold': i('_LOKI_COV_MIN'),
|
|
7831
|
+
'enforced': os.environ.get('_LOKI_COV_ENFORCED','0') == '1',
|
|
7832
|
+
'blocked': b('_LOKI_COV_BLOCKED'),
|
|
7833
|
+
'reason': os.environ.get('_LOKI_COV_REASON','') if not measured else '',
|
|
7834
|
+
'timestamp': __import__('datetime').datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
7835
|
+
}
|
|
7836
|
+
d=os.path.dirname(out)
|
|
7837
|
+
fd, tmp=tempfile.mkstemp(dir=d, suffix='.json')
|
|
7838
|
+
with os.fdopen(fd,'w') as f:
|
|
7839
|
+
json.dump(rec, f, indent=2)
|
|
7840
|
+
os.replace(tmp, out)
|
|
7841
|
+
" 2>/dev/null || true
|
|
7636
7842
|
fi
|
|
7637
7843
|
|
|
7638
7844
|
if [ "$test_passed" = "true" ]; then
|
|
@@ -7730,6 +7936,65 @@ ensure_completion_test_evidence() {
|
|
|
7730
7936
|
return 0
|
|
7731
7937
|
}
|
|
7732
7938
|
|
|
7939
|
+
# P1-1 (v7.51.0): ADVISORY consumer for the evidence-gate detail record that
|
|
7940
|
+
# completion-council.sh:_write_evidence_details writes on EVERY evidence-gate run
|
|
7941
|
+
# (pass and block) to .loki/council/evidence-gate-details.json. Until now run.sh
|
|
7942
|
+
# had ZERO consumers of that file -- the audit record was durable but invisible
|
|
7943
|
+
# to the operator and to the next-iteration prompt. This surfaces a one-line
|
|
7944
|
+
# advisory summary (verdict + diff axis + tests axis). It NEVER blocks and NEVER
|
|
7945
|
+
# introduces a new gate (the evidence gate itself already blocks; this is purely
|
|
7946
|
+
# visibility). Absent or malformed file -> degrade silently (no error, no block).
|
|
7947
|
+
surface_evidence_gate_details() {
|
|
7948
|
+
local _det_file="${TARGET_DIR:-.}/.loki/council/evidence-gate-details.json"
|
|
7949
|
+
[ -f "$_det_file" ] || return 0
|
|
7950
|
+
local _summary
|
|
7951
|
+
_summary=$(_LOKI_EGD_FILE="$_det_file" python3 -c "
|
|
7952
|
+
import json, os, sys
|
|
7953
|
+
try:
|
|
7954
|
+
with open(os.environ['_LOKI_EGD_FILE']) as f:
|
|
7955
|
+
d = json.load(f)
|
|
7956
|
+
except Exception:
|
|
7957
|
+
sys.exit(0)
|
|
7958
|
+
if not isinstance(d, dict):
|
|
7959
|
+
sys.exit(0)
|
|
7960
|
+
verdict = d.get('verdict', 'unknown')
|
|
7961
|
+
diff = d.get('diff', {}) if isinstance(d.get('diff'), dict) else {}
|
|
7962
|
+
tests = d.get('tests', {}) if isinstance(d.get('tests'), dict) else {}
|
|
7963
|
+
diff_ok = diff.get('ok')
|
|
7964
|
+
tests_ok = tests.get('ok')
|
|
7965
|
+
runner = tests.get('runner', 'none')
|
|
7966
|
+
parts = ['verdict=%s' % verdict]
|
|
7967
|
+
parts.append('diff_ok=%s' % diff_ok)
|
|
7968
|
+
parts.append('tests_ok=%s (runner=%s)' % (tests_ok, runner))
|
|
7969
|
+
if diff.get('inconclusive'):
|
|
7970
|
+
parts.append('diff_inconclusive=%s' % (diff.get('inconclusive_reason') or 'yes'))
|
|
7971
|
+
if tests.get('inconclusive'):
|
|
7972
|
+
parts.append('tests_inconclusive=%s' % (tests.get('inconclusive_reason') or 'yes'))
|
|
7973
|
+
print(' '.join(str(p) for p in parts))
|
|
7974
|
+
" 2>/dev/null) || return 0
|
|
7975
|
+
[ -n "$_summary" ] || return 0
|
|
7976
|
+
if printf '%s' "$_summary" | grep -q "verdict=block"; then
|
|
7977
|
+
log_warn "[Council] Evidence-gate details: $_summary"
|
|
7978
|
+
else
|
|
7979
|
+
log_info "[Council] Evidence-gate details: $_summary"
|
|
7980
|
+
fi
|
|
7981
|
+
return 0
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
# P1-1 (v7.51.0): wrapper that runs the evidence gate, then surfaces its detail
|
|
7985
|
+
# record on BOTH the pass and block paths, and returns the gate's exact rc. This
|
|
7986
|
+
# preserves the elif chain's `! council_evidence_gate` semantics byte-for-byte
|
|
7987
|
+
# (fall-through on pass so the held-out and assumption gates downstream still
|
|
7988
|
+
# evaluate; block on a 1). The surface call is advisory-only and never affects
|
|
7989
|
+
# the returned rc. The detail file is fresh here -- _write_evidence_details ran
|
|
7990
|
+
# inside council_evidence_gate just above on this same iteration.
|
|
7991
|
+
_evidence_gate_and_surface() {
|
|
7992
|
+
local _rc=0
|
|
7993
|
+
council_evidence_gate || _rc=$?
|
|
7994
|
+
surface_evidence_gate_details || true
|
|
7995
|
+
return $_rc
|
|
7996
|
+
}
|
|
7997
|
+
|
|
7733
7998
|
# ============================================================================
|
|
7734
7999
|
# Documentation Staleness Check (v6.75.0)
|
|
7735
8000
|
# Checks if generated documentation is stale relative to HEAD
|
|
@@ -14694,6 +14959,88 @@ if __name__ == "__main__":
|
|
|
14694
14959
|
log_warn "Mutation integrity gate FAILED ($mt_count consecutive) - HIGH test-fitting detected"
|
|
14695
14960
|
fi
|
|
14696
14961
|
fi
|
|
14962
|
+
# LSP diagnostics gate (P1-5 bash-route parity, v7.51.0). Closes the
|
|
14963
|
+
# parity gap: the Bun route ships runLSPDiagnostics
|
|
14964
|
+
# (loki-ts/src/runner/quality_gates.ts) with a route-neutral Python
|
|
14965
|
+
# writer (mcp/lsp_proxy.py); the bash route had NO writer/reader.
|
|
14966
|
+
# This block runs the SAME writer and mirrors the TS blocking
|
|
14967
|
+
# semantics byte-for-byte:
|
|
14968
|
+
# - Gate is OPT-IN: default OFF. Enabled by LOKI_GATE_LSP_DIAGNOSTICS=true
|
|
14969
|
+
# (the single toggle; mirrors flag("LOKI_GATE_LSP_DIAGNOSTICS", false)
|
|
14970
|
+
# at quality_gates.ts:1717). No second knob.
|
|
14971
|
+
# - When enabled, count_errors > 0 -> BLOCK (mirrors
|
|
14972
|
+
# "if (errorCount > 0) { passed: false }" at quality_gates.ts:1667).
|
|
14973
|
+
# - warnings only -> advisory PASS (quality_gates.ts:1673).
|
|
14974
|
+
# - artifact absent/malformed -> honest pass-through, NEVER block
|
|
14975
|
+
# (quality_gates.ts:1646 returns passed:true on null artifact).
|
|
14976
|
+
# The writer is OPT-OUT-able with LOKI_GATE_LSP_WRITER=0 (operator can
|
|
14977
|
+
# supply a pre-built artifact), matching the TS escape hatch
|
|
14978
|
+
# (quality_gates.ts:1630). cwd must be the install dir (PROJECT_DIR =
|
|
14979
|
+
# $SCRIPT_DIR/.. ) so `-m mcp.lsp_proxy` imports, while --root points
|
|
14980
|
+
# at the TARGET project the loop is building (mirrors
|
|
14981
|
+
# runLSPDiagnosticsWriter: cwd=REPO_ROOT, --root=ctx.cwd).
|
|
14982
|
+
if [ "${LOKI_GATE_LSP_DIAGNOSTICS:-false}" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
14983
|
+
log_info "Quality gate: LSP diagnostics..."
|
|
14984
|
+
# WRITER: route-neutral Python, same program as the Bun route.
|
|
14985
|
+
if [ "${LOKI_GATE_LSP_WRITER:-1}" != "0" ]; then
|
|
14986
|
+
( cd "$PROJECT_DIR" && LOKI_DIR="${TARGET_DIR:-.}/.loki" python3 -m mcp.lsp_proxy --write-diagnostics --root "${TARGET_DIR:-.}" ) >/dev/null 2>&1 || true
|
|
14987
|
+
fi
|
|
14988
|
+
# READER: read counts, mirror TS block policy.
|
|
14989
|
+
local _lsp_file="${TARGET_DIR:-.}/.loki/quality/lsp-diagnostics.json"
|
|
14990
|
+
local _lsp_verdict="absent"
|
|
14991
|
+
if [ -f "$_lsp_file" ]; then
|
|
14992
|
+
_lsp_verdict=$(_LOKI_LSP_FILE="$_lsp_file" python3 -c "
|
|
14993
|
+
import json, os, sys
|
|
14994
|
+
try:
|
|
14995
|
+
with open(os.environ['_LOKI_LSP_FILE']) as f:
|
|
14996
|
+
d = json.load(f)
|
|
14997
|
+
except Exception:
|
|
14998
|
+
print('absent'); sys.exit(0)
|
|
14999
|
+
if not isinstance(d, dict):
|
|
15000
|
+
print('absent'); sys.exit(0)
|
|
15001
|
+
diags = d.get('diagnostics') if isinstance(d.get('diagnostics'), list) else []
|
|
15002
|
+
ce = d.get('count_errors')
|
|
15003
|
+
cw = d.get('count_warnings')
|
|
15004
|
+
errors = ce if isinstance(ce, int) else sum(1 for x in diags if isinstance(x, dict) and x.get('severity') == 1)
|
|
15005
|
+
warns = cw if isinstance(cw, int) else sum(1 for x in diags if isinstance(x, dict) and x.get('severity') == 2)
|
|
15006
|
+
if errors > 0:
|
|
15007
|
+
print('block %d %d' % (errors, warns))
|
|
15008
|
+
elif warns > 0:
|
|
15009
|
+
print('warn %d %d' % (errors, warns))
|
|
15010
|
+
else:
|
|
15011
|
+
print('clean 0 0')
|
|
15012
|
+
" 2>/dev/null) || _lsp_verdict="absent"
|
|
15013
|
+
[ -n "$_lsp_verdict" ] || _lsp_verdict="absent"
|
|
15014
|
+
fi
|
|
15015
|
+
case "$_lsp_verdict" in
|
|
15016
|
+
block*)
|
|
15017
|
+
local _lsp_e _lsp_w
|
|
15018
|
+
_lsp_e=$(printf '%s' "$_lsp_verdict" | awk '{print $2}')
|
|
15019
|
+
_lsp_w=$(printf '%s' "$_lsp_verdict" | awk '{print $3}')
|
|
15020
|
+
local lsp_count
|
|
15021
|
+
lsp_count=$(track_gate_failure "lsp_diagnostics")
|
|
15022
|
+
gate_failures="${gate_failures}lsp_diagnostics,"
|
|
15023
|
+
log_warn "LSP diagnostics gate FAILED ($lsp_count consecutive) - ${_lsp_e} error(s), ${_lsp_w} warning(s); LSP reports compiler/type errors"
|
|
15024
|
+
;;
|
|
15025
|
+
warn*)
|
|
15026
|
+
local _lsp_w2
|
|
15027
|
+
_lsp_w2=$(printf '%s' "$_lsp_verdict" | awk '{print $3}')
|
|
15028
|
+
clear_gate_failure "lsp_diagnostics"
|
|
15029
|
+
log_info "LSP diagnostics: 0 errors, ${_lsp_w2} warning(s) (advisory)"
|
|
15030
|
+
;;
|
|
15031
|
+
clean*)
|
|
15032
|
+
clear_gate_failure "lsp_diagnostics"
|
|
15033
|
+
log_info "LSP diagnostics: 0 errors, 0 warnings"
|
|
15034
|
+
;;
|
|
15035
|
+
*)
|
|
15036
|
+
# Absent or malformed artifact: honest pass-through, never
|
|
15037
|
+
# block (mirrors quality_gates.ts:1646). Do not fabricate
|
|
15038
|
+
# a clean verdict from absence.
|
|
15039
|
+
clear_gate_failure "lsp_diagnostics"
|
|
15040
|
+
log_info "LSP diagnostics: no lsp-diagnostics.json artifact (lsp not available) -- gate did not run"
|
|
15041
|
+
;;
|
|
15042
|
+
esac
|
|
15043
|
+
fi
|
|
14697
15044
|
# Code review gate (upgraded from advisory, with escalation)
|
|
14698
15045
|
if [ "$PHASE_CODE_REVIEW" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
14699
15046
|
log_info "Quality gate: code review..."
|
|
@@ -14972,7 +15319,7 @@ if __name__ == "__main__":
|
|
|
14972
15319
|
# LOKI_EVIDENCE_GATE=0 (council_evidence_gate returns 0 immediately
|
|
14973
15320
|
# when disabled, so this branch never fires). Gate output (reason +
|
|
14974
15321
|
# opt-out hint) is printed by council_evidence_gate itself.
|
|
14975
|
-
elif [ "$_completion_claimed" = 1 ] && type council_evidence_gate &>/dev/null && !
|
|
15322
|
+
elif [ "$_completion_claimed" = 1 ] && type council_evidence_gate &>/dev/null && ! _evidence_gate_and_surface; then
|
|
14976
15323
|
log_warn "Completion claim rejected: evidence gate found no proof of completion (empty diff vs run-start SHA, or red tests)."
|
|
14977
15324
|
log_warn " Details under .loki/council/evidence-block.json ; opt out with LOKI_EVIDENCE_GATE=0"
|
|
14978
15325
|
# Fall through; keep iterating until there is real evidence.
|
|
@@ -15471,7 +15818,7 @@ check_human_intervention() {
|
|
|
15471
15818
|
fi
|
|
15472
15819
|
if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
|
|
15473
15820
|
log_info "Council force-review: blocked by checklist hard gate"
|
|
15474
|
-
elif type council_evidence_gate &>/dev/null && !
|
|
15821
|
+
elif type council_evidence_gate &>/dev/null && ! _evidence_gate_and_surface; then
|
|
15475
15822
|
log_info "Council force-review: blocked by evidence hard gate"
|
|
15476
15823
|
elif type council_heldout_gate &>/dev/null && ! council_heldout_gate; then
|
|
15477
15824
|
log_info "Council force-review: blocked by held-out spec-eval hard gate"
|