loki-mode 7.85.0 → 7.87.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/loki CHANGED
@@ -16500,6 +16500,10 @@ main() {
16500
16500
  # Receipt surface): same subcommands (list/show/verify/open/share).
16501
16501
  cmd_proof "$@"
16502
16502
  ;;
16503
+ secure)
16504
+ # Secure-by-default gate surface: inspect findings + manage waivers.
16505
+ cmd_secure "$@"
16506
+ ;;
16503
16507
  bench)
16504
16508
  cmd_bench "$@"
16505
16509
  ;;
@@ -30434,6 +30438,110 @@ cmd_bench() {
30434
30438
  bash "$bench_sh" "$@"
30435
30439
  }
30436
30440
 
30441
+ # loki secure - the secure-by-default gate surface (v7.87.0).
30442
+ # Subcommands: list (show findings) | waive <rule> <file> [reason] | unwaive.
30443
+ # Waivers are written to .loki/quality/security-waivers.json, which the gate
30444
+ # (run.sh run_secure_scan) and the Evidence Receipt both READ + honor. The gate
30445
+ # is advisory by default; LOKI_SECURE_GATE=block makes un-waived HIGH findings
30446
+ # block. Honest: a waiver is RECORDED in the receipt (accepted with intent), never
30447
+ # silently hides a finding.
30448
+ cmd_secure() {
30449
+ local loki_dir="${LOKI_DIR:-.loki}"
30450
+ local quality_dir="${loki_dir}/quality"
30451
+ local findings_file="${quality_dir}/security-findings.json"
30452
+ local waivers_file="${quality_dir}/security-waivers.json"
30453
+ local sub="${1:-}"
30454
+ [ $# -gt 0 ] && shift
30455
+ case "$sub" in
30456
+ ""|--help|-h|help)
30457
+ echo -e "${BOLD}loki secure${NC} - secure-by-default gate: findings + waivers"
30458
+ echo ""
30459
+ echo "Usage: loki secure <subcommand> [args]"
30460
+ echo ""
30461
+ echo "Subcommands:"
30462
+ echo " list Show security findings from the last scan"
30463
+ echo " waive <rule> <file> [reason] Waive a finding (accepted with intent)"
30464
+ echo " unwaive <rule> <file> Remove a waiver"
30465
+ echo ""
30466
+ echo "The gate is advisory by default; set LOKI_SECURE_GATE=block to make"
30467
+ echo "un-waived HIGH findings block completion. Waivers are recorded in the"
30468
+ echo "Evidence Receipt (loki proof show) -- they are never hidden."
30469
+ [ "$sub" = "" ] && exit 1
30470
+ exit 0
30471
+ ;;
30472
+ list)
30473
+ if [ ! -f "$findings_file" ]; then
30474
+ echo -e "${YELLOW}No security scan results yet.${NC} Run 'loki start' (the gate runs in the review phase)."
30475
+ exit 0
30476
+ fi
30477
+ if command -v jq &>/dev/null; then
30478
+ jq '.findings' "$findings_file" 2>/dev/null || cat "$findings_file"
30479
+ else
30480
+ LOKI_SEC_F="$findings_file" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_SEC_F'])); [print('%s [%s] %s%s -- %s' % (f.get('severity','?'), f.get('rule','?'), f.get('file','?'), (':'+str(f['line'])) if f.get('line') else '', f.get('fix',''))) for f in d.get('findings',[])] or print('No findings.')"
30481
+ fi
30482
+ exit 0
30483
+ ;;
30484
+ waive)
30485
+ local rule="${1:-}" file="${2:-}" reason="${3:-waived via loki secure}"
30486
+ if [ -z "$rule" ] || [ -z "$file" ]; then
30487
+ echo -e "${RED}Usage: loki secure waive <rule> <file> [reason]${NC}" >&2
30488
+ exit 2
30489
+ fi
30490
+ mkdir -p "$quality_dir"
30491
+ LOKI_SEC_W="$waivers_file" LOKI_SEC_RULE="$rule" LOKI_SEC_FILE="$file" LOKI_SEC_REASON="$reason" python3 - <<'PYW'
30492
+ import json, os
30493
+ p = os.environ["LOKI_SEC_W"]
30494
+ try:
30495
+ with open(p) as f: data = json.load(f)
30496
+ if not isinstance(data, dict): data = {}
30497
+ except Exception:
30498
+ data = {}
30499
+ waivers = data.get("waivers")
30500
+ if not isinstance(waivers, list): waivers = []
30501
+ rule, fl, reason = os.environ["LOKI_SEC_RULE"], os.environ["LOKI_SEC_FILE"], os.environ["LOKI_SEC_REASON"]
30502
+ if not any(w.get("rule") == rule and w.get("file") == fl for w in waivers if isinstance(w, dict)):
30503
+ waivers.append({"rule": rule, "file": fl, "reason": reason})
30504
+ data["waivers"] = waivers
30505
+ tmp = p + ".tmp"
30506
+ with open(tmp, "w") as f: json.dump(data, f, indent=2)
30507
+ os.replace(tmp, p)
30508
+ print("Waived %s on %s (recorded in the Evidence Receipt)." % (rule, fl))
30509
+ PYW
30510
+ exit $?
30511
+ ;;
30512
+ unwaive)
30513
+ local rule="${1:-}" file="${2:-}"
30514
+ if [ -z "$rule" ] || [ -z "$file" ]; then
30515
+ echo -e "${RED}Usage: loki secure unwaive <rule> <file>${NC}" >&2
30516
+ exit 2
30517
+ fi
30518
+ [ -f "$waivers_file" ] || { echo "No waivers to remove."; exit 0; }
30519
+ LOKI_SEC_W="$waivers_file" LOKI_SEC_RULE="$rule" LOKI_SEC_FILE="$file" python3 - <<'PYU'
30520
+ import json, os
30521
+ p = os.environ["LOKI_SEC_W"]
30522
+ try:
30523
+ with open(p) as f: data = json.load(f)
30524
+ except Exception:
30525
+ data = {}
30526
+ waivers = [w for w in (data.get("waivers") or [])
30527
+ if not (isinstance(w, dict) and w.get("rule") == os.environ["LOKI_SEC_RULE"]
30528
+ and w.get("file") == os.environ["LOKI_SEC_FILE"])]
30529
+ data["waivers"] = waivers
30530
+ tmp = p + ".tmp"
30531
+ with open(tmp, "w") as f: json.dump(data, f, indent=2)
30532
+ os.replace(tmp, p)
30533
+ print("Removed waiver for %s on %s." % (os.environ["LOKI_SEC_RULE"], os.environ["LOKI_SEC_FILE"]))
30534
+ PYU
30535
+ exit $?
30536
+ ;;
30537
+ *)
30538
+ echo -e "${RED}Unknown subcommand: secure $sub${NC}" >&2
30539
+ echo "Try: loki secure --help"
30540
+ exit 2
30541
+ ;;
30542
+ esac
30543
+ }
30544
+
30437
30545
  # loki proof - inspect and share proof-of-run artifacts (.loki/proofs/<id>/).
