loki-mode 9.16.0 → 9.17.2

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.
@@ -1323,6 +1323,14 @@
1323
1323
  <h3 style="font-family: 'Fraunces', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Logs</h3>
1324
1324
  <loki-log-stream id="log-stream" auto-scroll max-lines="500"></loki-log-stream>
1325
1325
  </div>
1326
+ <!-- LEARNINGS. /api/learnings held real records with a rootCause,
1327
+ a fix and a preventInFuture, and had no UI consumer -- the
1328
+ system was learning from its runs and never showing anyone
1329
+ what it learned. Hidden until data arrives. -->
1330
+ <div id="learnings-panel" style="display:none;">
1331
+ <h3 style="font-family: var(--loki-font-family, 'Inter', system-ui, -apple-system, sans-serif); font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">What this build learned</h3>
1332
+ <div id="learnings-list"></div>
1333
+ </div>
1326
1334
  <div>
1327
1335
  <h3 style="font-family: 'Fraunces', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Memory</h3>
1328
1336
  <loki-memory-browser id="memory-browser" tab="summary"></loki-memory-browser>
@@ -1675,6 +1683,13 @@
1675
1683
  <div class="section-page-header">
1676
1684
  <h2 class="section-page-title">Cost</h2>
1677
1685
  </div>
1686
+ <!-- SPEND CAP. /api/budget reports budget_limit, and it ships NULL:
1687
+ there is no automatic spend stop. Nothing in the UI said so, and
1688
+ the only place that fact surfaced was a bill. Shown ALWAYS, not
1689
+ just when a cap exists, because "no cap" is the state a user most
1690
+ needs to know about. -->
1691
+ <div id="budget-banner" style="display:none;margin-bottom:12px;padding:10px 12px;
1692
+ border:1px solid var(--loki-border);border-radius:6px;font-size:12px;"></div>
1678
1693
  <loki-cost-dashboard id="cost-dashboard"></loki-cost-dashboard>
1679
1694
  </div>
1680
1695
 
@@ -1685,8 +1700,18 @@
1685
1700
  <div class="section-page-header">
1686
1701
  <h2 class="section-page-title">Trust Trajectory</h2>
1687
1702
  </div>
1703
+ <!-- EVIDENCE RECEIPTS. /api/proofs served 9 receipts with verdict,
1704
+ file count and an HTML view, and NOTHING in the dashboard read it
1705
+ -- only /api/proofs/summary (the header badge) was consumed. The
1706
+ receipt is the thing we ask users to check; it was unreachable
1707
+ from the UI. -->
1708
+ <div id="receipts-panel" style="margin-bottom:16px;display:none;">
1709
+ <h3 style="font-size:13px;font-weight:600;margin:0 0 8px;">Evidence receipts</h3>
1710
+ <div id="receipts-list"></div>
1711
+ <div id="receipts-note" style="font-size:11px;color:var(--loki-text-muted);margin-top:8px;"></div>
1712
+ </div>
1688
1713
  <iframe id="trust-frame" title="Trust trajectory" src="about:blank"
1689
- style="width:100%;height:calc(100vh - 160px);border:0;border-radius:8px;background:var(--loki-bg-primary);"></iframe>
1714
+ style="width:100%;height:calc(100vh - 260px);border:0;border-radius:8px;background:var(--loki-bg-primary);"></iframe>
1690
1715
  </div>
1691
1716
 
1692
1717
  <!-- Checkpoints -->
