loki-mode 8.87.0 → 8.88.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/cost-guard.py +236 -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.88.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.88.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.88.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.88.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=9115B3EA38A3D81564756E2164756E21
|
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.88.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.88.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",
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fail a build when a run's cost regressed. A cost gate for CI.
|
|
3
|
+
|
|
4
|
+
Teams gate merges on tests. This lets them gate merges on cost the same way:
|
|
5
|
+
point it at a workspace, give it a ceiling or a baseline receipt, and it exits
|
|
6
|
+
non-zero when the run got more expensive than the policy allows.
|
|
7
|
+
|
|
8
|
+
THE RULE THAT MAKES IT A GATE RATHER THAN DECORATION:
|
|
9
|
+
|
|
10
|
+
UNMEASURED IS NOT WITHIN BUDGET. Exit 2, never 0.
|
|
11
|
+
|
|
12
|
+
A gate's exit code is a merge decision, and "we did not measure" carries zero
|
|
13
|
+
information about budget compliance. Mapping absence to 0 would make this gate
|
|
14
|
+
loudest -- a confident green -- at exactly the moment instrumentation broke and
|
|
15
|
+
it is least entitled to speak. That is the failure this repo spent thirteen
|
|
16
|
+
surfaces fixing: an unmeasured run rendered as "$0.00", which reads as free.
|
|
17
|
+
Free and unmeasured are different claims and only one of them is honest. Exit 2
|
|
18
|
+
keeps "no evidence" distinguishable from "evidence of compliance".
|
|
19
|
+
|
|
20
|
+
Exit 0 is reachable from exactly ONE place in this file (the end of evaluate()),
|
|
21
|
+
and only after a measurement was read. Everything else -- no policy given, an
|
|
22
|
+
unreadable baseline, an unmeasured run, an unmeasured baseline -- is 2.
|
|
23
|
+
|
|
24
|
+
Whether a number counts as measured is record_is_measured() in
|
|
25
|
+
autonomy/lib/efficiency_cost.py, imported and never restated: a second copy of
|
|
26
|
+
that predicate is precisely how the honesty rule drifts. Receipts store the
|
|
27
|
+
figure under cost.usd while the predicate reads cost_usd, so the KEY is mapped
|
|
28
|
+
here. Mapping a key name is not restating the rule.
|
|
29
|
+
|
|
30
|
+
A genuinely measured $0.00 under budget PASSES. Zero is falsy, so every guard
|
|
31
|
+
here is an explicit `is None` check. A falsy guard would blank a real
|
|
32
|
+
observation, which is the same lie pointed the other way (hit for real in
|
|
33
|
+
v8.72.0).
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
tools/cost-guard.py [workspace] --max-usd 5.00
|
|
37
|
+
tools/cost-guard.py [workspace] --baseline old/proof.json --max-increase-pct 10
|
|
38
|
+
tools/cost-guard.py [workspace] --max-usd 5.00 --json
|
|
39
|
+
|
|
40
|
+
Exit: 0 within budget, 1 over budget / regressed, 2 cannot evaluate.
|
|
41
|
+
|
|
42
|
+
NON-GOAL: the baseline receipt's integrity hash is not verified. A tampered
|
|
43
|
+
baseline that inflates the allowed ceiling is out of scope; use
|
|
44
|
+
tools/receipt-diff.py, which does verify. A baseline whose cost block is absent
|
|
45
|
+
or unmeasured still exits 2 here, via the same predicate as everything else.
|
|
46
|
+
|
|
47
|
+
INHERITED LIMIT, stated rather than silently owned. record_is_measured() is
|
|
48
|
+
satisfied by tokens OR cost, so a record carrying real token counts but no
|
|
49
|
+
cost_usd field sums to usd=0.0 with available=True, and this gate reports
|
|
50
|
+
"WITHIN BUDGET: $0.0000" for a run that did cost money. Verified, not
|
|
51
|
+
theoretical. It is not fixable here: requirement 3 demands that cost_usd=0.0
|
|
52
|
+
WITH tokens passes, so the two shapes are identical downstream of the sum. The
|
|
53
|
+
fix belongs in the pricing layer (price_from_tokens() in the same module exists
|
|
54
|
+
because tools do report tokens without a native cost), not in a second copy of
|
|
55
|
+
the honesty predicate. This gate is exactly as honest as its measurement
|
|
56
|
+
source, which is the correct coupling.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
import argparse
|
|
60
|
+
import json
|
|
61
|
+
import os
|
|
62
|
+
import sys
|
|
63
|
+
|
|
64
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
65
|
+
sys.path.insert(0, os.path.join(os.path.dirname(_HERE), "autonomy", "lib"))
|
|
66
|
+
|
|
67
|
+
from efficiency_cost import collect_efficiency, record_is_measured # noqa: E402
|
|
68
|
+
|
|
69
|
+
OK, OVER, CANNOT = 0, 1, 2
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _num(v):
|
|
73
|
+
"""A number as itself; None, "", or a bool as None."""
|
|
74
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
75
|
+
return None
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def measured_usd(cost):
|
|
80
|
+
"""The USD figure from a cost block, or None when it was never measured.
|
|
81
|
+
|
|
82
|
+
`available` is NOT trusted on its own: a real receipt shipped
|
|
83
|
+
available=true with every field zero (v8.52.0), so the flag is a claim and
|
|
84
|
+
the VALUES are the evidence. record_is_measured reads per-iteration key
|
|
85
|
+
names, hence usd -> cost_usd.
|
|
86
|
+
"""
|
|
87
|
+
if not isinstance(cost, dict):
|
|
88
|
+
return None
|
|
89
|
+
rec = {
|
|
90
|
+
"cost_usd": _num(cost.get("usd")),
|
|
91
|
+
"input_tokens": _num(cost.get("input_tokens")),
|
|
92
|
+
"output_tokens": _num(cost.get("output_tokens")),
|
|
93
|
+
"cache_read_tokens": _num(cost.get("cache_read_tokens")),
|
|
94
|
+
"cache_creation_tokens": _num(cost.get("cache_creation_tokens")),
|
|
95
|
+
}
|
|
96
|
+
if not record_is_measured(rec):
|
|
97
|
+
return None
|
|
98
|
+
return _num(rec["cost_usd"])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _loki_dir(workspace):
|
|
102
|
+
"""Accept either a workspace root or a .loki dir; collect_ wants .loki."""
|
|
103
|
+
if os.path.basename(os.path.normpath(workspace)) == ".loki":
|
|
104
|
+
return workspace
|
|
105
|
+
return os.path.join(workspace, ".loki")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _cannot(why):
|
|
109
|
+
return {"status": "cannot_evaluate", "exit_code": CANNOT, "why": why,
|
|
110
|
+
"actual_usd": None, "baseline_usd": None, "reason": None}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def evaluate(workspace, max_usd=None, baseline_path=None,
|
|
114
|
+
max_increase_pct=None):
|
|
115
|
+
"""Decide the gate. Returns a dict carrying exit_code and the numbers.
|
|
116
|
+
|
|
117
|
+
Every early return is CANNOT (2). The single exit-0 return is at the very
|
|
118
|
+
bottom, downstream of a measurement.
|
|
119
|
+
"""
|
|
120
|
+
if max_usd is None and max_increase_pct is None:
|
|
121
|
+
return _cannot(
|
|
122
|
+
"no budget policy given: pass --max-usd and/or --baseline with "
|
|
123
|
+
"--max-increase-pct. A gate with no policy checks nothing, and "
|
|
124
|
+
"must not report a pass for it.")
|
|
125
|
+
|
|
126
|
+
if baseline_path and max_increase_pct is None:
|
|
127
|
+
# A HALF-HONORED policy must not report a full pass. Without this,
|
|
128
|
+
# `--max-usd 5 --baseline old.json` never opens the baseline and can
|
|
129
|
+
# exit 0 having silently ignored half of what the operator asked for.
|
|
130
|
+
return _cannot(
|
|
131
|
+
"--baseline %s was given without --max-increase-pct, so the "
|
|
132
|
+
"baseline would be silently ignored. Refusing to report a pass "
|
|
133
|
+
"for a policy only half applied." % baseline_path)
|
|
134
|
+
|
|
135
|
+
cost, _model = collect_efficiency(_loki_dir(workspace))
|
|
136
|
+
actual = measured_usd(cost)
|
|
137
|
+
if actual is None:
|
|
138
|
+
# THE POINT OF THIS FILE. Absence is not compliance.
|
|
139
|
+
return _cannot(
|
|
140
|
+
"cost is UNMEASURED for %s -- no efficiency record carried an "
|
|
141
|
+
"observed cost or token count. Unmeasured is not within budget: "
|
|
142
|
+
"this gate cannot say whether the run complied, so it reports no "
|
|
143
|
+
"verdict rather than a green one." % workspace)
|
|
144
|
+
|
|
145
|
+
breaches = []
|
|
146
|
+
|
|
147
|
+
if max_usd is not None:
|
|
148
|
+
# Compared at the precision this gate REPORTS (4dp), not at binary
|
|
149
|
+
# float precision. Summed per-iteration costs land a few ulps over a
|
|
150
|
+
# round ceiling ($0.1+$0.2 > $0.30 is True in binary), which fails a
|
|
151
|
+
# run that is exactly at its budget and prints "$0.3000 exceeds the
|
|
152
|
+
# ceiling $0.3000 by $0.0000" -- a breach of nothing.
|
|
153
|
+
if round(actual - max_usd, 4) > 0:
|
|
154
|
+
breaches.append(
|
|
155
|
+
"cost $%.4f exceeds the ceiling $%.4f by $%.4f (%.1f%% over)"
|
|
156
|
+
% (actual, max_usd, actual - max_usd,
|
|
157
|
+
((actual - max_usd) / max_usd * 100.0) if max_usd else 0.0))
|
|
158
|
+
|
|
159
|
+
baseline = None
|
|
160
|
+
if max_increase_pct is not None:
|
|
161
|
+
if not baseline_path:
|
|
162
|
+
return _cannot(
|
|
163
|
+
"--max-increase-pct needs --baseline <proof.json>: a "
|
|
164
|
+
"percentage increase has nothing to increase from.")
|
|
165
|
+
try:
|
|
166
|
+
with open(baseline_path, "r", encoding="utf-8") as handle:
|
|
167
|
+
proof = json.load(handle)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
return _cannot("baseline unreadable (%s): %s"
|
|
170
|
+
% (baseline_path, exc))
|
|
171
|
+
baseline = measured_usd(
|
|
172
|
+
proof.get("cost") if isinstance(proof, dict) else None)
|
|
173
|
+
if baseline is None:
|
|
174
|
+
return _cannot(
|
|
175
|
+
"baseline receipt %s records no measured cost, so no increase "
|
|
176
|
+
"against it is a real number." % baseline_path)
|
|
177
|
+
|
|
178
|
+
rise = actual - baseline
|
|
179
|
+
if baseline == 0:
|
|
180
|
+
# Evaluable -- both sides ARE measured -- so not a 2. A percentage
|
|
181
|
+
# of zero is undefined, so the absolute rise is what gets named.
|
|
182
|
+
if rise > 0:
|
|
183
|
+
breaches.append(
|
|
184
|
+
"cost rose from a measured $0.0000 baseline to $%.4f "
|
|
185
|
+
"(+$%.4f); percent increase is undefined against zero, so "
|
|
186
|
+
"any rise breaches" % (actual, rise))
|
|
187
|
+
else:
|
|
188
|
+
# Rounded to the precision this gate REPORTS. Binary floats put
|
|
189
|
+
# $1.00 -> $1.10 at 10.000000000000009%, so an exact `>` fails a
|
|
190
|
+
# run that is exactly at its allowance and then prints "10.0% over
|
|
191
|
+
# the allowed 10.0%" -- a verdict its own message contradicts.
|
|
192
|
+
pct = round(rise / baseline * 100.0, 6)
|
|
193
|
+
if pct > max_increase_pct:
|
|
194
|
+
breaches.append(
|
|
195
|
+
"cost rose %.1f%% ($%.4f -> $%.4f, +$%.4f), over the "
|
|
196
|
+
"allowed %.1f%%"
|
|
197
|
+
% (pct, baseline, actual, rise, max_increase_pct))
|
|
198
|
+
|
|
199
|
+
if breaches:
|
|
200
|
+
return {"status": "over_budget", "exit_code": OVER,
|
|
201
|
+
"actual_usd": actual, "baseline_usd": baseline,
|
|
202
|
+
"reason": "; ".join(breaches), "why": None}
|
|
203
|
+
|
|
204
|
+
return {"status": "within_budget", "exit_code": OK, "actual_usd": actual,
|
|
205
|
+
"baseline_usd": baseline, "reason": None, "why": None}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def render(d):
|
|
209
|
+
if d["status"] == "cannot_evaluate":
|
|
210
|
+
return "CANNOT EVALUATE: %s" % d["why"]
|
|
211
|
+
if d["status"] == "over_budget":
|
|
212
|
+
return "OVER BUDGET: %s" % d["reason"]
|
|
213
|
+
return "WITHIN BUDGET: measured cost $%.4f" % d["actual_usd"]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main(argv=None):
|
|
217
|
+
ap = argparse.ArgumentParser(
|
|
218
|
+
description="Fail CI when a run's cost regressed past a budget policy.")
|
|
219
|
+
ap.add_argument("workspace", nargs="?", default=".",
|
|
220
|
+
help="workspace root (or its .loki dir); default .")
|
|
221
|
+
ap.add_argument("--max-usd", type=float, help="absolute USD ceiling")
|
|
222
|
+
ap.add_argument("--baseline", help="baseline proof.json to compare against")
|
|
223
|
+
ap.add_argument("--max-increase-pct", type=float,
|
|
224
|
+
help="allowed %% increase over the baseline's cost")
|
|
225
|
+
ap.add_argument("--json", action="store_true", dest="as_json",
|
|
226
|
+
help="emit the verdict as JSON")
|
|
227
|
+
args = ap.parse_args(argv)
|
|
228
|
+
|
|
229
|
+
d = evaluate(args.workspace, args.max_usd, args.baseline,
|
|
230
|
+
args.max_increase_pct)
|
|
231
|
+
print(json.dumps(d, indent=2) if args.as_json else render(d))
|
|
232
|
+
return d["exit_code"]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
sys.exit(main())
|