loki-mode 7.56.0 → 7.58.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.
@@ -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.56.0
5
+ **Version:** v7.58.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.56.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.58.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 r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.56.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,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,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ 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 a($){return JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){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 a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
2
+ var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.58.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,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,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ 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 a($){return JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){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 a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}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)
@@ -790,4 +790,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
790
790
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
791
791
  `),process.stderr.write(s6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var KZ=await XZ(Bun.argv.slice(2));process.exit(KZ);
792
792
 
793
- //# debugId=3D16FCC1B4694B0E64756E2164756E21
793
+ //# debugId=9F9D3B1A3FBD072264756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.56.0'
60
+ __version__ = '7.58.0'
@@ -221,10 +221,17 @@ class ConsolidationPipeline:
221
221
  if new_pattern:
222
222
  # Try to merge with existing
223
223
  merged = False
224
- for existing in existing_patterns:
224
+ for idx, existing in enumerate(existing_patterns):
225
225
  if self._patterns_similar(new_pattern, existing):
226
226
  merged_pattern = self.merge_with_existing(new_pattern, [existing])
227
227
  self.storage.update_pattern(merged_pattern)
228
+ # Refresh the in-memory copy so a later new pattern in
229
+ # this same run that also merges into this existing
230
+ # pattern builds on the just-merged state. Without this,
231
+ # the second merge reads the stale pre-merge base and its
232
+ # update_pattern() overwrites storage, silently dropping
233
+ # the first merge's conditions/source_episodes/confidence.
234
+ existing_patterns[idx] = merged_pattern
228
235
  result.patterns_merged += 1
229
236
  merged = True
230
237
  break
@@ -240,11 +247,16 @@ class ConsolidationPipeline:
240
247
  for anti_pattern in anti_patterns:
241
248
  # Check if similar anti-pattern already exists
242
249
  merged = False
243
- for existing in existing_patterns:
250
+ for idx, existing in enumerate(existing_patterns):
244
251
  if (existing.incorrect_approach and
245
252
  self._patterns_similar(anti_pattern, existing, threshold=0.6)):
246
253
  merged_pattern = self.merge_with_existing(anti_pattern, [existing])
247
254
  self.storage.update_pattern(merged_pattern)
255
+ # Refresh in-memory copy (same data-loss guard as the cluster
256
+ # merge loop above): a later anti-pattern merging into this same
257
+ # existing pattern must build on the just-merged state, not the
258
+ # stale pre-merge base.
259
+ existing_patterns[idx] = merged_pattern
248
260
  result.patterns_merged += 1
249
261
  merged = True
250
262
  break
@@ -855,6 +855,8 @@ class MemoryRetrieval:
855
855
  # Filter semantic patterns by last_used
856
856
  patterns_data = self.storage.read_json("semantic/patterns.json") or {}
857
857
  for pattern in patterns_data.get("patterns", []):
858
+ if not isinstance(pattern, dict):
859
+ continue
858
860
  last_used = pattern.get("last_used")
859
861
  if last_used:
860
862
  try:
@@ -1483,6 +1485,8 @@ class MemoryRetrieval:
1483
1485
  patterns_data = self.storage.read_json("semantic/patterns.json") or {}
1484
1486
 
1485
1487
  for pattern in patterns_data.get("patterns", []):
1488
+ if not isinstance(pattern, dict):
1489
+ continue
1486
1490
  pattern_text = pattern.get("pattern", "").lower()
1487
1491
  category = pattern.get("category", "").lower()
1488
1492
  correct = pattern.get("correct_approach", "").lower()
@@ -1566,6 +1570,8 @@ class MemoryRetrieval:
1566
1570
  # what_fails / why / prevention scoring shape.
1567
1571
  patterns_data = self.storage.read_json("semantic/patterns.json") or {}
1568
1572
  for pat in patterns_data.get("patterns", []):
1573
+ if not isinstance(pat, dict):
1574
+ continue
1569
1575
  if pat.get("category") != "anti-pattern":
1570
1576
  continue
1571
1577
  what_fails = (pat.get("incorrect_approach", "")
@@ -1633,6 +1639,8 @@ class MemoryRetrieval:
1633
1639
  patterns_data = self.storage.read_json("semantic/patterns.json") or {}
1634
1640
 
1635
1641
  for pattern in patterns_data.get("patterns", []):
1642
+ if not isinstance(pattern, dict):
1643
+ continue
1636
1644
  # Create text for embedding
1637
1645
  text = f"{pattern.get('pattern', '')} {pattern.get('category', '')} {pattern.get('correct_approach', '')}"
1638
1646
 
@@ -1690,6 +1698,8 @@ class MemoryRetrieval:
1690
1698
  # too so embedding-based retrieval sees consolidated anti-patterns.
1691
1699
  patterns_data = self.storage.read_json("semantic/patterns.json") or {}
1692
1700
  for pat in patterns_data.get("patterns", []):
1701
+ if not isinstance(pat, dict):
1702
+ continue
1693
1703
  if pat.get("category") != "anti-pattern":
1694
1704
  continue
1695
1705
  what_fails = pat.get("incorrect_approach", "") or pat.get("pattern", "")
package/memory/storage.py CHANGED
@@ -626,6 +626,8 @@ class MemoryStorage:
626
626
  # Upsert: update existing pattern or append new
627
627
  existing_idx = None
628
628
  for i, p in enumerate(patterns_file["patterns"]):
629
+ if not isinstance(p, dict):
630
+ continue
629
631
  if p.get("id") == pattern_id:
630
632
  existing_idx = i
631
633
  break
@@ -672,6 +674,8 @@ class MemoryStorage:
672
674
  return None
673
675
 
674
676
  for pattern in patterns_file.get("patterns", []):
677
+ if not isinstance(pattern, dict):
678
+ continue
675
679
  if pattern.get("id") == pattern_id:
676
680
  return pattern
677
681
 
@@ -695,6 +699,8 @@ class MemoryStorage:
695
699
 
696
700
  pattern_ids = []
697
701
  for pattern in patterns_file.get("patterns", []):
702
+ if not isinstance(pattern, dict):
703
+ continue
698
704
  if category is None or pattern.get("category") == category:
699
705
  pattern_ids.append(pattern.get("id"))
700
706
 
@@ -737,6 +743,8 @@ class MemoryStorage:
737
743
  # Find and update pattern
738
744
  found = False
739
745
  for i, p in enumerate(patterns_file.get("patterns", [])):
746
+ if not isinstance(p, dict):
747
+ continue
740
748
  if p.get("id") == pattern_id:
741
749
  pattern_data["updated_at"] = datetime.now(timezone.utc).isoformat()
742
750
  patterns_file["patterns"][i] = pattern_data
@@ -1364,6 +1372,8 @@ class MemoryStorage:
1364
1372
 
1365
1373
  updated = 0
1366
1374
  for pattern in patterns:
1375
+ if not isinstance(pattern, dict):
1376
+ continue
1367
1377
  original = pattern.get("importance", 0.5)
1368
1378
  self.apply_decay([pattern], decay_rate, half_life_days)
1369
1379
  if abs(pattern.get("importance", 0.5) - original) > 0.001:
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.56.0",
4
+ "version": "7.58.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.56.0",
5
+ "version": "7.58.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",
@@ -2,18 +2,35 @@
2
2
 
3
3
  **Never ship code without passing all quality gates.**
4
4
 
5
- ## The Quality Gates (8 default-on + 1 opt-in)
6
-
7
- Every gate below is wired into the orchestration loop (`autonomy/run.sh`). The 8
8
- numbered gates are default-on and block completion when they fail; the opt-in
9
- gate (marked below) is default-OFF and runs only when its flag is set. The table
10
- lists exactly what each gate detects, what it does NOT detect (so you never
11
- over-trust a green gate), its opt-out flag, and its blocking behavior. Transcribe
12
- this list verbatim; do not recompute it.
5
+ ## The Quality Gates (8 blocking default-on + 3 advisory default-on + 1 opt-in)
6
+
7
+ Every gate below is wired into the orchestration loop (`autonomy/run.sh`). Read
8
+ the count honestly, split by what a gate actually does:
9
+
10
+ - **8 BLOCKING default-on gates** (the numbered table below): run by default and
11
+ FAIL the build / block completion when they trip.
12
+ - **3 ADVISORY default-on gates** (LSP diagnostics, semantic test-authenticity,
13
+ invariant/property; see the advisory table further down): run by default,
14
+ SURFACE actionable findings into the next iteration's prompt, and never block
15
+ by default. Two of them gain a blocking arm only via an explicit `*_BLOCK`
16
+ opt-in flag; LSP diagnostics has no blocking arm at all.
17
+ - **1 OPT-IN gate** (coverage, `LOKI_COVERAGE_GATE`, default OFF): does not run
18
+ unless explicitly enabled. It is the lone opt-in because it doubles test
19
+ runtime (instrumented re-run); see the coverage section for why.
20
+
21
+ So: 8 blocking gates run by default, plus 3 advisory surfacing gates and 1
22
+ opt-in coverage gate. Only the 8 numbered gates block out of the box; the
23
+ advisory gates surface findings without blocking, and the coverage gate is OFF
24
+ unless explicitly enabled. Never count the advisory or opt-in gates as blockers.
25
+
26
+ The numbered table below lists the 8 BLOCKING default-on gates: exactly what each
27
+ detects, what it does NOT detect (so you never over-trust a green gate), its
28
+ opt-out flag, and its blocking behavior. Transcribe this list verbatim; do not
29
+ recompute it.
13
30
 
14
31
  | # | Gate | Detects | Does NOT detect | Blocking | Opt-out flag |
15
32
  |---|------|---------|-----------------|----------|--------------|
16
- | 1 | Static Analysis | CodeQL, ESLint/Pylint, type-checker findings on the diff | Logic bugs that pass the linters | Yes (severity ladder) | `PHASE_STATIC_ANALYSIS=false` |
33
+ | 1 | Static Analysis | ESLint/Pylint, type-checker findings on the diff | Logic bugs that pass the linters | Yes (severity ladder) | `PHASE_STATIC_ANALYSIS=false` |
17
34
  | 2 | Test Suite (pass/fail) | Whether the project test runner passes or fails (red blocks) | Coverage % (not measured in this release) | Yes (red blocks) | `PHASE_UNIT_TESTS=false` |
18
35
  | 3 | Blind Code Review (3-reviewer council + severity blocking) | Correctness/security/design issues via 3 blind reviewers; Critical/High block, Medium/Low advisory | Issues none of the 3 reviewers surface | Yes (Crit/High block) | `PHASE_CODE_REVIEW=false` |
19
36
  | 4 | Anti-Sycophancy / Devil's Advocate (on unanimous PASS) | Sycophantic unanimous approvals: a Devil's Advocate re-review on a unanimous PASS; its Crit/High findings block | Problems the Devil's Advocate reviewer also misses | Yes (DA Crit/High block) | `LOKI_GATE_DEVILS_ADVOCATE=false` |
@@ -21,7 +38,12 @@ this list verbatim; do not recompute it.
21
38
  | 6 | Test Mutation Detector | Assertion-value churn alongside implementation changes (test-fitting), low assertion density (`tests/detect-test-mutations.sh`); HIGH blocks | Logically-correct-but-weak assertions | Yes (HIGH blocks) | `LOKI_GATE_MUTATION=false` |
22
39
  | 7 | Documentation Coverage | README presence, docs freshness within 10 commits, API docs for exported symbols in packages | Whether the docs are accurate or useful | Yes | `LOKI_GATE_DOC_COVERAGE=false` |
23
40
  | 8 | Magic Modules Debate | Spec-vs-implementation debate findings on generated Magic Modules; BLOCK-severity findings block | Issues outside the Magic Modules debate scope | Yes (BLOCK severity) | `LOKI_GATE_MAGIC_DEBATE=false` |
24
- | 9 (opt-in, default OFF) | Semantic Test-Authenticity | Fake tests that look real but verify nothing (literal-via-variable echo, mock-return echo, deleted assertions) that gates 5+6 miss (`tests/detect-semantic-test-problems.sh --block-high`); CRITICAL/HIGH block | Deep dataflow, legitimate computed-literal assertions, Python/shell tests (JS/TS only); MED/LOW are advisory | Only when enabled, and only on CRITICAL/HIGH; runs solely on a completion claim | Opt-IN: `LOKI_GATE_SEMANTIC_TESTS=true` to enable (default off = not invoked, never blocks) |
41
+
42
+ The three advisory default-on gates (LSP diagnostics, semantic
43
+ test-authenticity, invariant/property) are documented in their own table under
44
+ "Advisory default-on verification gates" below. Semantic test-authenticity used
45
+ to appear here as "gate 9 (opt-in, default OFF)"; it is now a default-on
46
+ advisory gate and has moved to that table. See the migration note there.
25
47
 
26
48
  **Severity-based blocking** ties the review gates together: any Critical or High
27
49
  finding blocks completion. Medium, Low, and cosmetic findings are advisory and
@@ -100,6 +122,108 @@ LOKI_GATE_MUTATION=false # Disable gate 6 (Test Mutation Detector)
100
122
 
101
123
  ---
102
124
 
125
+ ## Advisory default-on verification gates (LSP, semantic, invariant)
126
+
127
+ These three gates extend the mock/mutation precedent (gates 5+6: default-on,
128
+ opt-out) into an ADVISORY-FIRST posture. They run by default, surface actionable
129
+ findings into the next iteration's prompt, and only BLOCK completion when their
130
+ opt-in `*_BLOCK` flag is set. LSP diagnostics has no blocking arm at all: it is
131
+ advisory by construction. Coverage is NOT in this group; it remains opt-in (see
132
+ the next section).
133
+
134
+ This is the FROZEN knob scheme. All flags accept `true` or `1`.
135
+
136
+ **Route scope (honest disclosure):** Bun-route parity is ACHIEVED for the
137
+ default-ON behavior. The Bun runner
138
+ (`loki-ts/src/runner/quality_gates.ts` `readToggles`) now defaults all three
139
+ advisory gates ON (`LOKI_GATE_SEMANTIC_TESTS`, `LOKI_GATE_INVARIANTS`,
140
+ `LOKI_GATE_LSP_DIAGNOSTICS` each `flag(X, true)`), matching the bash
141
+ `loki start` route (`autonomy/run.sh`), with blocking behind the same opt-in
142
+ `*_BLOCK` flags. Semantic and invariant findings surface into the next
143
+ iteration's prompt via the `build_prompt` readers
144
+ (`buildSemanticFindingsBlock` + `buildInvariantFindingsBlock`). The one real
145
+ remaining gap is a SURFACING ASYMMETRY: LSP runs default-ON on the Bun route,
146
+ but its advisory findings are NOT yet injected into the Bun prompt (LSP
147
+ contributes only the grounding instruction, not its diagnostics block). That
148
+ prompt-injection parity is still pending for LSP on the Bun route. This mirrors
149
+ the "Reachability note" at the end of this file for the v7.5.0 Phase 1 flags.
150
+
151
+ | Gate | Surfacing (advisory, default-ON, opt-out) | Blocking (opt-in, default-OFF) |
152
+ |------|--------------------------------------------|--------------------------------|
153
+ | LSP diagnostics | `LOKI_GATE_LSP_DIAGNOSTICS=true` | advisory-only, no blocking arm |
154
+ | Semantic test-authenticity | `LOKI_GATE_SEMANTIC_TESTS=true` | `LOKI_GATE_SEMANTIC_TESTS_BLOCK=true` |
155
+ | Invariant / property | `LOKI_GATE_INVARIANTS=true` | `LOKI_GATE_INVARIANTS_BLOCK=true` |
156
+
157
+ How to read the Surfacing column: each gate is already ON by default. The flag
158
+ shown is the gate's own toggle, and `true`/`1` is its enabled value (the default).
159
+ Set the flag to its off value (`false`/`0`) to OPT OUT of surfacing. Setting the
160
+ `=true` value is a no-op confirmation, not an opt-in: these are not opt-in gates.
161
+
162
+ How to read the Blocking column: by default these gates never block, they only
163
+ surface. To make a gate also block completion, set its `*_BLOCK` flag (default
164
+ OFF). The blocking arm fires only on a completion claim, and only on the gate's
165
+ high-severity findings; lower-severity findings stay advisory either way.
166
+
167
+ **What each gate surfaces:**
168
+
169
+ - **LSP diagnostics** (`LOKI_GATE_LSP_DIAGNOSTICS`, default on): language-server
170
+ diagnostics (errors/warnings) on the changed files. Advisory only; there is no
171
+ `_BLOCK` flag and no way to make it block. Inert when no language server is
172
+ available for the project's languages.
173
+ - **Semantic test-authenticity** (`LOKI_GATE_SEMANTIC_TESTS`, default on):
174
+ fake tests that look real but verify nothing (literal-via-variable echo,
175
+ mock-return echo, deleted assertions) that gates 5+6 miss
176
+ (`tests/detect-semantic-test-problems.sh`). Surfaces by default; blocks on
177
+ CRITICAL/HIGH only when `LOKI_GATE_SEMANTIC_TESTS_BLOCK=true`. Does NOT detect
178
+ deep dataflow, legitimate computed-literal assertions, or non-JS/TS tests
179
+ (JS/TS only).
180
+ - **Invariant / property** (`LOKI_GATE_INVARIANTS`, default on): property and
181
+ metamorphic invariant findings. Surfaces by default; blocks only when
182
+ `LOKI_GATE_INVARIANTS_BLOCK=true`.
183
+
184
+ **Advisory-first posture and deny-filter:** these gates run by default and feed
185
+ their findings into the next iteration's prompt so the agent can act on them.
186
+ They only stop a completion claim when the matching `*_BLOCK` opt-in is set. The
187
+ gates are deny-filtered: a clean result, an absent toolchain (e.g. no language
188
+ server, no test files in scope), or a timeout never fires the gate. The gate
189
+ surfaces or blocks only on real, parseable findings, so a missing toolchain does
190
+ not produce false surfacing or a false block.
191
+
192
+ ### Migration: semantic and invariant flags were repurposed
193
+
194
+ `LOKI_GATE_SEMANTIC_TESTS` and `LOKI_GATE_INVARIANTS` changed meaning:
195
+
196
+ - **Before:** these flags were default-OFF opt-ins that, when set, made the gate
197
+ BLOCK completion. Semantic test-authenticity was documented as "gate 9
198
+ (opt-in, default OFF)" and blocked on CRITICAL/HIGH when
199
+ `LOKI_GATE_SEMANTIC_TESTS=true`.
200
+ - **Now:** these flags are default-ON surfacing toggles (set to `false`/`0` to
201
+ opt OUT of surfacing). They no longer block. Blocking moved to the new
202
+ `LOKI_GATE_SEMANTIC_TESTS_BLOCK` and `LOKI_GATE_INVARIANTS_BLOCK` opt-ins
203
+ (default OFF).
204
+
205
+ **Action required if you relied on the old behavior:** anyone who set
206
+ `LOKI_GATE_SEMANTIC_TESTS=true` (or `LOKI_GATE_INVARIANTS=true`) specifically to
207
+ BLOCK the build must now set `LOKI_GATE_SEMANTIC_TESTS_BLOCK=true` (or
208
+ `LOKI_GATE_INVARIANTS_BLOCK=true`). The old flag set to `true` now only confirms
209
+ the default-on surfacing and will NOT block. This is the one behavior change in
210
+ the migration: a flag that used to block now only surfaces.
211
+
212
+ ### Coverage stays opt-in (the exception)
213
+
214
+ The coverage gate is the one verification gate that remains OPT-IN
215
+ (`LOKI_COVERAGE_GATE`, default OFF). It is the exception because measuring
216
+ coverage requires an instrumented SECOND test run, which roughly doubles test
217
+ runtime for every iteration. The advisory gates above add no such cost, so they
218
+ default on; coverage's cost is why it does not.
219
+
220
+ ```bash
221
+ LOKI_COVERAGE_GATE=1 # opt in: measure + record coverage. Default OFF because
222
+ # it doubles test runtime (instrumented re-run). Even
223
+ # when enabled it measures and warns; it does not block
224
+ # unless LOKI_ENFORCE_COVERAGE=1 is also set.
225
+ ```
226
+
103
227
  ## v7.5.0 Phase 1 environment flags
104
228
 
105
229
  These four flags activate the override council and structured-findings
@@ -662,7 +786,7 @@ Initial excitement -> Velocity spike -> Quality degradation accumulates
662
786
  ```yaml
663
787
  velocity_quality_balance:
664
788
  before_commit:
665
- - static_analysis: "Run ESLint/Pylint/CodeQL - warnings must not increase"
789
+ - static_analysis: "Run ESLint/Pylint - warnings must not increase"
666
790
  - complexity_check: "Cyclomatic complexity must not increase >10%"
667
791
  - test_suite: "Tests must pass (coverage % not measured in this release)"
668
792