loki-mode 11.54.3 → 11.55.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.
Files changed (68) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/lib/fast_verify.py +142 -18
  4. package/dashboard/__init__.py +1 -1
  5. package/docs/CLI-REFERENCE.md +1 -1
  6. package/docs/INSTALLATION.md +1 -1
  7. package/docs/v10/FAILURE-CLASSES.md +13 -4
  8. package/loki-ts/dist/loki.js +1047 -1043
  9. package/mcp/__init__.py +1 -1
  10. package/package.json +2 -2
  11. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  12. package/web-app/dist/assets/{AdminPage-Dmm3-KKX.js → AdminPage-BMe5dzyh.js} +1 -1
  13. package/web-app/dist/assets/{Avatar-7RrgWzKi.js → Avatar-DyXOOsPy.js} +1 -1
  14. package/web-app/dist/assets/{Badge-BONIA7yv.js → Badge-77uqgz2B.js} +1 -1
  15. package/web-app/dist/assets/{Button-E8PRMj4w.js → Button-r0OlMu23.js} +1 -1
  16. package/web-app/dist/assets/{CockpitPage-DktiMnMn.js → CockpitPage-HYcVxTky.js} +1 -1
  17. package/web-app/dist/assets/{ComparePage-Bv6TXlpZ.js → ComparePage-CTYq0eJh.js} +1 -1
  18. package/web-app/dist/assets/{ErrorBoundary-D4Q4NyU6.js → ErrorBoundary-O-uINmwk.js} +1 -1
  19. package/web-app/dist/assets/{EvidenceReceiptPanel-D-4aSIOD.js → EvidenceReceiptPanel-COZsfe8y.js} +1 -1
  20. package/web-app/dist/assets/{GitHubIssuesPanel-pyZB0ppB.js → GitHubIssuesPanel-ZYX-PcG9.js} +1 -1
  21. package/web-app/dist/assets/{GitHubPRsPanel-C8JzCo4i.js → GitHubPRsPanel-BqnF3V-C.js} +1 -1
  22. package/web-app/dist/assets/{HomePage-8_qQ6qAA.js → HomePage-Dc-S3z3k.js} +2 -2
  23. package/web-app/dist/assets/{LoginPage-BeHT-Edx.js → LoginPage-B18TgU21.js} +1 -1
  24. package/web-app/dist/assets/{MagicPage-CX4ztJG-.js → MagicPage-BChdKANc.js} +1 -1
  25. package/web-app/dist/assets/{MetricsPage-vskkOC9J.js → MetricsPage-BeO8E8J4.js} +1 -1
  26. package/web-app/dist/assets/{NotFoundPage-BGP_FwcI.js → NotFoundPage-ZPGBstWe.js} +1 -1
  27. package/web-app/dist/assets/{ProjectPage-4kWMfATR.js → ProjectPage-Ch1ERk3k.js} +3 -3
  28. package/web-app/dist/assets/{ProjectsPage-BLHLB1XV.js → ProjectsPage-DS-iCrWH.js} +1 -1
  29. package/web-app/dist/assets/{SettingsPage-D4LsHn8x.js → SettingsPage-C7Zfi6WQ.js} +1 -1
  30. package/web-app/dist/assets/{ShowcasePage-DfYFn_YN.js → ShowcasePage-D6Jzs3xn.js} +1 -1
  31. package/web-app/dist/assets/{SystemSettingsPage-CKAavyvT.js → SystemSettingsPage-BUToGQOi.js} +1 -1
  32. package/web-app/dist/assets/{TeamsPage-CC4AMNbY.js → TeamsPage-Ca-imiWe.js} +1 -1
  33. package/web-app/dist/assets/{TemplatesPage-DdMm9p_0.js → TemplatesPage-Cj4PDYvE.js} +1 -1
  34. package/web-app/dist/assets/{TerminalOutput-utetHzR1.js → TerminalOutput-OlfjIMaP.js} +1 -1
  35. package/web-app/dist/assets/{activity-D8bj2llh.js → activity-DGco0i8E.js} +1 -1
  36. package/web-app/dist/assets/{bell-ncgZ86_g.js → bell-CXrA_z4D.js} +1 -1
  37. package/web-app/dist/assets/{bot-BAiWhj3k.js → bot-C3bPbwTv.js} +1 -1
  38. package/web-app/dist/assets/{check-B98KRRY6.js → check-BH95oBUW.js} +1 -1
  39. package/web-app/dist/assets/{chevron-left-BeHgro_s.js → chevron-left-DkDiAgdy.js} +1 -1
  40. package/web-app/dist/assets/{circle-alert-D11Q4DJ2.js → circle-alert-DVm1iRCr.js} +1 -1
  41. package/web-app/dist/assets/{clock-DII7tRwl.js → clock-B9nq6lv2.js} +1 -1
  42. package/web-app/dist/assets/{cloud-BiVq9O0S.js → cloud--spL7-5c.js} +1 -1
  43. package/web-app/dist/assets/{code-xml-CbtEo2gf.js → code-xml-5dQ-OP-m.js} +1 -1
  44. package/web-app/dist/assets/{database-vSiFIjq7.js → database-Irge9nDf.js} +1 -1
  45. package/web-app/dist/assets/{dollar-sign-Crrvv0LV.js → dollar-sign-C-fOwQhD.js} +1 -1
  46. package/web-app/dist/assets/{file-code-corner-BcR71Niu.js → file-code-corner-DL6DXUq9.js} +1 -1
  47. package/web-app/dist/assets/{file-plus-rt0PxW5E.js → file-plus-CpD_jH9N.js} +1 -1
  48. package/web-app/dist/assets/{globe-FGzbkm5z.js → globe-75e1X6Qk.js} +1 -1
  49. package/web-app/dist/assets/{hammer-BlW5Aict.js → hammer-DVmWc_X3.js} +1 -1
  50. package/web-app/dist/assets/{index-DQDv_Igi.js → index-9vlQVMHV.js} +3 -3
  51. package/web-app/dist/assets/{layers-Btfkr8AR.js → layers-C_Ac7df5.js} +1 -1
  52. package/web-app/dist/assets/{loader-circle-DNksvTjl.js → loader-circle-Dwd40Yli.js} +1 -1
  53. package/web-app/dist/assets/{lock-CK3M3U8a.js → lock-B1QdrHw-.js} +1 -1
  54. package/web-app/dist/assets/{package-CAw54KBn.js → package-BIdnnta_.js} +1 -1
  55. package/web-app/dist/assets/{plus-Dc9lYqth.js → plus-djvMCLRk.js} +1 -1
  56. package/web-app/dist/assets/{refresh-cw-BBc-MpJ2.js → refresh-cw-CVw1ki2J.js} +1 -1
  57. package/web-app/dist/assets/{rotate-ccw-u9JmT2vq.js → rotate-ccw-BZuw7sZD.js} +1 -1
  58. package/web-app/dist/assets/{scroll-text-ZcPT96o0.js → scroll-text-BSIjb7vz.js} +1 -1
  59. package/web-app/dist/assets/{server-mJuRWb2u.js → server-CTzeJRJ1.js} +1 -1
  60. package/web-app/dist/assets/{shield-alert-B_KS9HMO.js → shield-alert-DAdTZIbR.js} +1 -1
  61. package/web-app/dist/assets/{thumbs-up-DjZsZxxg.js → thumbs-up-D6dHPYAF.js} +1 -1
  62. package/web-app/dist/assets/{trash-2-0A5x-xNv.js → trash-2-B1iUttVJ.js} +1 -1
  63. package/web-app/dist/assets/{trending-down-CE52PXTy.js → trending-down-B43IPcHY.js} +1 -1
  64. package/web-app/dist/assets/{trending-up-D6TRDISW.js → trending-up-Car5b0e9.js} +1 -1
  65. package/web-app/dist/assets/{upload-QRZTlnwD.js → upload-xtQ8SdqH.js} +1 -1
  66. package/web-app/dist/assets/{usePolling-B6KSIqY4.js → usePolling-CjBn839g.js} +1 -1
  67. package/web-app/dist/assets/{user-CDwLK_9N.js → user-BYZErN7w.js} +1 -1
  68. package/web-app/dist/index.html +1 -1
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Runs an autonomous software factory that knows what it is supposed to deliver and proves it did, with a signed receipt. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
4
4
  ---