@@ -11317,7 +11342,7 @@ var LokiDashboard=(()=>{var Be=Object.defineProperty;var zt=Object.getOwnPropert
11317
11342
  </div>
11318
11343
  `}
11319
11344
  </div>
11320
- `,this.shadowRoot.querySelectorAll("[data-tab]").forEach(l=>{l.addEventListener("click",c=>this._handleTabClick(c))});let r=this.shadowRoot.getElementById("tool-time-range");r&&r.addEventListener("change",l=>this._handleTimeRangeChange(l));let o=this.shadowRoot.getElementById("analytics-retry-btn");o&&o.addEventListener("click",()=>this._loadData());let n=this.shadowRoot.getElementById("analytics-overview-btn");n&&n.addEventListener("click",()=>this._navigateToOverview())}_navigateToOverview(){this.dispatchEvent(new CustomEvent("loki-navigate",{detail:{view:"overview"},bubbles:!0,composed:!0}))}};customElements.get("loki-analytics")||customElements.define("loki-analytics",ve);var kt={pass:{color:"var(--loki-green, #22c55e)",bg:"var(--loki-green-muted, rgba(34, 197, 94, 0.15))",label:"PASS"},fail:{color:"var(--loki-red, #ef4444)",bg:"var(--loki-red-muted, rgba(239, 68, 68, 0.15))",label:"FAIL"},pending:{color:"var(--loki-yellow, #eab308)",bg:"var(--loki-yellow-muted, rgba(234, 179, 8, 0.15))",label:"PENDING"}};function Qt(d){if(!d)return"Never";try{return new Date(d).toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return"Unknown"}}function Xt(d){if(!d||d.length===0)return{pass:0,fail:0,pending:0,total:0};let e={pass:0,fail:0,pending:0,total:d.length};for(let t of d){let i=(t.status||"pending").toLowerCase();i==="pass"?e.pass++:i==="fail"?e.fail++:e.pending++}return e}var fe=class extends v{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._gates=[],this._evidence={blocked:!1},this._pollInterval=null,this._lastDataHash=null,this._scanning=!1}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&this._api&&(this._api=h({baseUrl:i}),this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=h({baseUrl:e})}_startPolling(){this._poll=b({loadFn:()=>this._loadData(),intervalMs:3e4,element:this,immediate:!1})}_stopPolling(){this._poll&&(this._poll.stop(),this._poll=null)}async _loadData(){let e=this._api;try{this._loading=!0;let t=await e._get("/api/council/gate");if(e!==this._api)return;let i=t?.gates||t||[],a=t?.evidence||{blocked:!1},s=JSON.stringify({gates:i,evidence:a});if(s===this._lastDataHash)return;this._lastDataHash=s,this._gates=Array.isArray(i)?i:[],this._evidence=a,this._error=null}catch(t){if(e!==this._api)return;this._error||(this._error=`Failed to load quality gates: ${t.message}`)}finally{this._loading=!1}this.render()}async _triggerScan(){if(this._scanning)return;let e=this._api;this._scanning=!0,this.render();try{if(await e._post("/api/quality-scan",{},{timeout:3e5}),e!==this._api)return;this._lastDataHash=null,await this._loadData()}catch(t){if(e!==this._api)return;this._error=`Quality scan failed: ${t.message}`}finally{this._scanning=!1,this.render()}}_escapeHtml(e){return e?String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_getStyles(){return`
11345
+ `,this.shadowRoot.querySelectorAll("[data-tab]").forEach(l=>{l.addEventListener("click",c=>this._handleTabClick(c))});let r=this.shadowRoot.getElementById("tool-time-range");r&&r.addEventListener("change",l=>this._handleTimeRangeChange(l));let o=this.shadowRoot.getElementById("analytics-retry-btn");o&&o.addEventListener("click",()=>this._loadData());let n=this.shadowRoot.getElementById("analytics-overview-btn");n&&n.addEventListener("click",()=>this._navigateToOverview())}_navigateToOverview(){this.dispatchEvent(new CustomEvent("loki-navigate",{detail:{view:"overview"},bubbles:!0,composed:!0}))}};customElements.get("loki-analytics")||customElements.define("loki-analytics",ve);var kt={pass:{color:"var(--loki-green, #22c55e)",bg:"var(--loki-green-muted, rgba(34, 197, 94, 0.15))",label:"PASS"},fail:{color:"var(--loki-red, #ef4444)",bg:"var(--loki-red-muted, rgba(239, 68, 68, 0.15))",label:"FAIL"},pending:{color:"var(--loki-yellow, #eab308)",bg:"var(--loki-yellow-muted, rgba(234, 179, 8, 0.15))",label:"PENDING"}};function Qt(d){if(!d)return"Never";try{return new Date(d).toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return"Unknown"}}function Xt(d){if(!d||d.length===0)return{pass:0,fail:0,pending:0,total:0};let e={pass:0,fail:0,pending:0,total:d.length};for(let t of d){let i=(t.status||"pending").toLowerCase();i==="pass"?e.pass++:i==="fail"?e.fail++:e.pending++}return e}var fe=class extends v{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._gates=[],this._evidence={blocked:!1},this._policy=null,this._pollInterval=null,this._lastDataHash=null,this._scanning=!1}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&this._api&&(this._api=h({baseUrl:i}),this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=h({baseUrl:e})}_startPolling(){this._poll=b({loadFn:()=>this._loadData(),intervalMs:3e4,element:this,immediate:!1})}_stopPolling(){this._poll&&(this._poll.stop(),this._poll=null)}_policyLine(e){if(!Array.isArray(this._policy)||!e)return"";let i={"Static Analysis":"static_analysis","Test Suite":"test_coverage","Blind Code Review":"code_review","Anti-Sycophancy":"anti_sycophancy","Mock Integrity":"mock_integrity","Test Mutation":"mutation_integrity","Documentation Coverage":"doc_coverage","Magic Modules Debate":"magic_debate"}[e];if(!i)return"";let a=this._policy.find(l=>l&&l.gate===i);if(!a||!a.mode)return"";let s=a.mode==="blocking",r=a.audit_hits===null||a.audit_hits===void 0?"not measured":`${a.audit_hits} hit${a.audit_hits===1?"":"s"}`,o=s?"BLOCKS":"advisory",n=!s&&a.promote_with?` -- promote with ${this._escapeHtml(a.promote_with)}`:"";return`<div class="gate-meta gate-policy">${o} &middot; ${r}${n}</div>`}async _loadData(){let e=this._api;try{this._loading=!0;let t=await e._get("/api/council/gate");if(e!==this._api)return;let i=t?.gates||t||[],a=t?.evidence||{blocked:!1},s=null;try{let o=await e._get("/api/gate-policy");if(e!==this._api)return;s=Array.isArray(o?.gates)?o.gates:null}catch{s=null}let r=JSON.stringify({gates:i,evidence:a,policy:s});if(r===this._lastDataHash)return;this._lastDataHash=r,this._gates=Array.isArray(i)?i:[],this._evidence=a,this._policy=s,this._error=null}catch(t){if(e!==this._api)return;this._error||(this._error=`Failed to load quality gates: ${t.message}`)}finally{this._loading=!1}this.render()}async _triggerScan(){if(this._scanning)return;let e=this._api;this._scanning=!0,this.render();try{if(await e._post("/api/quality-scan",{},{timeout:3e5}),e!==this._api)return;this._lastDataHash=null,await this._loadData()}catch(t){if(e!==this._api)return;this._error=`Quality scan failed: ${t.message}`}finally{this._scanning=!1,this.render()}}_escapeHtml(e){return e?String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_getStyles(){return`
11321
11346
  :host {
11322
11347
  display: block;
11323
11348
  }
@@ -11593,6 +11618,7 @@ var LokiDashboard=(()=>{var Be=Object.defineProperty;var zt=Object.getOwnPropert
11593
11618
  </div>
11594
11619
  ${p.description?`<div class="gate-description">${this._escapeHtml(p.description)}</div>`:""}
11595
11620
  <div class="gate-meta">Last checked: ${Qt(p.last_checked||p.lastChecked)}</div>
11621
+ ${this._policyLine(p.name)}
11596
11622
  </div>
11597
11623
  `}).join("")}</div>`;let r="",o=this._evidence||{};if(o.blocked){let c={empty_diff:"No changes were shipped (empty diff vs run start).",tests_red:"Tests ran and were red.",empty_diff_and_tests_red:"No changes shipped and tests were red.",no_evidence_of_completion:"No evidence of completion."},p=o.error?this._escapeHtml(o.error):c[o.reason]||this._escapeHtml(o.reason||"Completion blocked."),u=Array.isArray(o.failures)?o.failures:[],m=u.length?`<ul class="evidence-failures">${u.map(g=>`<li>${this._escapeHtml(g)}</li>`).join("")}</ul>`:"";r=`
