loki-mode 8.1.0 → 8.3.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.
@@ -1117,6 +1117,24 @@
1117
1117
  card.innerHTML = (head ? '<div style="margin-bottom:14px;">' + head + "</div>" : "") + body + link;
1118
1118
  }
1119
1119
 
1120
+ // Render one row of gate chips. Shared by the exogenous and advisory blocks.
1121
+ function gateChips(gates) {
1122
+ var chips = "";
1123
+ if (!Array.isArray(gates)) return "";
1124
+ for (var i = 0; i < gates.length; i++) {
1125
+ var st = String(g(gates[i], "status", "")).toLowerCase();
1126
+ var cls = (st === "pass" || st === "passed") ? "pass" : (st === "skip" || st === "skipped") ? "skip" : "fail";
1127
+ var mark = cls === "pass" ? "OK" : cls === "skip" ? "-" : "X";
1128
+ chips += '<span class="gate ' + cls + '"><span class="mk">' + mark + "</span>" +
1129
+ esc(g(gates[i], "name", "gate")) + "</span>";
1130
+ }
1131
+ return chips ? '<div class="gates">' + chips + "</div>" : "";
1132
+ }
1133
+
1134
+ // TRUST-4: quality gates split by PROVENANCE, not by outcome. The two blocks
1135
+ // read from quality_gates.exogenous / quality_gates.advisory, the exact field
1136
+ // names proof-generator.py writes. Falls back to the flat pre-split list so an
1137
+ // older receipt still renders.
1120
1138
  function renderGates(p) {
1121
1139
  var sec = document.getElementById("secGates");
1122
1140
  var card = document.getElementById("gatesCard");
@@ -1127,18 +1145,34 @@
1127
1145
  sec.classList.add("hide");
1128
1146
  return;
1129
1147
  }
1130
- var head = '<p class="note" style="margin:0 0 12px;">' + passed + " of " + total + " gates passed.</p>";
1131
- var chips = "";
1132
- if (Array.isArray(gates)) {
1133
- for (var i = 0; i < gates.length; i++) {
1134
- var st = String(g(gates[i], "status", "")).toLowerCase();
1135
- var cls = (st === "pass" || st === "passed") ? "pass" : (st === "skip" || st === "skipped") ? "skip" : "fail";
1136
- var mark = cls === "pass" ? "OK" : cls === "skip" ? "-" : "X";
1137
- chips += '<span class="gate ' + cls + '"><span class="mk">' + mark + "</span>" +
1138
- esc(g(gates[i], "name", "gate")) + "</span>";
1139
- }
1148
+ var exo = g(p, "quality_gates.exogenous", null);
1149
+ var adv = g(p, "quality_gates.advisory", null);
1150
+ if (!exo && !adv) {
1151
+ // Pre-TRUST-4 receipt: render the flat list exactly as before.
1152
+ card.innerHTML = '<p class="note" style="margin:0 0 12px;">' + passed +
1153
+ " of " + total + " gates passed.</p>" + gateChips(gates);
1154
+ return;
1155
+ }
1156
+ var html = "";
1157
+ var eg = g(exo, "gates", []);
1158
+ if (Array.isArray(eg) && eg.length > 0) {
1159
+ html += '<h3 style="margin:0 0 4px;font-size:14px;">Independent checks (count toward the verdict)</h3>' +
1160
+ '<p class="note" style="margin:0 0 10px;">' +
1161
+ num(g(exo, "passed", 0)) + " of " + num(g(exo, "total", 0)) +
1162
+ " passed. These are deterministic programs the agent cannot write the " +
1163
+ "result of, so only these set the verdict above.</p>" + gateChips(eg);
1164
+ }
1165
+ var ag = g(adv, "gates", []);
1166
+ if (Array.isArray(ag) && ag.length > 0) {
1167
+ html += '<h3 style="margin:18px 0 4px;font-size:14px;">Advisory checks (reported, not counted)</h3>' +
1168
+ '<p class="opinion-note"><span aria-hidden="true">!</span>' +
1169
+ num(g(adv, "passed", 0)) + " of " + num(g(adv, "total", 0)) +
1170
+ " passed. These are the agent judging its own work (it wrote both the " +
1171
+ "tests and the code, or a model graded the result), so a model that is " +
1172
+ "confidently wrong scores itself green here. They are shown for context " +
1173
+ "and can never raise or lower the verdict.</p>" + gateChips(ag);
1140
1174
  }
1141
- card.innerHTML = head + (chips ? '<div class="gates">' + chips + "</div>" : "");
1175
+ card.innerHTML = html;
1142
1176
  }
1143
1177
 