30438
30546
  # Subcommands: list | show <id> | open <id> | share <id>.
30439
30547
  # The proof.json schema is frozen (R1 spec). Reads are tolerant of missing
package/autonomy/run.sh CHANGED
@@ -7309,6 +7309,157 @@ SAFEOF
7309
7309
  fi
7310
7310
  }
7311
7311
 
7312
+ # ============================================================================
7313
+ # Secure-by-default scan (v7.87.0 - Loop 4)
7314
+ # Runs the high-precision rule engine (autonomy/lib/secure-scan.py) over the
7315
+ # generated app and reports known-bad security patterns.
7316
+ #
7317
+ # ADVISORY BY DEFAULT (mirrors the ktlint/detekt advisory linters above):
7318
+ # findings are reported via log_warn + the receipt json, but do NOT block. This
7319
+ # guarantees no existing build starts blocking on this new gate.
7320
+ #
7321
+ # OPT-IN BLOCK: only when LOKI_SECURE_GATE=block do un-waived HIGH findings
7322
+ # cause a blocking gate failure (return 1, same mechanism the other gates use).
7323
+ #
7324
+ # Waivers: .loki/quality/security-waivers.json ({"waivers":[{rule,file},...]})
7325
+ # is READ here and honored (matched findings recorded as waived, never counted
7326
+ # active). The waiver-write surface is a separate slice.
7327
+ #
7328
+ # Honest degrade: if python3 or secure-scan.py is absent, pass through cleanly
7329
+ # (no crash, no block), exactly like the optional linters.
7330
+ # ============================================================================
7331
+ run_secure_scan() {
7332
+ local loki_dir="${TARGET_DIR:-.}/.loki"
7333
+ local quality_dir="$loki_dir/quality"
7334
+ mkdir -p "$quality_dir"
7335
+
7336
+ local out_file="$quality_dir/security-findings.json"
7337
+ local waivers_file="$quality_dir/security-waivers.json"
7338
+ local scanner="$SCRIPT_DIR/lib/secure-scan.py"
7339
+
7340
+ # Honest pass-through if the engine or python3 is unavailable. Still write a
7341
+ # valid (empty) receipt so downstream consumers never read malformed JSON.
7342
+ if ! command -v python3 >/dev/null 2>&1 || [ ! -f "$scanner" ]; then
7343
+ cat > "$out_file" << 'SECEMPTY'
7344
+ {"rules_version":null,"findings":[],"summary":{"total":0,"by_severity":{}},"skipped":"scanner-unavailable"}
7345
+ SECEMPTY
7346
+ log_info "Security scan: secure-scan.py or python3 not available, skipping (pass-through)"
7347
+ return 0
7348
+ fi
7349
+
7350
+ # Run the scanner. exit 0 = no findings, 1 = findings, 2 = bad input.
7351
+ local raw rc=0
7352
+ raw=$(python3 "$scanner" "${TARGET_DIR:-.}" --json 2>/dev/null) || rc=$?
7353
+ if [ "$rc" -eq 2 ] || [ -z "$raw" ]; then
7354
+ cat > "$out_file" << 'SECEMPTY'
7355
+ {"rules_version":null,"findings":[],"summary":{"total":0,"by_severity":{}},"skipped":"scanner-error"}
7356
+ SECEMPTY
7357
+ log_info "Security scan: scanner returned no parseable output, skipping (pass-through)"
7358
+ return 0
7359
+ fi
7360
+
7361
+ # Apply waivers, build the receipt json, and emit a machine-readable verdict.
7362
+ # All policy lives in this one python pass so the bash stays bash-3.2 safe.
7363
+ # It prints a final line: ACTIVE_HIGH=<n>\tACTIVE_TOTAL=<n>\tWAIVED=<n>
7364
+ # and writes the enriched receipt (findings carry a "waived" bool).
7365
+ local verdict
7366
+ verdict=$(_SEC_RAW="$raw" _SEC_WAIVERS="$waivers_file" _SEC_OUT="$out_file" python3 -c '
7367
+ import json, os, sys
7368
+ raw = os.environ.get("_SEC_RAW", "")
7369
+ waivers_file = os.environ.get("_SEC_WAIVERS", "")
7370
+ out_file = os.environ.get("_SEC_OUT", "")
7371
+
7372
+ try:
7373
+ data = json.loads(raw)
7374
+ except Exception:
7375
+ data = {"rules_version": None, "findings": [], "summary": {"total": 0, "by_severity": {}}}
7376
+
7377
+ # Load waivers: {"waivers":[{"rule":..,"file":..}, ...]}. Match on rule+file.
7378
+ waived_set = set()
7379
+ try:
7380
+ with open(waivers_file) as f:
7381
+ wdoc = json.load(f)
7382
+ for w in wdoc.get("waivers", []):
7383
+ r = w.get("rule"); fl = w.get("file")
7384
+ if r is not None and fl is not None:
7385
+ waived_set.add((r, fl))
7386
+ except (OSError, json.JSONDecodeError, AttributeError):
7387
+ pass
7388
+
7389
+ findings = data.get("findings", []) or []
7390
+ active_high = 0
7391
+ active_total = 0
7392
+ waived_count = 0
7393
+ for fnd in findings:
7394
+ key = (fnd.get("rule"), fnd.get("file"))
7395
+ is_waived = key in waived_set
7396
+ fnd["waived"] = is_waived
7397
+ if is_waived:
7398
+ waived_count += 1
7399
+ else:
7400
+ active_total += 1
7401
+ if str(fnd.get("severity", "")).upper() == "HIGH":
7402
+ active_high += 1
7403
+
7404
+ data["waived"] = waived_count
7405
+ data["active"] = active_total
7406
+ try:
7407
+ with open(out_file, "w") as f:
7408
+ json.dump(data, f, indent=2)
7409
+ except OSError:
7410
+ pass
7411
+
7412
+ sys.stdout.write("ACTIVE_HIGH=%d\tACTIVE_TOTAL=%d\tWAIVED=%d" % (active_high, active_total, waived_count))
7413
+ ' 2>/dev/null) || verdict=""
7414
+
7415
+ if [ -z "$verdict" ]; then
7416
+ # python policy pass failed unexpectedly; preserve the raw scan as the
7417
+ # receipt so nothing is lost, and pass through (never crash the gate).
7418
+ printf '%s\n' "$raw" > "$out_file" 2>/dev/null || true
7419
+ log_info "Security scan: result recorded (policy pass unavailable, advisory)"
7420
+ return 0
7421
+ fi
7422
+
7423
+ local active_high active_total waived
7424
+ active_high=$(printf '%s' "$verdict" | sed -n 's/.*ACTIVE_HIGH=\([0-9]*\).*/\1/p')
7425
+ active_total=$(printf '%s' "$verdict" | sed -n 's/.*ACTIVE_TOTAL=\([0-9]*\).*/\1/p')
7426
+ waived=$(printf '%s' "$verdict" | sed -n 's/.*WAIVED=\([0-9]*\).*/\1/p')
7427
+ active_high=${active_high:-0}
7428
+ active_total=${active_total:-0}
7429
+ waived=${waived:-0}
7430
+
7431
+ if [ "$active_total" -eq 0 ]; then
7432
+ log_info "Security scan: no active findings (waived: $waived)"
7433
+ return 0
7434
+ fi
7435
+
7436
+ # Actionable advisory summary: rule + file + fix, from the receipt json.
7437
+ log_warn "Security scan: $active_total active finding(s) (HIGH: $active_high, waived: $waived)"
7438
+ _SEC_OUT="$out_file" python3 -c '
7439
+ import json, os
7440
+ try:
7441
+ with open(os.environ["_SEC_OUT"]) as f:
7442
+ data = json.load(f)
7443
+ except Exception:
7444
+ data = {"findings": []}
7445
+ for fnd in data.get("findings", []):
7446
+ if fnd.get("waived"):
7447
+ continue
7448
+ print(" [%s] %s %s:%s -- %s | fix: %s" % (
7449
+ fnd.get("severity", "?"), fnd.get("rule", "?"),
7450
+ fnd.get("file", "?"), fnd.get("line", "?"),
7451
+ fnd.get("message", ""), fnd.get("fix", "")))
7452
+ ' 2>/dev/null | while IFS= read -r line; do log_warn "$line"; done
7453
+
7454
+ # OPT-IN BLOCK: only un-waived HIGH findings block, and only when explicitly
7455
+ # enabled. Advisory default returns 0 (never surprise-blocks).
7456
+ if [ "${LOKI_SECURE_GATE:-advisory}" = "block" ] && [ "$active_high" -gt 0 ]; then
7457
+ log_warn "Security gate: $active_high un-waived HIGH finding(s) - BLOCK (LOKI_SECURE_GATE=block)"
7458
+ return 1
7459
+ fi
7460
+ return 0
7461
+ }
7462
+
7312
7463
  #===============================================================================
7313
7464
  # Gate Failure Tracking (v6.10.0)
7314
7465
  #===============================================================================
@@ -15262,6 +15413,18 @@ if __name__ == "__main__":
15262
15413
  log_warn "Static analysis FAILED ($sa_count consecutive) - findings injected into next iteration"
15263
15414
  fi
15264
15415
  fi
15416
+ # Secure-by-default scan (v7.87.0). Advisory by default (never
15417
+ # blocks); records .loki/quality/security-findings.json each
15418
+ # iteration. Blocks only on un-waived HIGH when LOKI_SECURE_GATE=block.
15419
+ log_info "Quality gate: security scan (advisory)..."
15420
+ if run_secure_scan; then
15421
+ clear_gate_failure "security_scan"
15422
+ else
15423
+ local sec_count
15424
+ sec_count=$(track_gate_failure "security_scan")
15425
+ gate_failures="${gate_failures}security_scan,"
15426
+ log_warn "Security gate BLOCKED ($sec_count consecutive) - un-waived HIGH findings (LOKI_SECURE_GATE=block)"
15427
+ fi
15265
15428
  # BUG-ST-002: Check pause signal between quality gates
15266
15429
  if [ -f "${TARGET_DIR:-.}/.loki/PAUSE" ] || [ -f "${TARGET_DIR:-.}/.loki/STOP" ]; then
15267
15430
  log_warn "Pause/stop signal detected between quality gates - deferring remaining gates"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.85.0"
10
+ __version__ = "7.87.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -10110,6 +10110,62 @@ async def list_proofs():
10110
10110
  return {"proofs": items}
10111
10111
 
10112
10112
 
10113
+ @app.get("/api/proofs/summary",
10114
+ dependencies=[Depends(auth.require_scope("read"))])
10115
+ async def proofs_summary():
10116
+ """Honest aggregate over the active project's Evidence Receipts.
10117
+
10118
+ Counts are computed ONLY from real proof.json files; nothing is invented.
10119
+ The single source of truth for "verified" is the v1.1 deterministic
10120
+ honesty.headline (proof-generator.py::_compute_headline), which a forger
10121
+ cannot turn green without real exit_code:0 evidence. Buckets:
10122
+
10123
+ verified -> honesty.headline == "VERIFIED"
10124
+ with_gaps -> honesty.headline == "VERIFIED WITH GAPS"
10125
+ not_verified -> honesty.headline == "NOT VERIFIED"
10126
+ unknown -> no honesty block (schema v1.0 proofs) or any other/
10127
+ missing headline. We refuse to count these as verified
10128
+ because we cannot prove they were.
10129
+
10130
+ Empty or missing proofs dir -> all zeros (200), an honest empty state.
10131
+ Mirrors list_proofs' iteration + _safe_json_read so the counts can never
10132
+ drift from what the list endpoint shows.
10133
+ """
10134
+ proofs_dir = _proofs_dir()
10135
+ total = verified = with_gaps = not_verified = unknown = 0
10136
+ try:
10137
+ entries = sorted(proofs_dir.iterdir())
10138
+ except (OSError, FileNotFoundError):
10139
+ entries = []
10140
+ for entry in entries:
10141
+ if not entry.is_dir():
10142
+ continue
10143
+ proof_json = entry / "proof.json"
10144
+ if not proof_json.is_file():
10145
+ continue
10146
+ data = _safe_json_read(proof_json, default=None)
10147
+ if not isinstance(data, dict):
10148
+ continue
10149
+ total += 1
10150
+ honesty = data.get("honesty")
10151
+ headline = honesty.get("headline") if isinstance(honesty, dict) else None
10152
+ if headline == "VERIFIED":
10153
+ verified += 1
10154
+ elif headline == "VERIFIED WITH GAPS":
10155
+ with_gaps += 1
10156
+ elif headline == "NOT VERIFIED":
10157
+ not_verified += 1
10158
+ else:
10159
+ unknown += 1
10160
+ return {
10161
+ "total_receipts": total,
10162
+ "verified": verified,
10163
+ "with_gaps": with_gaps,
10164
+ "not_verified": not_verified,
10165
+ "unknown": unknown,
10166
+ }
10167
+
10168
+
10113
10169
  @app.get("/api/proofs/{run_id}", dependencies=[Depends(auth.require_scope("read"))])
10114
10170
  async def get_proof(run_id: str):
10115
10171
  """Return the redacted proof.json for one run."""
@@ -204,6 +204,41 @@
204
204
  font-weight: 500;
205
205
  }
