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.
- package/README.md +87 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +45 -4
- package/autonomy/council-v2.sh +63 -1
- package/autonomy/grill.sh +32 -0
- package/autonomy/lib/done-recognition.sh +16 -4
- package/autonomy/lib/fast_verify.py +346 -0
- package/autonomy/lib/no_mock_scan.py +21 -1
- package/autonomy/lib/prd-enrich.sh +7 -1
- package/autonomy/lib/proof-generator.py +146 -6
- package/autonomy/lib/proof-template.html +60 -12
- package/autonomy/lib/proof-verify.py +52 -0
- package/autonomy/loki +236 -12
- package/autonomy/verify.sh +12 -6
- package/dashboard/__init__.py +1 -1
- package/docs/COMPARISON.md +8 -1
- package/docs/EVALUATING.md +142 -0
- package/events/emit.sh +66 -6
- package/loki-ts/dist/loki.js +319 -316
- package/mcp/__init__.py +30 -12
- package/mcp/server.py +150 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +72 -0
- package/providers/codex.sh +13 -0
- package/providers/loader.sh +10 -4
- package/providers/model_catalog.json +50 -0
- package/providers/models.sh +26 -3
- package/providers/opencode.sh +145 -0
- package/references/design-archetypes.md +85 -0
|
@@ -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
|
|
1131
|
-
var
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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 =
|
|
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}
|
|
11137
|
-
|
|
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
|
;;
|
package/autonomy/verify.sh
CHANGED
|
@@ -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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
package/dashboard/__init__.py
CHANGED
package/docs/COMPARISON.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Autonomous Coding Agents Comparison (2025-2026)
|
|
2
2
|
|
|
3
|
-
> Last
|
|
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.
|