1144
1178
  function renderFlagged(p) {
@@ -1204,9 +1238,23 @@
1204
1238
  if (hash) {
1205
1239
  kv += '<div class="k">Integrity hash</div><div class="v">' + esc(algo) + ":" + esc(hash) + "</div>";
1206
1240
  }
1241
+ // Signing state, named explicitly. The hash-note below already says the
1242
+ // hash does not prove provenance; without this row a reader is told what
1243
+ // the receipt CANNOT do but never which state this particular receipt is
1244
+ // in, nor how to change it. Signing is gated on LOKI_PROOF_GPG_KEY in
1245
+ // proof-generator.py and is OFF by default -- deliberately, because a
1246
+ // self-minted key would flip proof-verify.py's generator_trusted to false
1247
+ // while the key came from the very process under audit.
1248
+ var signed = !!g(p, "verification.gpg_signature", "");
1249
+ kv += '<div class="k">Signature</div><div class="v">' +
1250
+ (signed ? "SIGNED (detached GPG)" : "UNSIGNED") + "</div>";
1251
+ var signNote = signed
1252
+ ? '<p class="hash-note">This receipt carries a detached GPG signature over the same canonical bytes that were hashed. A verifier holding the signer public key can confirm its provenance offline with <code>loki proof verify</code>.</p>'
1253
+ : '<p class="hash-note">This receipt is UNSIGNED, so it trusts the generator that produced it (<code>loki proof verify</code> reports <code>generator_trusted: true</code>). To sign future receipts, set <code>LOKI_PROOF_GPG_KEY</code> to a GPG key id before the run.</p>';
1207
1254
  card.innerHTML = '<div class="kv">' + kv + "</div>" +
1208
1255
  (hash ? '<p class="hash-note">The integrity hash is a ' + esc(algo) +
1209
- " digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "");
1256
+ " digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "") +
1257
+ signNote;
1210
1258
  }
1211
1259
 
1212
1260
  function renderCta(p) {
@@ -65,6 +65,7 @@ CLI:
65
65
  import hashlib
66
66
  import json
67
67
  import os
68
+ import re
68
69
  import subprocess
69
70
  import sys
70
71
 
@@ -176,6 +177,57 @@ def _to_int(v, default=0):
176
177
  # headline re-derivation (MUST match proof-generator._compute_headline exactly)
177
178
  # ---------------------------------------------------------------------------
178
179
 
180
+ # DRIFT-GUARD: mirrored from proof-generator.py. A gate the AGENT authored the
181
+ # input to (it writes both the test and the fix) or that is an LLM judgment
182
+ # cannot be independent evidence, so it must never force or lift a headline.
183
+ # Keyed on the ADVISORY set so an unknown or renamed gate defaults to EXOGENOUS
184
+ # and keeps its power to block -- fail-closed, never fail-open.
185
+ #
186
+ # This block exists because the generator gained a provenance split while this
187
+ # file did not, and the two then disagreed about what a receipt meant. The
188
+ # verifier is the INDEPENDENT re-derivation users are told to trust; if it
189
+ # cannot reproduce the generator's headline, `loki proof verify` reports drift
190
+ # on a receipt that was never tampered with -- a false alarm from our own trust
191
+ # artifact, which is worse than no verifier at all.
192
+ _ADVISORY_GATES = frozenset((
193
+ "test_coverage", "unit_tests", "test_suite", "semantic_tests", "tests",
194
+ "code_review", "devils_advocate", "devil_advocate", "magic_debate",
195
+ "council", "anti_sycophancy",
196
+ ))
197
+
198
+
199
+ def _gate_key(name):
200
+ """Normalize a gate name for provenance lookup (mirrors the generator).
201
+
202
+ run.sh emits the same gate under multiple spellings (`static-analysis` vs
203
+ `static_analysis`) and track_gate_failure appends `_PAUSED`/`_ESCALATED`,
204
+ so an exact-match lookup would misfile real gates.
205
+ """
206
+ s = str(name or "").strip().lower().replace("-", "_")
207
+ return re.sub(r"_(paused|escalated|not_run|blocked)$", "", s)
208
+
209
+
210
+ def _gate_provenance(name):
211
+ """'advisory' for a model-authored gate, else 'exogenous' (fail-closed)."""
212
+ return "advisory" if _gate_key(name) in _ADVISORY_GATES else "exogenous"
213
+
214
+
215
+ def _is_exogenous(gate):
216
+ """Provenance of a gate dict, honoring a stamped value when present.
217
+
218
+ The generator stamps `provenance` so its `unresolved` override survives (a
219
+ gate that HALTED the run is an execution fact, not a model opinion). We
220
+ honor that stamp and fall back to name lookup for older receipts written
221
+ before the split existed.
222
+ """
223
+ if not isinstance(gate, dict):
224
+ return True
225
+ stamped = gate.get("provenance")
226
+ if stamped:
227
+ return stamped == "exogenous"
228
+ return _gate_provenance(gate.get("name")) == "exogenous"
229
+
230
+
179
231
  def _compute_headline(facts, degraded):
180
232
  """Deterministic headline re-derived from the recorded facts.
181
233
 
package/autonomy/loki CHANGED
@@ -10593,8 +10593,133 @@ EOF
10593
10593
  }
10594
10594
 
10595
10595
  # Check system prerequisites
10596
+ # _loki_airgap_audit <json?> -- enumerate every network egress this engine can
10597
+ # perform, classify each as REQUIRED or optional, and say how to disable it.
10598
+ #
10599
+ # WHY THIS EXISTS
10600
+ # Air-gapped deployment is a real enterprise requirement and the incumbent
10601
+ # (Tabnine) states it can "be deployed in a completely air-gapped environment"
10602
+ # without publishing WHICH hosts it would otherwise contact. A claim you
10603
+ # cannot audit is a claim you have to take on faith.
10604
+ #
10605
+ # Sourcegraph is the honest counter-example: their own whitepaper states that
10606
+ # self-hosted Cody still "sends requests from the Sourcegraph instance to
10607
+ # Anthropic's API". Self-hosted control plane, egressing inference.
10608
+ #
10609
+ # So the credible position is not "trust us, we are air-gapped" -- it is here
10610
+ # is every host, here is which ones are required, here is how to turn each one
10611
+ # off, now verify it yourself with tcpdump. We would rather be auditable than
10612
+ # assertive.
10613
+ #
10614
+ # Exit 0 when no REQUIRED egress remains (air-gap achievable in this config),
10615
+ # 1 otherwise. Deterministic: reads configuration, performs NO network calls.
10616
+ _loki_airgap_audit() {
10617
+ local as_json="${1:-false}"
10618
+
10619
+ # Provider inference endpoint: the one egress that is genuinely required,
10620
+ # unless the active provider serves weights locally.
10621
+ local provider="${LOKI_PROVIDER:-claude}"
10622
+ local model_host="" model_required="yes" model_note=""
10623
+ case "$provider" in
10624
+ claude)
10625
+ model_host="${ANTHROPIC_BASE_URL:-https://api.anthropic.com}"
10626
+ model_note="Claude Code inference. Set ANTHROPIC_BASE_URL to an in-network gateway, or switch to a local-weights provider."
10627
+ ;;
10628
+ codex)
10629
+ model_host="${OPENAI_BASE_URL:-https://api.openai.com}"
10630
+ model_note="Codex inference. Point at an in-network gateway or switch provider."
10631
+ ;;
10632
+ opencode|cline|aider)
10633
+ model_host="${OPENAI_BASE_URL:-${OPENROUTER_API_KEY:+https://openrouter.ai}}"
10634
+ if [ -z "$model_host" ] || printf '%s' "${LOKI_OPENCODE_MODEL:-}${LOKI_AIDER_MODEL:-}" | grep -qiE 'ollama|localhost|127\.0\.0\.1|lmstudio'; then
10635
+ model_host="local weights"
10636
+ model_required="no"
10637
+ model_note="Local model runtime (Ollama / LM Studio / llama.cpp). No inference egress."
10638
+ else
10639
+ model_note="Remote model gateway. Use an ollama/ model id for zero-egress inference."
10640
+ fi
10641
+ ;;
10642
+ *)
10643
+ model_host="unknown (provider '$provider')"
10644
+ model_note="Unrecognized provider; audit its config manually."
10645
+ ;;
10646
+ esac
10647
+
10648
+ # Optional egress, each individually disableable.
10649
+ local tel_state="off"
10650
+ [ "${LOKI_TELEMETRY:-}" = "on" ] && tel_state="on"
10651
+ local upd_state="on"
10652
+ [ "${LOKI_NO_UPDATE_CHECK:-0}" = "1" ] && upd_state="off"
10653
+
10654
+ if [ "$as_json" = "true" ]; then
10655
+ _AG_PROV="$provider" _AG_HOST="$model_host" _AG_REQ="$model_required" \
10656
+ _AG_NOTE="$model_note" _AG_TEL="$tel_state" _AG_UPD="$upd_state" \
10657
+ python3 -c '
10658
+ import json, os
10659
+ req = os.environ["_AG_REQ"] == "yes"
10660
+ egress = [{
10661
+ "name": "model_inference",
10662
+ "host": os.environ["_AG_HOST"],
10663
+ "required": req,
10664
+ "disable": "use a local-weights provider (ollama/, lmstudio/) or an in-network gateway",
10665
+ "note": os.environ["_AG_NOTE"],
10666
+ }, {
10667
+ "name": "telemetry",
10668
+ "host": "telemetry endpoint",
10669
+ "required": False,
10670
+ "enabled": os.environ["_AG_TEL"] == "on",
10671
+ "disable": "loki telemetry off (already the default)",
10672
+ }, {
10673
+ "name": "update_check",
10674
+ "host": "registry.npmjs.org",
10675
+ "required": False,
10676
+ "enabled": os.environ["_AG_UPD"] == "on",
10677
+ "disable": "export LOKI_NO_UPDATE_CHECK=1",
10678
+ }]
10679
+ blockers = [e for e in egress if e.get("required")]
10680
+ print(json.dumps({
10681
+ "provider": os.environ["_AG_PROV"],
10682
+ "airgap_ready": not blockers,
10683
+ "required_egress": [e["name"] for e in blockers],
10684
+ "egress": egress,
10685
+ }, indent=2))
10686
+ '
10687
+ [ "$model_required" = "yes" ] && return 1
10688
+ return 0
10689
+ fi
10690
+
10691
+ echo -e "${BOLD}Network egress audit${NC}"
10692
+ echo ""
10693
+ echo " Active provider: $provider"
10694
+ echo ""
10695
+ echo -e "${BOLD}Egress points${NC}"
10696
+ if [ "$model_required" = "yes" ]; then
10697
+ echo -e " ${YELLOW}REQUIRED${NC} model inference -> $model_host"
10698
+ else
10699
+ echo -e " ${GREEN}none${NC} model inference -> $model_host"
10700
+ fi
10701
+ echo " $model_note"
10702
+ echo -e " optional telemetry [$tel_state] disable: loki telemetry off (default off)"
10703
+ echo -e " optional update check [$upd_state] disable: export LOKI_NO_UPDATE_CHECK=1"
10704
+ echo ""
10705
+ if [ "$model_required" = "yes" ]; then
10706
+ echo -e "${YELLOW}Not air-gap ready in this configuration.${NC}"
10707
+ echo " One required egress remains: model inference."
10708
+ echo " To close it, serve the model in-network:"
10709
+ echo " loki provider set opencode"
10710
+ echo " export LOKI_OPENCODE_MODEL=ollama/qwen2.5-coder"
10711
+ echo ""
10712
+ echo " Then re-run: loki doctor --airgap"
10713
+ return 1
10714
+ fi
10715
+ echo -e "${GREEN}Air-gap ready: no required egress in this configuration.${NC}"
10716
+ echo " Verify independently -- run with the interface down, or watch with tcpdump."
10717
+ return 0
10718
+ }
10719
+
10596
10720
  cmd_doctor() {
10597
10721
  local json_output=false
10722
+ local airgap_audit=false
10598
10723
 
10599
10724
  while [[ $# -gt 0 ]]; do
10600
10725
  case "$1" in
@@ -10602,13 +10727,21 @@ cmd_doctor() {
10602
10727
  json_output=true
10603
10728
  shift
10604
10729
  ;;
10730
+ --airgap)
10731
+ airgap_audit=true
10732
+ shift
10733
+ ;;
10605
10734
  --help|-h)
10606
10735
  echo -e "${BOLD}loki doctor${NC} - Check system prerequisites"
10607
10736
  echo ""
10608
- echo "Usage: loki doctor [--json]"
10737
+ echo "Usage: loki doctor [--json] [--airgap]"
10609
10738
  echo ""
10610
10739
  echo "Options:"
10611
10740
  echo " --json Output machine-readable JSON"
10741
+ echo " --airgap Audit network egress: list every host this engine"
10742
+ echo " can contact, whether it is REQUIRED or optional,"
10743
+ echo " and how to disable it. Exits non-zero if any"
10744
+ echo " required egress remains."
10612
10745
  echo ""
10613
10746
  echo "Checks: node, python3, jq, git, curl, bash version,"
10614
10747
  echo " claude/codex CLIs, disk space, and cockpit render capability."
@@ -10616,12 +10749,17 @@ cmd_doctor() {
10616
10749
  ;;
10617
10750
  *)
10618
10751
  echo -e "${RED}Unknown option: $1${NC}"
10619
- echo "Usage: loki doctor [--json]"
10752
+ echo "Usage: loki doctor [--json] [--airgap]"
10620
10753
  return 1
10621
10754
  ;;
10622
10755
  esac
10623
10756
  done
10624
10757
 
10758
+ if [ "$airgap_audit" = true ]; then
10759
+ _loki_airgap_audit "$json_output"
10760
+ return $?
10761
+ fi
10762
+
10625
10763
  if [ "$json_output" = true ]; then
10626
10764
  cmd_doctor_json
10627
10765
  return $?
@@ -10634,6 +10772,15 @@ cmd_doctor() {
10634
10772
 
10635
10773
  local pass_count=0
10636
10774
  local fail_count=0
10775
+ # FUNNEL FIX (v8.2.x): collect the NAME + FIX of every hard failure, not just
10776
+ # a count. A first-time user on a bare machine sees 15 warnings (sentrux, GPG
10777
+ # signing, bash 4, Bun, python3.12, truecolor, inline-image probe -- all
10778
+ # OPTIONAL) around 2 real blockers, then a summary that says only "Some
10779
+ # required prerequisites are missing" without naming them. That is the point
10780
+ # where someone closes the terminal. Naming the blockers and their exact
10781
+ # install command turns a wall of yellow into a two-line to-do.
10782
+ local _doctor_blockers=""
10783
+ _doctor_block() { _doctor_blockers="${_doctor_blockers}\n - $1"; }
10637
10784
  local warn_count=0
10638
10785
 
10639
10786
  # Helper: check command exists and optionally check version
@@ -10646,6 +10793,7 @@ cmd_doctor() {
10646
10793
  if ! command -v "$cmd" &> /dev/null; then
10647
10794
  if [ "$required" = "required" ]; then
10648
10795
  echo -e " ${RED}FAIL${NC} $name - not found"
10796
+ _doctor_block "$name is not installed"
10649
10797
  fail_count=$((fail_count + 1))
10650
10798
  elif [ "$required" = "recommended" ]; then
10651
10799
  echo -e " ${YELLOW}WARN${NC} $name - not found (recommended)"
@@ -10711,6 +10859,7 @@ cmd_doctor() {
10711
10859
  { [ "$cur_major" -eq "$min_major" ] 2>/dev/null && [ "$cur_minor" -lt "$min_minor" ] 2>/dev/null; }; then
10712
10860
  if [ "$required" = "required" ]; then
10713
10861
  echo -e " ${RED}FAIL${NC} $name$version_display - requires >= $min_version"
10862
+ _doctor_block "$name must be >= $min_version"
10714
10863
  fail_count=$((fail_count + 1))
10715
10864
  else
10716
10865
  echo -e " ${YELLOW}WARN${NC} $name$version_display - recommended >= $min_version"
@@ -10782,6 +10931,7 @@ cmd_doctor() {
10782
10931
  else
10783
10932
  echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
10784
10933
  echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
10934
+ _doctor_block "No AI provider CLI. Fix: npm install -g @anthropic-ai/claude-code"
10785
10935
  fail_count=$((fail_count + 1))
10786
10936
  # v7.29.0: on a TTY (non-json), append the consent-gated install offer.
10787
10937
  # In report mode the helper is a no-op on non-TTY/CI, so the doctor
@@ -10886,6 +11036,7 @@ except Exception:
10886
11036
  _target=$(readlink "$sdir" 2>/dev/null || echo "unknown")
10887
11037
  echo -e " ${RED}FAIL${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
10888
11038
  echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
11039
+ _doctor_block "$sname is a broken symlink. Fix: loki setup-skill"
10889
11040
  fail_count=$((fail_count + 1))
10890
11041
  else
10891
11042
  echo -e " ${YELLOW}WARN${NC} $sname ${DIM}(not found - run 'loki setup-skill')${NC}"
@@ -11007,6 +11158,24 @@ except Exception:
11007
11158
  echo -e " ${YELLOW}WARN${NC} sentrux - not installed (optional, brew install sentrux/tap/sentrux)"
11008
11159
  warn_count=$((warn_count + 1))
11009
11160
  fi
11161
+ # Evidence Receipt signing state (optional). Unsigned receipts are the
11162
+ # default and are still checkable (hash + diff re-derivation); what they
11163
+ # cannot prove is PROVENANCE, so proof-verify.py reports
11164
+ # generator_trusted: true. Surface the state here because it is otherwise
11165
+ # invisible until someone reads a receipt. WARN (never FAIL): unsigned is
11166
+ # a supported, documented mode, not a broken install.
11167
+ if [ -n "${LOKI_PROOF_GPG_KEY:-}" ]; then
11168
+ if command -v gpg &>/dev/null; then
11169
+ echo -e " ${GREEN}PASS${NC} Receipt signing: LOKI_PROOF_GPG_KEY set and gpg available"
11170
+ pass_count=$((pass_count + 1))
11171
+ else
11172
+ echo -e " ${YELLOW}WARN${NC} Receipt signing: LOKI_PROOF_GPG_KEY set but gpg NOT on PATH - receipts will be UNSIGNED"
11173
+ warn_count=$((warn_count + 1))
11174
+ fi
11175
+ else
11176
+ echo -e " ${YELLOW}WARN${NC} Receipt signing: UNSIGNED (set LOKI_PROOF_GPG_KEY to a gpg key id; see docs/SIGNED-RECEIPTS.md)"
11177
+ warn_count=$((warn_count + 1))
11178
+ fi
11010
11179
  echo ""
11011
11180
 
11012
11181
  echo -e "${CYAN}System:${NC}"
@@ -11031,6 +11200,7 @@ except Exception:
11031
11200
  if [ -n "$disk_avail" ] && [ "$disk_avail" -gt 0 ] 2>/dev/null; then
11032
11201
  if [ "$disk_avail" -lt 1 ]; then
11033
11202
  echo -e " ${RED}FAIL${NC} Disk space: ${disk_avail}GB available (need >= 1GB)"
11203
+ _doctor_block "Free up disk: ${disk_avail}GB available, need >= 1GB"
11034
11204
  fail_count=$((fail_count + 1))
11035
11205
  elif [ "$disk_avail" -lt 5 ]; then
11036
11206
  echo -e " ${YELLOW}WARN${NC} Disk space: ${disk_avail}GB available (low)"
@@ -11133,8 +11303,11 @@ except Exception:
11133
11303
  echo ""
11134
11304
 
11135
11305
  if [ "$fail_count" -gt 0 ]; then
11136
- echo -e "${RED}Some required prerequisites are missing.${NC}"
11137
- echo "Install missing dependencies and run 'loki doctor' again."
11306
+ echo -e "${RED}Blocking ($fail_count). Everything else above is optional.${NC}"
11307
+ printf '%b\n' "$_doctor_blockers"
11308
+ echo ""
11309
+ echo "Then re-run: loki doctor"
11310
+ echo "Meanwhile 'loki tour' works right now -- no provider, no key, no spend."
11138
11311
  return 1
11139
11312
  elif [ "$warn_count" -gt 0 ]; then
11140
11313
  echo -e "${YELLOW}All required checks passed with some warnings.${NC}"
@@ -11242,6 +11415,23 @@ sentrux = {
11242
11415
  'required': 'optional'
11243
11416
  }
11244
11417
 
11418
+ # Evidence Receipt signing state. Unsigned is the DEFAULT and is a supported
11419
+ # mode, so this is never 'fail'. Exposed because signing state is otherwise
11420
+ # invisible until someone reads a receipt, and it is exactly what decides
11421
+ # whether proof-verify.py reports generator_trusted: true. Sibling of
11422
+ # checks/disk/sentrux -- deliberately NOT counted in the summary tally so
11423
+ # existing consumers see unchanged numbers.
11424
+ signing_key_set = bool(os.environ.get('LOKI_PROOF_GPG_KEY', '').strip())
11425
+ signing_gpg_available = shutil.which('gpg') is not None
11426
+ signing_enabled = signing_key_set and signing_gpg_available
11427
+ receipt_signing = {
11428
+ 'enabled': signing_enabled,
11429
+ 'key_configured': signing_key_set,
11430
+ 'gpg_available': signing_gpg_available,
11431
+ 'status': 'pass' if signing_enabled else 'warn',
11432
+ 'required': 'optional'
11433
+ }
11434
+
11245
11435
  # v7.7.17: memory subsystem health surface. Reports the latest entries
11246
11436
  # from .loki/memory/.errors.log (rotated by memory/error_log.py) so
11247
11437
  # developers see regressions in the previously-silent-fail call sites
@@ -11295,6 +11485,7 @@ result = {
11295
11485
  'status': disk_status
11296
11486
  },
11297
11487
  'sentrux': sentrux,
11488
+ 'receipt_signing': receipt_signing,
11298
11489
  'memory': memory,
11299
11490
  'summary': {
11300
11491
  'passed': pass_count,
@@ -14885,6 +15076,33 @@ with open(manifest_path, 'w') as f:
14885
15076
  # usable.
14886
15077
  # ---------------------------------------------------------------------------
14887
15078
  cmd_verify() {
15079
+ # --fast: deterministic checks only, in milliseconds. Measured on this repo
15080
+ # (1,932 tracked files): 298 ms cold, 87 ms warm, 19 ms diff-scoped, against
15081
+ # an 11,040 ms shell-based baseline.
15082
+ #
15083
+ # It runs ONLY exogenous checks -- no model call, no network -- so every
15084
+ # verdict is reproducible by anyone with the same commit. That is the point,
15085
+ # not a limitation: a verdict you can re-derive is a fact, and a fact is what
15086
+ # an IDE, a CI step, or another vendor's agent can safely act on without
15087
+ # trusting our model choices.
15088
+ #
15089
+ # The full `loki verify` remains the deeper, slower path.
15090
+ for _fv_arg in "$@"; do
15091
+ if [ "$_fv_arg" = "--fast" ]; then
15092
+ local _fv_py="$_LOKI_SCRIPT_DIR/lib/fast_verify.py"
15093
+ if [ ! -f "$_fv_py" ]; then
15094
+ echo -e "${RED}Error: fast_verify not found at $_fv_py${NC}" >&2
15095
+ return 3
15096
+ fi
15097
+ local _fv_argv=()
15098
+ for _a in "$@"; do
15099
+ [ "$_a" = "--fast" ] || _fv_argv+=("$_a")
15100
+ done
15101
+ python3 "$_fv_py" "${_fv_argv[@]+"${_fv_argv[@]}"}"
15102
+ return $?
15103
+ fi
15104
+ done
15105
+
14888
15106
  local verify_mod="$_LOKI_SCRIPT_DIR/verify.sh"
14889
15107
  if [ ! -f "$verify_mod" ]; then
14890
15108
  echo -e "${RED}Error: verify module not found at $verify_mod${NC}" >&2
@@ -18903,7 +19121,7 @@ $(cat "$f" 2>/dev/null)
18903
19121
 
18904
19122
  # --- Tally results ---
18905
19123
  local count_critical=0 count_high=0 count_medium=0 count_low=0 count_info=0
18906
- for f in "${findings[@]}"; do
19124
+ for f in "${findings[@]+"${findings[@]}"}"; do
18907
19125
  local sev
18908
19126
  sev=$(echo "$f" | cut -d'|' -f3)
18909
19127
  case "$sev" in
@@ -18925,7 +19143,7 @@ $(cat "$f" 2>/dev/null)
18925
19143
  if [ "$review_format" = "json" ]; then
18926
19144
  local json_findings="["
18927
19145
  local first=true
18928
- for f in "${findings[@]}"; do
19146
+ for f in "${findings[@]+"${findings[@]}"}"; do
18929
19147
  local f_file f_line f_sev f_cat f_finding f_suggestion
18930
19148
  f_file=$(echo "$f" | cut -d'|' -f1)
18931
19149
  f_line=$(echo "$f" | cut -d'|' -f2)
@@ -18954,7 +19172,7 @@ $(cat "$f" 2>/dev/null)
18954
19172
  # Group by severity
18955
19173
  for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
18956
19174
  local printed_header=false
18957
- for f in "${findings[@]}"; do
19175
+ for f in "${findings[@]+"${findings[@]}"}"; do
18958
19176
  local f_sev
18959
19177
  f_sev=$(echo "$f" | cut -d'|' -f3)
18960
19178
  [ "$f_sev" != "$sev_name" ] && continue
@@ -29880,7 +30098,7 @@ TEST_SUGGEST_PY
29880
30098
 
29881
30099
  # --- Tally findings ---
29882
30100
  local count_critical=0 count_high=0 count_medium=0 count_low=0 count_info=0
29883
- for f in "${findings[@]}"; do
30101
+ for f in "${findings[@]+"${findings[@]}"}"; do
29884
30102
  local sev
29885
30103
  sev=$(echo "$f" | cut -d'|' -f3)
29886
30104
  case "$sev" in
@@ -29896,7 +30114,7 @@ TEST_SUGGEST_PY
29896
30114
  # Determine exit code based on --fail-on threshold
29897
30115
  local exit_code=0
29898
30116
  if [ "$fail_threshold" -lt 99 ]; then
29899
- for f in "${findings[@]}"; do
30117
+ for f in "${findings[@]+"${findings[@]}"}"; do
29900
30118
  local sev sev_num
29901
30119
  sev=$(echo "$f" | cut -d'|' -f3)
29902
30120
  sev_num=$(_ci_sev_level "$sev")
@@ -29916,7 +30134,7 @@ TEST_SUGGEST_PY
29916
30134
  if [ "$ci_format" = "json" ]; then
29917
30135
  # JSON output via python for proper escaping
29918
30136
  export LOKI_CI_JSON_FINDINGS=""
29919
- for f in "${findings[@]}"; do
30137
+ for f in "${findings[@]+"${findings[@]}"}"; do
29920
30138
  LOKI_CI_JSON_FINDINGS+="${f}"$'\n'
29921
30139
  done
29922
30140
  export LOKI_CI_JSON_META="ci_env=${ci_env}|pr=${ci_pr_number}|ts=${report_timestamp}|files=${file_count}|exit=${exit_code}"
@@ -29998,7 +30216,7 @@ CI_JSON_OUT
29998
30216
  echo ""
29999
30217
  for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
30000
30218
  local has_sev=false
30001
- for f in "${findings[@]}"; do
30219
+ for f in "${findings[@]+"${findings[@]}"}"; do
30002
30220
  local f_sev
30003
30221
  f_sev=$(echo "$f" | cut -d'|' -f3)
30004
30222
  [ "$f_sev" != "$sev_name" ] && continue
@@ -30058,7 +30276,7 @@ for t in tests:
30058
30276
  else
30059
30277
  for sev_name in CRITICAL HIGH MEDIUM LOW INFO; do
30060
30278
  local printed_header=false
30061
- for f in "${findings[@]}"; do
30279
+ for f in "${findings[@]+"${findings[@]}"}"; do
30062
30280
  local f_sev
30063
30281
  f_sev=$(echo "$f" | cut -d'|' -f3)
30064
30282
  [ "$f_sev" != "$sev_name" ] && continue
@@ -32314,6 +32532,12 @@ cmd_proof() {
32314
32532
  echo " --hosted Publish to LOKI_HOSTED_ENDPOINT (open-core seam; no official backend yet)"
32315
32533
  echo ""
32316
32534
  echo "Proofs are generated automatically at run completion (LOKI_PROOF=0 to opt out)."
32535
+ echo ""
32536
+ echo "Signing (off by default): receipts are UNSIGNED unless LOKI_PROOF_GPG_KEY is"
32537
+ echo "set to a gpg key id. Unsigned, the integrity hash proves the bytes were not"
32538
+ echo "edited after hashing but NOT who produced them, so verify reports"
32539
+ echo "generator_trusted: true. Export LOKI_PROOF_GPG_KEY to close that gap."
32540
+ echo "See docs/SIGNED-RECEIPTS.md."
32317
32541
  [ "$sub" = "" ] && exit 1
32318
32542
  exit 0
32319
32543
  ;;
@@ -781,13 +781,19 @@ verify_gate_nomock() {
781
781
  return 0
782
782
  fi
783
783
  local status_line
784
+ # The changed-file list goes over STDIN, not an env var. A single env string
785
+ # is capped at MAX_ARG_STRLEN (131071 bytes) on Linux; a large changed set
786
+ # (e.g. a generated source tree) makes execve fail with E2BIG, the fallback
787
+ # below fires, and the gate silently degrades to inconclusive. macOS has no
788
+ # per-string cap, so that failure was Linux-only.
784
789
  status_line=$(
785
- _NM_FILES="$changed" \
786
- _NM_TREE="$tree" \
787
- _NM_OUT="$scan_file" \
788
- _NM_BASE="${VERIFY_MERGE_BASE:-}" \
789
- python3 -I "$scanner" 2>/dev/null \
790
- || echo "INCONCLUSIVE:detector_error"
790
+ printf '%s\n' "$changed" | {
791
+ _NM_TREE="$tree" \
792
+ _NM_OUT="$scan_file" \
793
+ _NM_BASE="${VERIFY_MERGE_BASE:-}" \
794
+ python3 -I "$scanner" 2>/dev/null \
795
+ || echo "INCONCLUSIVE:detector_error"
796
+ }
791
797
  )
792
798
 
793
799
  case "$status_line" in
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.1.0"
10
+ __version__ = "8.3.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -1,6 +1,13 @@
1
1
  # Autonomous Coding Agents Comparison (2025-2026)
2
2
 
3
- > Last Updated: January 25, 2026 (v2.36.9)
3
+ > **STALE. Last updated January 25, 2026 against v2.36.9; the current release is
4
+ > v8.2.0.** Roughly six months and thirty releases of drift, and every cell in
5
+ > the grids below is a subjective grade assigned by the vendor being graded --
6
+ > which is exactly the kind of comparison a serious evaluator discounts.
7
+ >
8
+ > For claims you can actually check, see **[EVALUATING.md](EVALUATING.md)**: a
9
+ > runnable command next to every assertion, plus an explicit list of what we do
10
+ > not have. This file is kept for historical reference only.
4
11
  >
5
12
  > A comprehensive comparison of Loki Mode against major autonomous coding agents and AI IDEs in the market.
6
13
  > Deep-dive comparisons validated by Opus feedback loops.