206
206
 
207
+ /* Verified-receipts badge: an honest trust signal next to the brand.
208
+ Counts are pulled live from /api/proofs/summary and reflect only real,
209
+ deterministic Evidence Receipts. Hidden until we have data; shows a muted
210
+ "No receipts yet" at zero; never a fabricated number. */
211
+ .receipts-badge {
212
+ display: none;
213
+ align-items: center;
214
+ gap: 5px;
215
+ margin-top: 6px;
216
+ padding: 3px 8px;
217
+ width: fit-content;
218
+ max-width: 100%;
219
+ border: 1px solid var(--loki-border);
220
+ border-radius: 999px;
221
+ background: var(--loki-bg-card);
222
+ font-family: 'Inter', system-ui, sans-serif;
223
+ font-size: 10px;
224
+ font-weight: 500;
225
+ line-height: 1.2;
226
+ color: var(--loki-text-secondary);
227
+ cursor: default;
228
+ }
229
+ .receipts-badge.show { display: inline-flex; }
230
+ .receipts-badge.empty { color: var(--loki-text-muted); }
231
+ .receipts-badge .receipts-dot {
232
+ width: 6px;
233
+ height: 6px;
234
+ border-radius: 50%;
235
+ background: var(--loki-success);
236
+ flex: 0 0 auto;
237
+ }
238
+ .receipts-badge.empty .receipts-dot { background: var(--loki-text-muted); }
239
+ .receipts-badge .receipts-verified { color: var(--loki-success); font-weight: 600; }
240
+ .receipts-badge.empty .receipts-verified { color: var(--loki-text-muted); font-weight: 500; }
241
+
207
242
  /* Navigation: the only scrolling region. min-height:0 + overflow-y:auto so
208
243
  a long grouped nav scrolls within the sidebar while the header + footer
209
244
  stay pinned. */
