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 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.87.0
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.87.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.88.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.87.0
1
+ 8.88.0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.87.0"
10
+ __version__ = "8.88.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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.87.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}
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=963043D4CCDC5F7D64756E2164756E21
1235
+ //# debugId=9115B3EA38A3D81564756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.87.0'
78
+ __version__ = '8.88.0'
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.87.0",
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.87.0",
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())