5
5
 
6
- # Loki Mode v11.54.3
6
+ # Loki Mode v11.55.0
7
7
 
8
8
  **Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
9
9
 
@@ -476,4 +476,4 @@ Implemented, planned and deprecated items, with the version each landed in: `ski
476
476
 
477
477
  ---
478
478
 
479
- **v11.54.3 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
479
+ **v11.55.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.54.3
1
+ 11.55.0
@@ -42,11 +42,13 @@ WHAT THIS DELIBERATELY DOES NOT DO
42
42
  from __future__ import annotations
43
43
 
44
44
  import hashlib
45
+ import hmac
45
46
  import json
46
47
  import os
47
48
  import re
48
49
  import subprocess
49
50
  import sys
51
+ import tempfile
50
52
  import time
51
53
  from dataclasses import dataclass, field, asdict
52
54
  from pathlib import Path
@@ -210,43 +212,165 @@ def discover(root: str, diff_base: str = "") -> list:
210
212
 
211
213
  # --- Content-addressed cache -------------------------------------------------
212
214
 
215
+ def _producer_fingerprint() -> str | None:
216
+ """Digest of this file's own bytes. Every cached verdict is bound to the
217
+ exact rules that produced it, so any rule edit orphans old entries with no
218
+ hand-bumped version to forget. None when the source cannot be read, in
219
+ which case the cache is off."""
220
+ try:
221
+ with open(os.path.abspath(__file__), "rb") as fh:
222
+ return hashlib.blake2b(fh.read(), digest_size=16).hexdigest()
223
+ except OSError:
224
+ return None
225
+
226
+
227
+ PRODUCER = _producer_fingerprint()
228
+
229
+
230
+ def cache_key(rel: str, raw: bytes) -> str:
231
+ """A verdict depends on the rules, the path (is_test selects which rules
232
+ run) and the content, so all three are in the key."""
233
+ h = hashlib.blake2b(digest_size=16)
234
+ h.update((PRODUCER or "").encode("ascii"))
235
+ h.update(b"\0")
236
+ h.update(rel.encode("utf-8", "replace"))
237
+ h.update(b"\0")
238
+ h.update(raw)
239
+ return h.hexdigest()
240
+
241
+
242
+ def cache_dir() -> str:
243
+ """User-owned cache root, outside every repo the agent works in
244
+ (LOKI_FAST_VERIFY_CACHE_DIR overrides it, for tests and sandboxes)."""
245
+ return os.environ.get("LOKI_FAST_VERIFY_CACHE_DIR") or os.path.join(
246
+ os.path.expanduser("~"), ".loki", "cache", "fast-verify")
247
+
248
+
249
+ def cache_path(root: str) -> str:
250
+ tag = hashlib.blake2b(os.path.realpath(root).encode("utf-8", "replace"), digest_size=8).hexdigest()
251
+ return os.path.join(cache_dir(), tag + ".json")
252
+
253
+
254
+ def _private_dir(path: str) -> bool:
255
+ """Create the cache dir and force it to 0700, including a pre-existing one
256
+ created loose. A dir this user cannot chmod is not ours: refuse it."""
257
+ try:
258
+ os.makedirs(path, mode=0o700, exist_ok=True)
259
+ os.chmod(path, 0o700)
260
+ return True
261
+ except OSError:
262
+ return False
263
+
264
+
265
+ def _write_private(path: str, data: bytes, replace: bool) -> None:
266
+ """Write data to a 0600 temp file in the same dir, then publish it
267
+ atomically: os.replace, or os.link so an existing file is never clobbered
268
+ and no reader sees a partial one."""
269
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".tmp-")
270
+ try:
271
+ os.fchmod(fd, 0o600)
272
+ with os.fdopen(fd, "wb") as fh:
273
+ fh.write(data)
274
+ if replace:
275
+ os.replace(tmp, path)
276
+ else:
277
+ os.link(tmp, path)
278
+ finally:
279
+ try:
280
+ os.unlink(tmp)
281
+ except OSError:
282
+ pass
283
+
284
+
285
+ def _read_key(path: str) -> bytes | None:
286
+ try:
287
+ st = os.stat(path)
288
+ if st.st_mode & 0o077 or st.st_uid != os.getuid():
289
+ return None # a key others could read or write proves nothing
290
+ with open(path, "rb") as fh:
291
+ key = fh.read()
292
+ return key if len(key) >= 32 else None
293
+ except OSError:
294
+ return None
295
+
296
+
297
+ def _cache_secret() -> bytes | None:
298
+ """Per-user HMAC key kept beside the cache, never inside the repo. None when
299
+ it can be neither read nor created, in which case the cache is off."""
300
+ if PRODUCER is None or not _private_dir(cache_dir()):
301
+ return None
302
+ path = os.path.join(cache_dir(), "key")
303
+ key = _read_key(path)
304
+ if key is not None:
305
+ return key
306
+ try:
307
+ _write_private(path, os.urandom(32), replace=False)
308
+ except FileExistsError:
309
+ pass
310
+ except OSError:
311
+ return None
312
+ return _read_key(path)
313
+
314
+
213
315
  class Cache:
214
316
  """Findings keyed by content hash, so unchanged files are never re-read.
215
317
 
216
- Correctness note: the key is the file's CONTENT, not its path or mtime. A
217
- file that moves keeps its result; a file that changes gets a new key. There
218
- is no staleness window to reason about, which is what makes it safe to trust
219
- a cache hit as if the scan had just run.
318
+ The key covers the producing rules (this file's own digest), the path and
319
+ the content, never mtime, so there is no staleness window and a verdict
320
+ cannot outlive a rule change or follow a file to a path where other rules
321
+ apply. Content addressing does not make an entry trustworthy
322
+ on its own: the key is computable by anyone, so an entry written by the
323
+ agent under review could suppress a finding. Each entry therefore carries
324
+ an HMAC under a per-user secret held outside the repo, and an entry whose
325
+ MAC does not verify is a miss: the file is rescanned. Without a secret
326
+ the cache is disabled.
220
327
  """
221
328
 
222
329
  def __init__(self, path: str):
223
330
  self.path = path
224
331
  self.data = {}
225
332
  self.hits = 0
333
+ self.secret = _cache_secret()
334
+ if self.secret is None:
335
+ return
226
336
  try:
227
337
  with open(path, encoding="utf-8") as fh:
228
338
  blob = json.load(fh)
229
- if isinstance(blob, dict) and blob.get("schema") == SCHEMA_VERSION:
230
- self.data = blob.get("entries", {})
339
+ if isinstance(blob, dict) and blob.get("producer") == PRODUCER:
340
+ entries = blob.get("entries", {})
341
+ if isinstance(entries, dict):
342
+ self.data = entries
231
343
  except (OSError, ValueError):
232
344
  self.data = {}
233
345
 
346
+ def _mac(self, key: str, findings: list) -> str:
347
+ body = json.dumps([PRODUCER, key, findings], sort_keys=True, separators=(",", ":"))
348
+ return hmac.new(self.secret, body.encode("utf-8"), "sha256").hexdigest()
349
+
234
350
  def get(self, key: str):
235
- v = self.data.get(key)
236
- if v is not None:
237
- self.hits += 1
238
- return v
351
+ entry = self.data.get(key)
352
+ if self.secret is None or not isinstance(entry, dict):
353
+ return None
354
+ findings, mac = entry.get("findings"), entry.get("mac")
355
+ if not isinstance(findings, list) or not isinstance(mac, str):
356
+ return None
357
+ if not hmac.compare_digest(mac, self._mac(key, findings)):
358
+ return None
359
+ self.hits += 1
360
+ return findings
239
361
 
240
362
  def put(self, key: str, findings: list) -> None:
241
- self.data[key] = findings
363
+ if self.secret is not None:
364
+ self.data[key] = {"findings": findings, "mac": self._mac(key, findings)}
242
365
 
243
366
  def save(self) -> None:
367
+ if self.secret is None:
368
+ return
244
369
  try:
245
- os.makedirs(os.path.dirname(self.path), exist_ok=True)
246
- tmp = self.path + ".tmp"
247
- with open(tmp, "w", encoding="utf-8") as fh:
248
- json.dump({"schema": SCHEMA_VERSION, "entries": self.data}, fh)
249
- os.replace(tmp, self.path)
370
+ if not _private_dir(os.path.dirname(self.path)):
371
+ return
372
+ blob = json.dumps({"producer": PRODUCER, "entries": self.data})
373
+ _write_private(self.path, blob.encode("utf-8"), replace=True)
250
374
  except OSError:
251
375
  pass # a cache that cannot be written must never break a verdict
252
376
 
@@ -256,7 +380,7 @@ class Cache:
256
380
  def verify(root: str = ".", diff_base: str = "", use_cache: bool = True) -> Result:
257
381
  t0 = time.perf_counter()
258
382
  root = os.path.abspath(root)
259
- cache = Cache(os.path.join(root, ".loki", "cache", "fast-verify.json")) if use_cache else None
383
+ cache = Cache(cache_path(root)) if use_cache else None
260
384
 
261
385
  findings = []
262
386
  scanned = 0
@@ -270,7 +394,7 @@ def verify(root: str = ".", diff_base: str = "", use_cache: bool = True) -> Resu
270
394
  continue
271
395
  scanned += 1
272
396
 
273
- key = hashlib.blake2b(raw, digest_size=16).hexdigest()
397
+ key = cache_key(rel, raw)
274
398
  if cache is not None:
275
399
  hit = cache.get(key)
276
400
  if hit is not None:
@@ -7,6 +7,6 @@ Modules:
7
7
  control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.54.3"
10
+ __version__ = "11.55.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -1,6 +1,6 @@
1
1
  # CLI reference
2
2
 
3
- Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.54.3. Do not edit by hand.
3
+ Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.55.0. Do not edit by hand.
4
4
  Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for the full inventory.
5
5
 
6
6
  | Command | Description |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Loki Mode, by [Autonomi](https://www.autonomi.dev/), turns a spec or an issue into a verified pull request with a receipt you can check. This page covers installing it, a first run, and the options around it.
4
4
 
5
- **Version:** v11.54.3
5
+ **Version:** v11.55.0
6
6
 
7
7
  **Engine note:** `loki start` is the primary command. `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` all run the Loki 10 engine, whatever the flag order or input form (PRD file, issue ref, task or one-word brief). The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, or for one call when a flag Loki 10 cannot honor is present. See the [user guide](v10/GUIDE.md) and the [5-minute quickstart](QUICKSTART.md).
8
8
 
@@ -1417,9 +1417,18 @@ L0 review rule (ENGINE-LAWS.md L0): a fix that adds an `if` or a regex about the
1417
1417
  - Fixture: loki-ts/tests/engine10/seal_group.test.ts "#317: local HEAD ahead of origin/HEAD, every unit starts at the journal-key base". Red before the fix (unit base undefined), green after.
1418
1418
 
1419
1419
  ## FC-TRUSTED-CACHE-VERDICT fast_verify trusts an agent-writable content-hash cache as a fresh scan, so a planted entry (an empty list suppressing a high finding, or injected finding text) decides the verdict
1420
- - User-visible vs raw: user-visible. A planted `.loki/cache/fast-verify.json` entry keyed by a file's content hash is returned as that file's findings without rescanning. `[]` makes a file with a high finding PASS; a crafted message reaches the MCP client as trusted tool output. Tracked in #319.
1420
+ - User-visible vs raw: user-visible. A planted `.loki/cache/fast-verify.json` entry keyed by a file's content hash is returned as that file's findings without rescanning. `[]` makes a file with a high finding PASS; a crafted message reaches the MCP client as trusted tool output. Closed by #319.
1421
1421
  - Law: a verdict is derived from the content being judged, never from a file the judged agent can write (EL exogenous gates).
1422
1422
  - Cause (measured): autonomy/lib/fast_verify.py Cache reads entries by blake2b(content) and returns them unvalidated; mcp/server.py listed loki_verify_fast in _TRUSTED_TOOLS so its finding text skipped the untrusted envelope.
1423
- - Siblings swept: every registered MCP tool and resource, via the runtime-derived guard in tests/mcp/test_phase1_tools.py (seeds this cache with the marker; the observer now also sees os.scandir, os.stat, Path.iterdir and glob); the JS mirror declares its fields in src/protocols and counts them in tests/protocols/mcp-server.test.js.
1424
- - Mechanism: part 1 (this change) moves loki_verify_fast findings under untrusted_content via _UNTRUSTED_FIELDS. The verdict-suppression half is open in #319 (cache entries must be authenticated or re-derived, then the row is closed).
1425
- - Fixture: tests/mcp/test_phase1_tools.py test_stored_text_appears_only_inside_untrusted_content (red with loki_verify_fast trusted: leaks ['loki_verify_fast']; green after).
1423
+ - Siblings swept (grep for cache files under .loki and ~/.loki in loki-ts/src, autonomy, mcp): engine10/cache.ts repomap/testmap cache lives under ~/.loki/cache/v10, outside the repo, keyed by tree hash, no MAC (same-user write is out of this row's threat model; it selects tests, it does not return findings); run.sh `.loki/cache/install` is in-repo and agent-writable but stores node_modules, not a verdict; router history and update_check feed estimates and notices, not a verdict. No other in-repo cache returns findings. Every MCP tool and resource is covered by the runtime-derived guard in tests/mcp/test_phase1_tools.py.
1424
+ - Mechanism: two layers. (1) loki_verify_fast findings sit under untrusted_content via _UNTRUSTED_FIELDS. (2) fast_verify.Cache lives at ~/.loki/cache/fast-verify/<realpath hash>.json (LOKI_FAST_VERIFY_CACHE_DIR overrides), and each entry carries an HMAC under a per-user 0600 key beside it. The cache key and the MAC body are derived from everything the verdict depends on: a producer fingerprint (blake2b of fast_verify.py's own bytes, computed at import, so any rule edit orphans every old entry and there is no hand-bumped schema constant), the repo-relative path (is_test decides which rules run, so a file moved from a test path to a product path is rescanned), and the content. An entry whose MAC fails is a miss and the file is rescanned; with no readable key, no readable source or an unusable cache dir the cache is off. The key is created through a 0600 temp file and os.link (never clobbered, never partial), the cache dir is forced to 0700 even when it pre-exists loose, a key that is group or other accessible or not owned by the user is refused, and the cache json is written at 0600 through a temp file and os.replace. The old repo-local `.loki/cache/fast-verify.json` is never read. Closes #319.
1425
+ - Trust boundary: closed for repo content and for any write the judged agent makes inside the repo. NOT closed against a same-user process that can read ~/.loki/cache/fast-verify/key: it can forge a valid MAC. A same-user agent that is allowed to read the home directory therefore defeats this cache; the only complete answer for that threat is --no-cache (the fast scan is milliseconds), which the exogenous gates should use.
1426
+ - Sibling not closed (engine10 cache): loki-ts/src/engine10/cache.ts keys repomap and testmap by HEAD^{tree} plus a hand-bumped "repomap2-" prefix, so a builder change does not invalidate stored maps (same class as the hand-bumped CACHE_SCHEMA removed here). It selects tests and does not return findings, so it is not a verdict, but the fix is the same idea: derive the builder version from the builder's bytes at build time instead of a manual prefix. Tracked as a follow-up, not changed here because the TS bundle has no stable source path to hash at runtime.
1427
+ - Fixture: tests/mcp/test_phase1_tools.py test_stored_text_appears_only_inside_untrusted_content (red with loki_verify_fast trusted: leaks ['loki_verify_fast']; green after) and tests/test-fast-verify.sh CACHE BINDING block (planted repo-local and forged-MAC empty-list entries; red when the MAC check is replaced by `if False:`, green with it), plus the CACHE KEY and CACHE FILES blocks there: a test-path file moved to a product path must FAIL (red when the path is dropped from cache_key), a copy of fast_verify.py with disabled rules must not poison the real rules' cache (red when PRODUCER is a fixed string), the key and cache json are 0600 and the dir 0700 (red when the key is written 0666), and no verify cache file appears in a repo (red when cache_path points at root/.loki/cache/fast-verify.json). Tests that run fast_verify set LOKI_FAST_VERIFY_CACHE_DIR to a run tmp or pass --no-cache, never the real ~/.loki.
1428
+
1429
+ ## FC-CATALOG-CAPABILITY-DRIFT-UNCHECKED A catalog entry whose capabilities drifted from the API failed only at request time
1430
+ - User-visible: a catalog entry whose thinking types or effort levels drifted from what the model accepts fails only at request time, as an API error mid-run. Raw: the catalog (providers/model_catalog.json) is static and never compared with models.retrieve.
1431
+ - Law broken: a capability the harness relies on is claimed without being checked against its source of truth.
1432
+ - Siblings swept: effortForTier clamp (bash and Bun, parity-checked), claudeEffortLevels, the catalog freshness row (age only, offline).
1433
+ - Mechanism: loki doctor "Model capabilities" row. With ANTHROPIC_API_KEY it calls models.retrieve per claude catalog id and prints each mismatch (effort levels, API-expressible thinking types); without a key or offline it warns and never changes the tally or exit code. The bench runner takes --effort sweep, expanding to the catalog levels for --model.
1434
+ - Fixture: loki-ts/tests/commands/doctor.test.ts "model capability row", tests/test_bench_runner.py sweep tests, tests/test-bash-bun-parity.sh effort clamp cells. Red when the comparison, the sweep catalog read or the clamp model argument is removed.