@@ -851,6 +886,15 @@
851
886
  </button>
852
887
  <span class="logo-brand">Loki Mode</span>
853
888
  <span class="logo-subtitle">powered by Autonomi</span>
889
+ <!-- Verified-receipts badge: honest trust signal. Populated at runtime
890
+ from /api/proofs/summary; hidden until data arrives, shows a muted
891
+ empty state at zero, and degrades silently if the endpoint is
892
+ unavailable. Every receipt is a deterministic, re-verifiable
893
+ Evidence Receipt (loki proof verify). -->
894
+ <span class="receipts-badge" id="receipts-badge" role="status">
895
+ <span class="receipts-dot" aria-hidden="true"></span>
896
+ <span id="receipts-badge-text"></span>
897
+ </span>
854
898
  <!-- v7.84 single project switcher: ONE searchable <select> with two
855
899
  <optgroup>s ("Running" and "All projects"), built at runtime from
856
900
  /api/running-projects. A running-app count pill sits beside it; the
@@ -15250,6 +15294,55 @@ document.addEventListener('DOMContentLoaded', function() {
15250
15294
  } catch (err) { /* polling fallback still covers it */ }
15251
15295
  })();
15252
15296
 
15297
+ // Verified-receipts badge: honest trust signal beside the brand. Fetches the
15298
+ // real aggregate from /api/proofs/summary and shows "N receipts - M
15299
+ // verified". At zero it shows a muted "No receipts yet"; if the endpoint is
15300
+ // unavailable it stays hidden (no error spew). We never display a number the
15301
+ // data does not support: "verified" here is the deterministic
15302
+ // honesty.headline == VERIFIED count, re-verifiable via loki proof verify.
15303
+ (function initReceiptsBadge() {
15304
+ var badge = document.getElementById('receipts-badge');
15305
+ var textEl = document.getElementById('receipts-badge-text');
15306
+ if (!badge || !textEl) return;
15307
+
15308
+ function plural(n, word) { return n + ' ' + word + (n === 1 ? '' : 's'); }
15309
+
15310
+ function render(s) {
15311
+ var total = (s && typeof s.total_receipts === 'number') ? s.total_receipts : 0;
15312
+ var verified = (s && typeof s.verified === 'number') ? s.verified : 0;
15313
+ if (total <= 0) {
15314
+ // Honest empty state: no fabricated number.
15315
+ badge.classList.add('empty');
15316
+ textEl.textContent = 'No receipts yet';
15317
+ badge.title = 'Evidence Receipts appear here once Loki completes a '
15318
+ + 'verified run. Each is deterministic and re-verifiable with '
15319
+ + '"loki proof verify".';
15320
+ badge.classList.add('show');
15321
+ return;
15322
+ }
15323
+ badge.classList.remove('empty');
15324
+ // "N receipts - M verified" with the verified count emphasized.
15325
+ textEl.innerHTML = plural(total, 'receipt') + ' - '
15326
+ + '<span class="receipts-verified"></span> verified';
15327
+ var vEl = textEl.querySelector('.receipts-verified');
15328
+ if (vEl) vEl.textContent = String(verified);
15329
+ badge.title = plural(verified, 'receipt') + ' of ' + total
15330
+ + ' verified by a deterministic Evidence Receipt (re-verifiable with '
15331
+ + '"loki proof verify"). "Verified" means tests passed with real '
15332
+ + 'exit-code evidence, not an LLM opinion.';
15333
+ badge.classList.add('show');
15334
+ }
15335
+
15336
+ function poll() {
15337
+ fetch('/api/proofs/summary', { headers: { 'Accept': 'application/json' } })
15338
+ .then(function (r) { return r.ok ? r.json() : null; })
15339
+ .then(function (d) { if (d) render(d); })
15340
+ .catch(function () { /* endpoint unavailable: leave badge hidden */ });
15341
+ }
15342
+ poll();
15343
+ setInterval(poll, 30000);
15344
+ })();
15345
+
15253
15346
  // Mobile menu toggle
