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.
- package/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +101 -0
- package/autonomy/lib/prd-enrich.sh +437 -0
- package/autonomy/loki +58 -9
- package/autonomy/run.sh +175 -60
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +652 -194
- package/dashboard/static/index.html +164 -151
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +14 -2
- package/memory/retrieval.py +10 -0
- package/memory/storage.py +10 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/skills/quality-gates.md +135 -11
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
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.
|
|
398
|
+
asklokesh/loki-mode:7.58.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -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.
|
|
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=
|
|
793
|
+
//# debugId=9F9D3B1A3FBD072264756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/consolidation.py
CHANGED
|
@@ -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
|
package/memory/retrieval.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
package/skills/quality-gates.md
CHANGED
|
@@ -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`).
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 |
|
|
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
|
-
|
|
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
|
|
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
|
|