11598
11624
  <div class="evidence-banner">
@@ -16021,6 +16047,143 @@ document.addEventListener('DOMContentLoaded', function() {
16021
16047
  badge.classList.add('show');
16022
16048
  }
16023
16049
 
16050
+ // SPEND CAP STATE. budget_limit is NULL by default -- LOKI_BUDGET_LIMIT
16051
+ // ships unset, so a long run has no automatic stop. That is a defensible
16052
+ // default (a run killed at a threshold the user never chose is worse), but
16053
+ // it must not be INVISIBLE: today the only place it surfaced was a bill.
16054
+ //
16055
+ // Renders the no-cap state as prominently as a cap, and names the variable
16056
+ // that sets one. Never invents a number: an unknown current cost reads
16057
+ // "not measured", not $0.00.
16058
+ window.loadBudget = function () {
16059
+ var el = document.getElementById('budget-banner');
16060
+ if (!el) return;
16061
+ fetch('/api/budget', { headers: { 'Accept': 'application/json' } })
16062
+ .then(function (r) { return r.ok ? r.json() : null; })
16063
+ .then(function (d) {
16064
+ if (!d) return;
16065
+ var lim = d.budget_limit;
16066
+ var cur = d.current_cost;
16067
+ var curTxt = (cur === null || cur === undefined) ? 'not measured'
16068
+ : ('$' + Number(cur).toFixed(2));
16069
+ if (lim === null || lim === undefined) {
16070
+ el.style.borderColor = 'var(--loki-warning)';
16071
+ el.innerHTML = '<strong>No spend cap set.</strong> This run will not stop on cost. '
16072
+ + 'Spent so far: ' + curTxt + '. '
16073
+ + 'Set one with <code>LOKI_BUDGET_LIMIT=&lt;usd&gt;</code>.';
16074
+ } else if (d.exceeded) {
16075
+ el.style.borderColor = 'var(--loki-error)';
16076
+ el.innerHTML = '<strong>Budget exceeded.</strong> Cap $' + Number(lim).toFixed(2)
16077
+ + ', spent ' + curTxt + '.';
16078
+ } else {
16079
+ var rem = (d.remaining === null || d.remaining === undefined)
16080
+ ? 'not measured' : ('$' + Number(d.remaining).toFixed(2));
16081
+ el.style.borderColor = 'var(--loki-border)';
16082
+ el.innerHTML = 'Spend cap $' + Number(lim).toFixed(2)
16083
+ + '. Spent ' + curTxt + ', remaining ' + rem + '.';
16084
+ }
16085
+ el.style.display = 'block';
16086
+ })
16087
+ .catch(function () { /* leave hidden */ });
16088
+ };
16089
+
16090
+ // LEARNINGS. What the build learned from its own gate failures.
16091
+ //
16092
+ // /api/learnings held real records -- rootCause, fix, preventInFuture --
16093
+ // with no UI consumer. The system was learning from its runs and showing
16094
+ // nobody, which makes the memory unfalsifiable: a user cannot correct a
16095
+ // learning they cannot see. Devin's "misleading knowledge" surface is the
16096
+ // same idea and the reason it is worth showing.
16097
+ //
16098
+ // Renders the record's OWN words. Nothing is summarised or re-derived: a
16099
+ // paraphrased root cause is a second claim about a claim.
16100
+ window.loadLearnings = function () {
16101
+ var panel = document.getElementById('learnings-panel');
16102
+ var list = document.getElementById('learnings-list');
16103
+ if (!panel || !list) return;
16104
+ fetch('/api/learnings', { headers: { 'Accept': 'application/json' } })
16105
+ .then(function (r) { return r.ok ? r.json() : null; })
16106
+ .then(function (d) {
16107
+ if (!d) return;
16108
+ var rows = Array.isArray(d) ? d : (d.learnings || []);
16109
+ if (!rows.length) return; // nothing learned yet: say nothing
16110
+ rows = rows.slice().reverse().slice(0, 8);
16111
+ var html = '';
16112
+ for (var i = 0; i < rows.length; i++) {
16113
+ var x = rows[i] || {};
16114
+ var when = x.timestamp ? String(x.timestamp).slice(0, 16).replace('T', ' ') : '-';
16115
+ var iter = (x.iteration === null || x.iteration === undefined) ? '-' : ('iter ' + x.iteration);
16116
+ html += '<div style="padding:8px;border-bottom:1px solid var(--loki-border);font-size:12px;">'
16117
+ + '<div style="display:flex;gap:10px;color:var(--loki-text-muted);margin-bottom:4px;">'
16118
+ + '<span>' + when + '</span><span>' + iter + '</span>'
16119
+ + '<span>' + String(x.trigger || 'unknown trigger') + '</span></div>'
16120
+ + '<div style="margin-bottom:3px;"><strong>cause:</strong> '
16121
+ + String(x.rootCause || 'not recorded') + '</div>'
16122
+ + (x.fix ? '<div style="margin-bottom:3px;"><strong>fix:</strong> ' + String(x.fix) + '</div>' : '')
16123
+ + (x.preventInFuture ? '<div style="color:var(--loki-text-muted);"><strong>prevent:</strong> '
16124
+ + String(x.preventInFuture) + '</div>' : '')
16125
+ + '</div>';
16126
+ }
16127
+ list.innerHTML = html;
16128
+ panel.style.display = 'block';
16129
+ })
16130
+ .catch(function () { /* leave hidden */ });
16131
+ };
16132
+
16133
+ // RECEIPT LIST. Fetched on demand when the Trust section opens, not on a
16134
+ // timer: it is a history view, and polling it would spend a request every
16135
+ // 30s on data that only changes when a run finishes.
16136
+ //
16137
+ // Every field rendered comes straight from the receipt. Nothing is derived,
16138
+ // and an absent value renders as "-" rather than a zero or a guess -- a
16139
+ // fabricated 0 cost or a blank verdict read as "clean" is the exact false
16140
+ // green the receipt exists to prevent.
16141
+ window.loadReceipts = function () {
16142
+ var panel = document.getElementById('receipts-panel');
16143
+ var list = document.getElementById('receipts-list');
16144
+ var note = document.getElementById('receipts-note');
16145
+ if (!panel || !list) return;
16146
+ fetch('/api/proofs', { headers: { 'Accept': 'application/json' } })
16147
+ .then(function (r) { return r.ok ? r.json() : null; })
16148
+ .then(function (d) {
16149
+ if (!d) return; // endpoint absent: leave hidden
16150
+ var rows = Array.isArray(d) ? d : (d.proofs || d.receipts || []);
16151
+ if (!rows.length) return; // no receipts yet: say nothing
16152
+ rows = rows.slice().reverse().slice(0, 10);
16153
+ var html = '';
16154
+ for (var i = 0; i < rows.length; i++) {
16155
+ var x = rows[i] || {};
16156
+ var verdict = x.headline || x.final_verdict || 'UNKNOWN';
16157
+ var verified = /^VERIFIED/i.test(verdict);
16158
+ var col = verified ? 'var(--loki-success)' : 'var(--loki-text-muted)';
16159
+ var files = (x.files_changed === null || x.files_changed === undefined)
16160
+ ? '-' : String(x.files_changed);
16161
+ var cost = (x.cost_usd === null || x.cost_usd === undefined)
16162
+ ? '-' : ('$' + Number(x.cost_usd).toFixed(2));
16163
+ var when = x.generated_at ? String(x.generated_at).slice(0, 16).replace('T', ' ') : '-';
16164
+ var link = x.has_html
16165
+ ? ('<a href="/api/proofs/' + encodeURIComponent(x.run_id || '') + '/html"'
16166
+ + ' target="_blank" rel="noopener" style="color:var(--loki-accent);">open</a>')
16167
+ : '<span style="color:var(--loki-text-muted);">no html</span>';
16168
+ html += '<div style="display:flex;gap:12px;align-items:center;padding:6px 8px;'
16169
+ + 'border-bottom:1px solid var(--loki-border);font-size:12px;">'
16170
+ + '<span style="color:' + col + ';font-weight:600;min-width:130px;">'
16171
+ + String(verdict).slice(0, 22) + '</span>'
16172
+ + '<span style="color:var(--loki-text-muted);min-width:120px;">' + when + '</span>'
16173
+ + '<span style="min-width:90px;">' + files + ' files</span>'
16174
+ + '<span style="min-width:70px;">' + cost + '</span>'
16175
+ + link + '</div>';
16176
+ }
16177
+ list.innerHTML = html;
16178
+ if (note) {
16179
+ note.textContent = 'Newest first. "open" renders the receipt itself. '
16180
+ + 'Re-check any of them: loki proof verify <run-id>';
16181
+ }
16182
+ panel.style.display = 'block';
16183
+ })
16184
+ .catch(function () { /* leave hidden: no receipt surface is better than a wrong one */ });
16185
+ };
16186
+
16024
16187
  function poll() {
16025
16188
  fetch('/api/proofs/summary', { headers: { 'Accept': 'application/json' } })
16026
16189
  .then(function (r) { return r.ok ? r.json() : null; })
@@ -16065,7 +16228,10 @@ document.addEventListener('DOMContentLoaded', function() {
16065
16228
  // document and cannot see the SPA's manual data-loki-theme toggle, so we
16066
16229
  // pass the resolved theme as a query param (?theme=dark|light); the
16067
16230
  // standalone page reads it and matches. v7.18.0.
16231
+ if (sectionId === 'insights') { loadLearnings(); }
16232
+ if (sectionId === 'cost') { loadBudget(); }
16068
16233
  if (sectionId === 'trust') {
16234
+ loadReceipts();
16069
16235
  var tframe = document.getElementById('trust-frame');
16070
16236
  if (tframe && (!tframe.src || tframe.src === 'about:blank' ||
16071
16237
  tframe.getAttribute('src') === 'about:blank')) {
@@ -25,6 +25,8 @@ different answer on your hardware.
25
25
  | FULL local gate | 23 to 26 minutes | `LOCAL_CI_TIER=full bash scripts/local-ci.sh`, 166 checks, five runs on an M-series Mac |
26
26
  | FAST local gate | about 1 minute | `bash scripts/local-ci.sh`, the documented pre-push tier |
27
27
  | Shell suite, sharded | 352 seconds | 323 suites, `LOCAL_CI_SHARDS=4`; 1440 seconds serial, so 4.1x |
28
+ | `loki verify` | 77 seconds | `loki verify HEAD~1` on this repository, 5 changed files; dominated by the project's own test suite, not by our gates |
29
+ | of which, shipped-vs-dev CVE split | 418 ms | one extra `npm audit --omit=dev`; 0.5% of the run, and the reason the audit finding can say whether a CVE reaches users |
28
30
  | `loki outcomes` | under 1 second per receipt | `git blame` and one `git log` per changed file |
29
31
  | `loki intent status` | milliseconds | hash comparison against `.loki/spec/spec.lock` |
30
32
  | Agent readiness | milliseconds | filesystem checks only, no network |
@@ -50,6 +52,24 @@ non-forgeability requires the signed path
50
52
  (`LOKI_PROOF_GPG_KEY`, see [SIGNED-RECEIPTS.md](SIGNED-RECEIPTS.md)). We removed
51
53
  our own "non-forgeable" claim in v7.111.0 after finding it false on that path.
52
54
 
55
+ **The tests gate depends on correctly identifying YOUR test runner, and it has
56
+ been wrong before.** Until 2026-08-07 the runner was chosen by grepping
57
+ `package.json` for `"jest"` / `"vitest"` / `"mocha"`, which matches a
58
+ **devDependency**. This repository is the case that exposed it: jest is a
59
+ devDependency with no jest config while `scripts.test` runs `bash -n` plus
60
+ `node --test`, so verify ran jest, jest globbed 895 files that are not jest
61
+ tests, and `loki verify` returned BLOCKED on a clean tree -- permanently, for a
62
+ defect that did not exist. It now reads `scripts.test` with a JSON parser and
63
+ runs what the project declares.
64
+
65
+ We record this rather than quietly fixing it because a false BLOCK is the more
66
+ damaging direction of that error: a gate that cries wolf on every run trains
67
+ you to ignore the verdict, which costs more than the gate ever earned. If the
68
+ tests gate reports a runner you do not use, that is a bug in our detection, not
69
+ a finding about your code -- the runner is named in
70
+ `.loki/verify/evidence.json` under `deterministic_gates[].runner` so you can
71
+ check which one it picked.
72
+
53
73
  **Only four of the eight quality gates are agent-independent.** Static analysis,
54
74
  mock-integrity, test-mutation and documentation coverage do not ask a model
55
75
  anything. The other four involve model judgment and are labelled ASSESSMENTS