15254
15347
  var mobileMenuBtn = document.getElementById('mobile-menu-btn');
15255
15348
  var sidebar = document.getElementById('sidebar');
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.85.0
5
+ **Version:** v7.87.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.85.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.87.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.85.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=UQ(WQ(import.meta.url)),Z=t$(Q);Q$=VQ(JQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var e$=L(()=>{C()});var g1={};b(g1,{runOrThrow:()=>HQ,run:()=>F,readStreamCapped:()=>m1,commandVersion:()=>BQ,commandExists:()=>f,ShellError:()=>$1,MAX_STDOUT_BYTES:()=>v1});async function m1($,Q=v1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:U}=await Z.read();if(K)break;if(!U)continue;if(q+=U.byteLength,q>Q){let J=U.byteLength-(q-Q);X+=z.decode(U.subarray(0,J),{stream:!0});break}X+=z.decode(U,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,U]=await Promise.all([m1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function HQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new $1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=GQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function GQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function BQ($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var v1=16777216,$1;var d=L(()=>{$1=class $1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return YQ?"":$}var YQ,O,S,_,_Z,I,k,h,V;var c=L(()=>{YQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as jQ}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(jQ($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var Y$;var V$=L(()=>{d()});var X0={};b(X0,{runStatus:()=>oQ});import{existsSync as y,readFileSync as U$,readdirSync as r1,statSync as t1}from"fs";import{resolve as D,basename as vQ}from"path";import{homedir as mQ}from"os";function i1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function e1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*N$/Q);if(X>N$)X=N$;let q=N$-X,K=S;if(z>=80)K=O;else if(z>=50)K=_;let U="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=i1($),W=i1(Q);return` ${k}${Z}${V} ${K}[${U}]${V} ${z}% (${J} / ${W})`}async function fQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}Error: jq is required but not installed.${V}
2
+ var QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.87.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=UQ(WQ(import.meta.url)),Z=t$(Q);Q$=VQ(JQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var e$=L(()=>{C()});var g1={};b(g1,{runOrThrow:()=>HQ,run:()=>F,readStreamCapped:()=>m1,commandVersion:()=>BQ,commandExists:()=>f,ShellError:()=>$1,MAX_STDOUT_BYTES:()=>v1});async function m1($,Q=v1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:U}=await Z.read();if(K)break;if(!U)continue;if(q+=U.byteLength,q>Q){let J=U.byteLength-(q-Q);X+=z.decode(U.subarray(0,J),{stream:!0});break}X+=z.decode(U,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,U]=await Promise.all([m1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function HQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new $1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=GQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function GQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function BQ($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var v1=16777216,$1;var d=L(()=>{$1=class $1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return YQ?"":$}var YQ,O,S,_,_Z,I,k,h,V;var c=L(()=>{YQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as jQ}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(jQ($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var Y$;var V$=L(()=>{d()});var X0={};b(X0,{runStatus:()=>oQ});import{existsSync as y,readFileSync as U$,readdirSync as r1,statSync as t1}from"fs";import{resolve as D,basename as vQ}from"path";import{homedir as mQ}from"os";function i1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function e1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*N$/Q);if(X>N$)X=N$;let q=N$-X,K=S;if(z>=80)K=O;else if(z>=50)K=_;let U="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=i1($),W=i1(Q);return` ${k}${Z}${V} ${K}[${U}]${V} ${z}% (${J} / ${W})`}async function fQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}Error: jq is required but not installed.${V}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -796,4 +796,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
796
796
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
797
797
  `),process.stderr.write($Q),2}}s1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
798
798
 
799
- //# debugId=05CD23D774CD678C64756E2164756E21
799
+ //# debugId=7982E696DB64940A64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.85.0'
60
+ __version__ = '7.87.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.85.0",
4
+ "version": "7.87.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.85.0",
5
+ "version": "7.87.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",