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/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
- # Log but proceed (full approval flow is P1-3 scope)
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 && ! council_evidence_gate; then
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 && ! council_evidence_gate; then
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"