loki-mode 8.91.0 → 8.92.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tools/ci-gate.py +222 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v8.
|
|
6
|
+
# Loki Mode v8.92.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
-
**v8.
|
|
472
|
+
**v8.92.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.92.0
|
package/dashboard/__init__.py
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.
|
|
2
|
+
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.92.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Xf(Qf(import.meta.url)),Q=X$(X);h5=e_(Zf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var bO={};l0(bO,{runOrThrow:()=>jf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Tf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>yO});async function NQ(Z,X=yO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function jf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Mf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Mf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Tf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return wf?"":Z}var wf,L0,F8,p0,zV0,a0,W8,Q9,v;var S6=p(()=>{wf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),zV0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),W8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as bf}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(bf(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Kh});import{existsSync as Y9,readFileSync as g3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as rf}from"path";import{homedir as tf}from"os";function sO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*LQ/X);if(J>LQ)J=LQ;let z=LQ-J,K=F8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${W8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function Zh(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}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)
|
|
@@ -1232,4 +1232,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1232
1232
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (h_(),f_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1233
1233
|
`),process.stderr.write(v_),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var uW0=await mW0(Bun.argv.slice(2));process.exit(uW0);
|
|
1234
1234
|
|
|
1235
|
-
//# debugId=
|
|
1235
|
+
//# debugId=E465E6ADABBF4BDF64756E2164756E21
|
package/mcp/__init__.py
CHANGED
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": "8.
|
|
4
|
+
"version": "8.92.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": "8.
|
|
5
|
+
"version": "8.92.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/tools/ci-gate.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One exit code for EVERY merge policy. The gate a CI job actually calls.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS. The policies already ship, each with its own honest verdict:
|
|
5
|
+
cost-guard.py decides budget, receipt-attest.py decides attestation. But CI
|
|
6
|
+
jobs do not run verdicts, they run ONE command and branch on ONE exit code.
|
|
7
|
+
Without this, every team wires its own `&&` chain, and every chain gets the
|
|
8
|
+
aggregation subtly wrong in the same direction: toward green.
|
|
9
|
+
|
|
10
|
+
THE RULE THAT MAKES IT A GATE RATHER THAN A DASHBOARD:
|
|
11
|
+
|
|
12
|
+
WEAKEST LINK. Never an average, never a majority.
|
|
13
|
+
|
|
14
|
+
One policy failing fails the gate. "3 of 4 passed" is not a gate, it is a
|
|
15
|
+
score, and a score cannot stop a merge. There is no threshold here to tune,
|
|
16
|
+
because a tunable threshold is the request to let one known-bad policy through.
|
|
17
|
+
|
|
18
|
+
THE SECOND RULE, WHICH IS THE ONE THAT GETS DROPPED:
|
|
19
|
+
|
|
20
|
+
A POLICY THAT COULD NOT BE EVALUATED HAS NOT PASSED.
|
|
21
|
+
|
|
22
|
+
"We checked and it is fine" and "we could not check" are opposite facts about
|
|
23
|
+
the world, and only one of them earns a merge. Collapsing the second into the
|
|
24
|
+
first makes this gate loudest -- a confident green -- at exactly the moment its
|
|
25
|
+
instrumentation broke and it is least entitled to speak. That is the hole this
|
|
26
|
+
repo has now paid for repeatedly: four quality-gate detectors absent from the
|
|
27
|
+
shipped package, a dist-freshness check deferred by the tier that justified it,
|
|
28
|
+
a tarball assertion that passed on "6 or more" of 6 patterns. Each was a check
|
|
29
|
+
reporting a pass without having checked.
|
|
30
|
+
|
|
31
|
+
So three states, and three exit codes:
|
|
32
|
+
|
|
33
|
+
0 every configured policy was evaluated and passed
|
|
34
|
+
1 a policy was evaluated and FAILED
|
|
35
|
+
2 a policy could not be evaluated -- including no policy configured at all
|
|
36
|
+
|
|
37
|
+
PRECEDENCE. When a run has both a failure and an unevaluable policy, this exits
|
|
38
|
+
2, not 1. "Your gate is partly blind" outranks "this run is over budget": with
|
|
39
|
+
1, the operator fixes the cost, re-runs, sees green, and is still blind on the
|
|
40
|
+
dead axis. The per-policy table shows both regardless.
|
|
41
|
+
|
|
42
|
+
NO POLICY CONFIGURED IS 2. A gate invoked with nothing to enforce has checked
|
|
43
|
+
nothing, so it has no pass to report. Exit 0 there would let a CI job that lost
|
|
44
|
+
its flags in a refactor go green forever while enforcing nothing at all -- a
|
|
45
|
+
vacuously-green gate, which is worse than no gate, because it is trusted.
|
|
46
|
+
|
|
47
|
+
NOTHING HERE RE-IMPLEMENTS A POLICY. Each is invoked as a subprocess via
|
|
48
|
+
sys.executable and its verdict is mapped, never recomputed. A second copy of a
|
|
49
|
+
rule is how the rule drifts, and this repo has proven that five times over with
|
|
50
|
+
provider lists alone. The consequence is that a MISSING tool is UNEVALUABLE,
|
|
51
|
+
never a pass: an absent policy file means the rule was not applied.
|
|
52
|
+
|
|
53
|
+
Child exit codes are WHITELISTED to {0,1,2}. Anything else is unevaluable, with
|
|
54
|
+
the raw code in the reason. receipt-attest.py returns 64 on a usage error, and
|
|
55
|
+
under an `rc != 1 means pass` mapping that malformed call would read as green.
|
|
56
|
+
For the same reason rc=1 with unparseable JSON on stdout is unevaluable, not a
|
|
57
|
+
failure: an uncaught traceback in a child also exits 1, and is not a verdict.
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
tools/ci-gate.py [workspace] --max-usd 5.00 --require-receipt [--json]
|
|
61
|
+
|
|
62
|
+
Exit: 0 all pass, 1 a policy failed, 2 a policy could not be evaluated.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
import argparse
|
|
66
|
+
import glob
|
|
67
|
+
import json
|
|
68
|
+
import os
|
|
69
|
+
import subprocess
|
|
70
|
+
import sys
|
|
71
|
+
|
|
72
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
73
|
+
|
|
74
|
+
# Module-level so a test can point one at a nonexistent path and assert that a
|
|
75
|
+
# missing policy tool reads as unevaluable rather than as a pass.
|
|
76
|
+
_COST_GUARD = os.path.join(_HERE, "cost-guard.py")
|
|
77
|
+
_RECEIPT_ATTEST = os.path.join(_HERE, "receipt-attest.py")
|
|
78
|
+
|
|
79
|
+
PASS, FAIL, UNEVALUABLE = 0, 1, 2
|
|
80
|
+
|
|
81
|
+
_STATE = {PASS: "PASS", FAIL: "FAIL", UNEVALUABLE: "UNEVALUABLE"}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _row(policy, code, reason):
|
|
85
|
+
return {"policy": policy, "state": _STATE[code], "exit_code": code,
|
|
86
|
+
"reason": reason}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _run(policy, argv):
|
|
90
|
+
"""Invoke a policy tool and map its exit code. Never recompute its rule."""
|
|
91
|
+
tool = argv[0]
|
|
92
|
+
if not os.path.isfile(tool):
|
|
93
|
+
# An absent policy file means the rule was never applied. That is the
|
|
94
|
+
# absence of evidence, not evidence of compliance.
|
|
95
|
+
return _row(policy, UNEVALUABLE,
|
|
96
|
+
"policy tool is missing from disk: %s -- the rule was "
|
|
97
|
+
"never applied, so this is not a pass" % tool)
|
|
98
|
+
try:
|
|
99
|
+
proc = subprocess.run([sys.executable] + argv, capture_output=True,
|
|
100
|
+
text=True)
|
|
101
|
+
except OSError as exc:
|
|
102
|
+
return _row(policy, UNEVALUABLE, "could not run %s: %s" % (tool, exc))
|
|
103
|
+
|
|
104
|
+
rc = proc.returncode
|
|
105
|
+
detail = None
|
|
106
|
+
try:
|
|
107
|
+
detail = json.loads(proc.stdout)
|
|
108
|
+
except (ValueError, TypeError):
|
|
109
|
+
detail = None
|
|
110
|
+
|
|
111
|
+
if rc not in _STATE:
|
|
112
|
+
# 64 (usage error), 2xx signals, an import blowup: not a verdict.
|
|
113
|
+
return _row(policy, UNEVALUABLE,
|
|
114
|
+
"%s exited %d, which is not a verdict this gate "
|
|
115
|
+
"recognises: %s" % (os.path.basename(tool), rc,
|
|
116
|
+
(proc.stderr or proc.stdout).strip()
|
|
117
|
+
or "no output"))
|
|
118
|
+
if rc == FAIL and detail is None:
|
|
119
|
+
# An uncaught traceback also exits 1. A failure this gate reports must
|
|
120
|
+
# be one the policy actually rendered.
|
|
121
|
+
return _row(policy, UNEVALUABLE,
|
|
122
|
+
"%s exited 1 but emitted no JSON verdict, so the failure "
|
|
123
|
+
"cannot be attributed to the policy: %s"
|
|
124
|
+
% (os.path.basename(tool),
|
|
125
|
+
(proc.stderr or "no output").strip()))
|
|
126
|
+
return _row(policy, rc, _reason(detail, proc))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _reason(detail, proc):
|
|
130
|
+
"""The child's own words. Never this file's paraphrase of its rule."""
|
|
131
|
+
if isinstance(detail, dict):
|
|
132
|
+
# reason/why are null on a clean pass, so status/verdict carry it there.
|
|
133
|
+
for key in ("reason", "why", "headline", "verdict", "status", "state"):
|
|
134
|
+
value = detail.get(key)
|
|
135
|
+
if isinstance(value, str) and value.strip():
|
|
136
|
+
return value.strip()
|
|
137
|
+
# Falling back to raw stdout, a pretty-printed JSON body would put a bare
|
|
138
|
+
# "{" in the table -- a cell that reports nothing while looking filled in.
|
|
139
|
+
text = (proc.stdout or proc.stderr or "").strip()
|
|
140
|
+
if text.startswith(("{", "[")):
|
|
141
|
+
text = (proc.stderr or "").strip()
|
|
142
|
+
return text.splitlines()[0] if text else "no detail reported"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _newest_receipt(workspace):
|
|
146
|
+
root = workspace
|
|
147
|
+
if os.path.basename(os.path.normpath(workspace)) == ".loki":
|
|
148
|
+
root = os.path.dirname(os.path.normpath(workspace)) or "."
|
|
149
|
+
found = glob.glob(os.path.join(root, ".loki", "proofs", "*", "proof.json"))
|
|
150
|
+
return max(found, key=os.path.getmtime) if found else None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def evaluate(workspace=".", max_usd=None, require_receipt=False):
|
|
154
|
+
"""Run every configured policy. Returns the verdict dict with exit_code."""
|
|
155
|
+
rows = []
|
|
156
|
+
|
|
157
|
+
if max_usd is not None:
|
|
158
|
+
rows.append(_run("cost", [_COST_GUARD, workspace,
|
|
159
|
+
"--max-usd", str(max_usd), "--json"]))
|
|
160
|
+
|
|
161
|
+
if require_receipt:
|
|
162
|
+
receipt = _newest_receipt(workspace)
|
|
163
|
+
if receipt is None:
|
|
164
|
+
# Checked, and the answer is no. That is a FAIL, not a blind spot:
|
|
165
|
+
# 2 is reserved for a receipt that exists and could not be checked.
|
|
166
|
+
rows.append(_row("receipt", FAIL,
|
|
167
|
+
"--require-receipt was given but no receipt "
|
|
168
|
+
"exists under %s/.loki/proofs/*/proof.json"
|
|
169
|
+
% workspace))
|
|
170
|
+
else:
|
|
171
|
+
rows.append(_run("receipt", [_RECEIPT_ATTEST, receipt, "--json"]))
|
|
172
|
+
|
|
173
|
+
if not rows:
|
|
174
|
+
# A gate with nothing to enforce has checked nothing, so it has no pass
|
|
175
|
+
# to report.
|
|
176
|
+
return {"exit_code": UNEVALUABLE, "state": _STATE[UNEVALUABLE],
|
|
177
|
+
"policies": [],
|
|
178
|
+
"reason": "no policy configured: pass --max-usd and/or "
|
|
179
|
+
"--require-receipt. A gate with nothing to enforce "
|
|
180
|
+
"checked nothing, and must not report a pass for it."}
|
|
181
|
+
|
|
182
|
+
# THE DECISION. Weakest link, with unevaluable outranking failure. Written
|
|
183
|
+
# once, here, so a mutation of it has nowhere to hide.
|
|
184
|
+
codes = [r["exit_code"] for r in rows]
|
|
185
|
+
worst = UNEVALUABLE if UNEVALUABLE in codes else (
|
|
186
|
+
FAIL if FAIL in codes else PASS)
|
|
187
|
+
|
|
188
|
+
return {"exit_code": worst, "state": _STATE[worst], "policies": rows,
|
|
189
|
+
"reason": "%d of %d policies passed" % (codes.count(PASS),
|
|
190
|
+
len(codes))}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def render(d):
|
|
194
|
+
lines = ["%-10s %-12s %s" % ("POLICY", "STATE", "DETAIL")]
|
|
195
|
+
for row in d["policies"]:
|
|
196
|
+
lines.append("%-10s %-12s %s" % (row["policy"], row["state"],
|
|
197
|
+
row["reason"]))
|
|
198
|
+
lines.append("")
|
|
199
|
+
lines.append("GATE: %s -- %s" % (d["state"], d["reason"]))
|
|
200
|
+
return "\n".join(lines)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main(argv=None):
|
|
204
|
+
ap = argparse.ArgumentParser(
|
|
205
|
+
description="One exit code over every configured merge policy.")
|
|
206
|
+
ap.add_argument("workspace", nargs="?", default=".",
|
|
207
|
+
help="workspace root (or its .loki dir); default .")
|
|
208
|
+
ap.add_argument("--max-usd", type=float,
|
|
209
|
+
help="enforce a cost ceiling via cost-guard.py")
|
|
210
|
+
ap.add_argument("--require-receipt", action="store_true",
|
|
211
|
+
help="enforce receipt attestation via receipt-attest.py")
|
|
212
|
+
ap.add_argument("--json", action="store_true", dest="as_json",
|
|
213
|
+
help="emit the verdict as JSON")
|
|
214
|
+
args = ap.parse_args(argv)
|
|
215
|
+
|
|
216
|
+
d = evaluate(args.workspace, args.max_usd, args.require_receipt)
|
|
217
|
+
print(json.dumps(d, indent=2) if args.as_json else render(d))
|
|
218
|
+
return d["exit_code"]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
sys.exit(main())
|