loki-mode 8.98.0 → 9.0.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.98.0
6
+ # Loki Mode v9.0.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.98.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v9.0.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.98.0
1
+ 9.0.0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.98.0"
10
+ __version__ = "9.0.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.98.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="9.0.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=A8CE268C65C8280964756E2164756E21
1235
+ //# debugId=4F7AB0182556BEE964756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.98.0'
78
+ __version__ = '9.0.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.98.0",
4
+ "version": "9.0.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.98.0",
5
+ "version": "9.0.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,251 @@
1
+ #!/usr/bin/env python3
2
+ """Pin one run as THE cost baseline, then resolve it later.
3
+
4
+ cost-guard.py compares against a baseline receipt, but somebody has to hand-pick
5
+ that file and keep it fresh. There is no way to say "the current main is the
6
+ reference" and have every later PR compare against it. This is that:
7
+
8
+ tools/baseline-pin.py set <workspace> # pin the newest receipt there
9
+ tools/baseline-pin.py show # what is pinned, and is it intact
10
+ tools/baseline-pin.py path # the proof path, for --baseline
11
+
12
+ tools/cost-guard.py . --max-increase-pct 10 \\
13
+ --baseline "$(tools/baseline-pin.py path)"
14
+
15
+ WHAT MAKES A PIN WORTH TRUSTING, and why each one is a refusal rather than a
16
+ warning:
17
+
18
+ 1. NEVER PIN AN UNVERIFIABLE OR UNMEASURED RUN. A baseline nobody can verify
19
+ makes every later comparison meaningless -- the percentage is computed
20
+ against a number that may have been typed in. A baseline with no measured
21
+ cost makes every percentage undefined. Both refuse, with DIFFERENT messages,
22
+ because the operator's next move differs: one means re-run the build, the
23
+ other means fix instrumentation. Integrity is checked FIRST: a receipt that
24
+ fails it has no trustworthy cost to report, so "no measured cost" would be
25
+ the wrong diagnosis to hand back.
26
+
27
+ Neither predicate is restated here. Integrity is verify_integrity() from
28
+ autonomy/lib/proof-verify.py; measurement is measured_usd() from
29
+ cost-guard.py, which already maps the receipt's cost.usd onto the
30
+ record_is_measured() key name. A second copy of either is how the rule
31
+ drifts.
32
+
33
+ 2. A PIN RECORDS WHAT IT PINNED, not just a path. The receipt's hash, the cost
34
+ observed at pin time, and when. A path alone silently follows a file that
35
+ can be edited afterwards, so "the baseline" would mean whatever that file
36
+ says today.
37
+
38
+ The hash is over the RAW FILE BYTES, deliberately not the canonical
39
+ verification-stripped digest the receipt hashes itself with. That digest is
40
+ blind to any edit confined to the verification block -- swap the whole block
41
+ and it is unchanged. For pinning, the question is "did this file change",
42
+ and a reordered or re-signed file IS a changed file.
43
+
44
+ 3. show DETECTS DRIFT SINCE PINNING and says so loudly, and `path` REFUSES to
45
+ emit a drifted baseline. A stale baseline that quietly drifts is worse than
46
+ none: composed into --baseline "$(...)" it poisons the comparison while
47
+ still looking like a green gate.
48
+
49
+ Exit: 0 fine, 1 refused / drifted / no pin. Never an empty success.
50
+ """
51
+
52
+ import argparse
53
+ import glob
54
+ import hashlib
55
+ import importlib.util
56
+ import json
57
+ import os
58
+ import sys
59
+ import time
60
+
61
+ sys.dont_write_bytecode = True
62
+
63
+ _HERE = os.path.dirname(os.path.abspath(__file__))
64
+ _LIB = os.path.join(os.path.dirname(_HERE), "autonomy", "lib")
65
+ sys.path.insert(0, _LIB)
66
+
67
+
68
+ def _load(name, path):
69
+ spec = importlib.util.spec_from_file_location(name, path)
70
+ mod = importlib.util.module_from_spec(spec)
71
+ spec.loader.exec_module(mod)
72
+ return mod
73
+
74
+
75
+ # Hyphenated filenames, so both need the loader dance rather than an import.
76
+ _pv = _load("proof_verify", os.path.join(_LIB, "proof-verify.py"))
77
+ _cg = _load("cost_guard", os.path.join(_HERE, "cost-guard.py"))
78
+
79
+ OK, REFUSED = 0, 1
80
+ DEFAULT_FILE = os.path.join(".loki", "baseline.json")
81
+
82
+
83
+ def _file_sha256(path):
84
+ with open(path, "rb") as handle:
85
+ return hashlib.sha256(handle.read()).hexdigest()
86
+
87
+
88
+ def newest_receipt(workspace):
89
+ """The most recent proof.json under a workspace, or None.
90
+
91
+ Same convention as ci-gate.py: accept a workspace root or its .loki dir.
92
+ """
93
+ root = os.path.normpath(workspace)
94
+ if os.path.basename(root) == ".loki":
95
+ root = os.path.dirname(root) or "."
96
+ found = glob.glob(os.path.join(root, ".loki", "proofs", "*", "proof.json"))
97
+ return max(found, key=os.path.getmtime) if found else None
98
+
99
+
100
+ class Refused(Exception):
101
+ """A refusal carrying the reason the operator needs to act on."""
102
+
103
+
104
+ def build_pin(workspace):
105
+ """The pin record for a workspace, or raise Refused explaining why not."""
106
+ proof_path = newest_receipt(workspace)
107
+ if proof_path is None:
108
+ raise Refused(
109
+ "no receipt under %s/.loki/proofs/*/proof.json, so there is no run "
110
+ "to pin. Run a build first." % os.path.normpath(workspace))
111
+
112
+ try:
113
+ proof = _pv._load_proof(proof_path)
114
+ except Exception as exc:
115
+ raise Refused("receipt %s could not be read: %s" % (proof_path, exc))
116
+
117
+ verdict = _pv.verify_integrity(proof)
118
+ if not verdict["ok"]:
119
+ raise Refused(
120
+ "receipt %s FAILED integrity verification, so it must not become a "
121
+ "baseline: every later comparison would be against a number nobody "
122
+ "can verify.\n %s"
123
+ % (proof_path,
124
+ "\n ".join(verdict["reasons"] or [verdict["reason"]])))
125
+
126
+ usd = _cg.measured_usd(proof.get("cost"))
127
+ if usd is None:
128
+ raise Refused(
129
+ "receipt %s records NO MEASURED COST, so it must not become a "
130
+ "baseline: a percentage increase against an unmeasured number is "
131
+ "undefined. Unmeasured is not $0.00. Fix the cost instrumentation "
132
+ "for that run, then pin it." % proof_path)
133
+
134
+ return {
135
+ "pin": "loki-cost-baseline/v1",
136
+ "proof_path": os.path.abspath(proof_path),
137
+ "receipt_sha256": _file_sha256(proof_path),
138
+ "cost_usd": usd,
139
+ "pinned_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
140
+ }
141
+
142
+
143
+ def read_pin(pin_file):
144
+ """The pin record, or raise Refused. Never an empty success."""
145
+ if not os.path.exists(pin_file):
146
+ raise Refused(
147
+ "no baseline pinned (%s does not exist). Pin one with: "
148
+ "tools/baseline-pin.py set <workspace>" % pin_file)
149
+ try:
150
+ with open(pin_file, "r", encoding="utf-8") as handle:
151
+ rec = json.load(handle)
152
+ except Exception as exc:
153
+ raise Refused("pin file %s is unreadable: %s" % (pin_file, exc))
154
+ if not isinstance(rec, dict) or not rec.get("proof_path"):
155
+ raise Refused("pin file %s records no proof_path" % pin_file)
156
+ return rec
157
+
158
+
159
+ def check_pin(rec):
160
+ """Re-hash the pinned receipt. Returns (state, detail).
161
+
162
+ state is "intact", "changed", or "missing".
163
+ """
164
+ path = rec["proof_path"]
165
+ if not os.path.exists(path):
166
+ return "missing", "the pinned receipt no longer exists at %s" % path
167
+ now = _file_sha256(path)
168
+ if now != rec.get("receipt_sha256"):
169
+ return "changed", (
170
+ "the pinned receipt CHANGED since it was pinned: %s\n pinned "
171
+ "sha256 %s\n current sha256 %s\n This baseline no longer "
172
+ "describes the run it was pinned from. Re-pin it deliberately, or "
173
+ "every comparison against it is against an unknown."
174
+ % (path, rec.get("receipt_sha256"), now))
175
+ return "intact", "receipt unchanged since pinning"
176
+
177
+
178
+ def main(argv):
179
+ ap = argparse.ArgumentParser(
180
+ description="Pin a run as the cost baseline, then resolve it later.")
181
+ sub = ap.add_subparsers(dest="cmd")
182
+
183
+ p_set = sub.add_parser("set", help="pin the newest receipt in a workspace")
184
+ p_set.add_argument("workspace")
185
+ p_set.add_argument("--file", default=DEFAULT_FILE)
186
+
187
+ p_show = sub.add_parser("show", help="what is pinned, and is it intact")
188
+ p_show.add_argument("--file", default=DEFAULT_FILE)
189
+ p_show.add_argument("--json", action="store_true")
190
+
191
+ p_path = sub.add_parser("path", help="the pinned proof path, for --baseline")
192
+ p_path.add_argument("--file", default=DEFAULT_FILE)
193
+
194
+ args = ap.parse_args(argv)
195
+ if not args.cmd:
196
+ ap.print_help(sys.stderr)
197
+ return REFUSED
198
+
199
+ if args.cmd == "set":
200
+ try:
201
+ rec = build_pin(args.workspace)
202
+ except Refused as exc:
203
+ sys.stderr.write("REFUSED TO PIN: %s\n" % exc)
204
+ return REFUSED
205
+ parent = os.path.dirname(os.path.abspath(args.file))
206
+ if parent:
207
+ os.makedirs(parent, exist_ok=True)
208
+ with open(args.file, "w", encoding="utf-8") as handle:
209
+ json.dump(rec, handle, indent=2, sort_keys=True)
210
+ handle.write("\n")
211
+ sys.stderr.write(
212
+ "PINNED %s\n cost $%.4f sha256 %s\n written to %s\n"
213
+ % (rec["proof_path"], rec["cost_usd"], rec["receipt_sha256"],
214
+ args.file))
215
+ return OK
216
+
217
+ try:
218
+ rec = read_pin(args.file)
219
+ except Refused as exc:
220
+ sys.stderr.write("NO BASELINE: %s\n" % exc)
221
+ return REFUSED
222
+
223
+ state, detail = check_pin(rec)
224
+
225
+ if args.cmd == "path":
226
+ if state != "intact":
227
+ sys.stderr.write("REFUSING TO EMIT BASELINE: %s\n" % detail)
228
+ return REFUSED
229
+ # ONLY the path on stdout, so this composes into --baseline "$(...)".
230
+ sys.stdout.write("%s\n" % rec["proof_path"])
231
+ return OK
232
+
233
+ if getattr(args, "json", False):
234
+ out = dict(rec)
235
+ out["state"] = state
236
+ out["detail"] = detail
237
+ sys.stdout.write(json.dumps(out, indent=2, sort_keys=True) + "\n")
238
+ else:
239
+ sys.stdout.write(
240
+ "baseline: %s\n pinned at %s\n cost at pin $%.4f\n"
241
+ % (rec["proof_path"], rec.get("pinned_at", "unknown"),
242
+ rec.get("cost_usd") or 0.0))
243
+ if state != "intact":
244
+ sys.stderr.write("BASELINE %s: %s\n" % (state.upper(), detail))
245
+ return REFUSED
246
+ sys.stderr.write("baseline INTACT: %s\n" % detail)
247
+ return OK
248
+
249
+
250
+ if __name__ == "__main__":
251
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env python3
2
+ """Generate the policy file and CI snippet that turn the cost gate on.
3
+
4
+ WHY THIS EXISTS. The chain already works: policy-load.py validates a policy
5
+ file, ci-gate.py enforces it, gate-report.py renders the verdict where an
6
+ engineer will see it. Adopting it does not. A team has to hand-write a JSON
7
+ file against a schema documented only in a validator's source, work out that
8
+ `--as-args` is the flag that joins the first tool to the second, and compose
9
+ three commands in the right order. That is several required steps before any
10
+ value, which is the highest-severity adoption defect this project recognises:
11
+ a gate nobody turns on enforces exactly as much as no gate at all.
12
+
13
+ So: one command that writes the file and prints the snippet.
14
+
15
+ python3 tools/gate-init.py . --out .loki-policy.json --print-workflow
16
+
17
+ THE RULE THIS FILE EXISTS TO HOLD, and the reason it is not a template dump:
18
+
19
+ NEVER INVENT A CEILING.
20
+
21
+ A generator's whole appeal is that it fills in the blanks, and a cost ceiling
22
+ is the one blank it must refuse to fill from nothing. A plausible-looking
23
+ $5.00 in a generated file is worse than an empty one: the operator commits it,
24
+ CI goes green, and the number enforced is one nobody chose and nobody can
25
+ defend when it starts blocking merges. The green is the problem -- it reads as
26
+ "reviewed and within budget" when it means "arbitrary". This repo has paid for
27
+ that shape repeatedly, most recently a tarball assertion that passed on "6 or
28
+ more" of 6 patterns: a check reporting a pass without having checked.
29
+
30
+ So a ceiling is emitted ONLY when measured runs exist to derive it from, and
31
+ the derivation is stated in the output. With no measured history the `max_usd`
32
+ key is ABSENT and the operator is told, on stderr, that they must choose one.
33
+
34
+ WHY ABSENT AND NOT null, WHICH IS WHAT YOU WOULD REACH FOR FIRST. Verified by
35
+ running policy-load.py on all four candidates:
36
+
37
+ {"max_usd": null, ...} rejected: "max_usd must be a number"
38
+ {"_note": "...", ...} rejected: unknown policy key
39
+ {"require_receipt": false} rejected: "enforces nothing"
40
+ {"require_receipt": true} loads, exit 0
41
+
42
+ JSON has no comments and policy-load rejects unknown keys, so the "you must
43
+ choose a ceiling" note CANNOT live in the file. It goes to stderr and into the
44
+ printed snippet, where it is at least as visible and cannot break the loader.
45
+ The receipt requirement carries the file on its own so the emitted policy still
46
+ enforces something rather than tripping policy-load's own vacuity check.
47
+
48
+ WHY IT RUNS policy-load ON ITS OWN OUTPUT. A generator that emits a file the
49
+ loader rejects is worse than one that emits nothing: the operator has a
50
+ committed artifact, a passing generator, and a broken gate. Self-validation is
51
+ a subprocess call to the real validator, never a re-implementation of its rules
52
+ here -- a second copy of the schema is how the two drift apart, and the copy
53
+ that drifts is always the one nobody runs.
54
+
55
+ WHAT IT REFUSES TO DO. It never overwrites an existing policy without --force:
56
+ that file is reviewed, committed, and possibly a LOWER ceiling than this tool
57
+ would derive, so silently replacing it is a destructive act that reads as a
58
+ routine re-run. It never writes into .github/ -- that directory is live, and a
59
+ snippet this tool has never executed has no business landing in a real
60
+ workflow. The snippet is labelled as a starting point for that reason.
61
+
62
+ Exit: 0 policy written and validated; 1 refused (existing file, or the
63
+ generated policy failed its own validation); 2 cannot write.
64
+ """
65
+
66
+ import argparse
67
+ import json
68
+ import os
69
+ import subprocess
70
+ import sys
71
+
72
+ _HERE = os.path.dirname(os.path.abspath(__file__))
73
+
74
+ OK, REFUSED, CANNOT = 0, 1, 2
75
+
76
+ DEFAULT_OUT = ".loki-policy.json"
77
+
78
+ # Headroom over observed spend. A ceiling at the median blocks half of all
79
+ # normal runs, which trains the team to bypass the gate -- the failure mode a
80
+ # too-tight budget shares with a too-loose one is that it stops being obeyed.
81
+ HEADROOM = 2.0
82
+
83
+ # ponytail: reuse cost-history's loader and median rather than re-reading
84
+ # JSONL here. Its honesty rules (unmeasured is null and excluded, corrupt lines
85
+ # are counted) are the ones this derivation needs, and a second reader is how
86
+ # they drift.
87
+ sys.path.insert(0, _HERE)
88
+
89
+
90
+ def _load_cost_history():
91
+ """cost-history.py as a module, despite the hyphen in its name."""
92
+ import importlib.util
93
+
94
+ sys.dont_write_bytecode = True
95
+ path = os.path.join(_HERE, "cost-history.py")
96
+ spec = importlib.util.spec_from_file_location("cost_history", path)
97
+ module = importlib.util.module_from_spec(spec)
98
+ spec.loader.exec_module(module)
99
+ return module
100
+
101
+
102
+ def measured_costs(workspace, history_file=None):
103
+ """Every measured USD figure in this workspace's history, oldest first.
104
+
105
+ Rows recorded as unmeasured carry usd=null and are EXCLUDED, never read as
106
+ 0. A workspace whose every run failed to measure has no basis for a
107
+ ceiling, and averaging zeros into one would produce a $0.00 ceiling that
108
+ blocks every future run while looking like a real budget.
109
+ """
110
+ ch = _load_cost_history()
111
+ path = history_file or os.path.join(workspace, ch.DEFAULT_FILE)
112
+ entries, corrupt = ch.load(path)
113
+ if entries is None:
114
+ return [], 0, path
115
+ costs = [e["usd"] for e in entries
116
+ if e.get("measured") and ch._num(e.get("usd")) is not None]
117
+ return costs, corrupt, path
118
+
119
+
120
+ def derive(costs):
121
+ """(ceiling, basis) from measured runs, or (None, why-not).
122
+
123
+ Returns None for the ceiling whenever there is nothing to derive it from.
124
+ That None is the product: it is what stops a number with no basis being
125
+ written into a file someone will commit.
126
+ """
127
+ if not costs:
128
+ return None, ("no measured runs in this workspace's cost history")
129
+ ch = _load_cost_history()
130
+ med = ch.median(costs)
131
+ if med <= 0:
132
+ # Every measured run summed to zero. Present records, no spend: that is
133
+ # a measurement gap wearing a number's clothes, and a $0.00 ceiling
134
+ # derived from it would block every run that ever does real work.
135
+ return None, ("%d measured run(s) but their median is $%.4f; a ceiling "
136
+ "of zero would block every run that does real work"
137
+ % (len(costs), med))
138
+ ceiling = round(med * HEADROOM, 2)
139
+ basis = ("median of %d measured run(s) = $%.4f, times %.1fx headroom"
140
+ % (len(costs), med, HEADROOM))
141
+ return ceiling, basis
142
+
143
+
144
+ def build_policy(ceiling):
145
+ """The policy dict. max_usd is ABSENT, never null, when unset.
146
+
147
+ require_receipt is always true: it is the one policy that needs no measured
148
+ history to justify, and without it a ceiling-less policy would imply no
149
+ ci-gate flags at all -- which policy-load correctly rejects as a gate with
150
+ nothing to check.
151
+ """
152
+ policy = {"require_receipt": True}
153
+ if ceiling is not None:
154
+ policy["max_usd"] = ceiling
155
+ return policy
156
+
157
+
158
+ def validate(path):
159
+ """Run the REAL loader on the generated file. Returns (ok, message)."""
160
+ proc = subprocess.run(
161
+ [sys.executable, os.path.join(_HERE, "policy-load.py"),
162
+ "--file", path, "--as-args"],
163
+ capture_output=True, text=True)
164
+ if proc.returncode != 0:
165
+ return False, (proc.stderr.strip() or
166
+ "policy-load exited %d with no message" % proc.returncode)
167
+ return True, proc.stdout.strip()
168
+
169
+
170
+ def workflow_snippet(out_path):
171
+ """A starting point, NOT a tested workflow. Labelled as such in the text.
172
+
173
+ This tool has never run this YAML. Calling it verified would be the same
174
+ manufactured confidence the gate itself exists to prevent, one layer up.
175
+ """
176
+ return """# STARTING POINT -- NOT TESTED. This tool generated this snippet but has
177
+ # never executed it. Review it, adapt the checkout/setup steps to your repo,
178
+ # and run it once on a branch before trusting it to block a merge.
179
+ #
180
+ # Add to a job in .github/workflows/<your-workflow>.yml:
181
+
182
+ - name: Cost gate
183
+ run: |
184
+ python3 tools/ci-gate.py . --json \\
185
+ $(python3 tools/policy-load.py --file {out} --as-args) \\
186
+ | python3 tools/gate-report.py --format github
187
+ """.format(out=out_path)
188
+
189
+
190
+ def generate(workspace, out_path, force=False, history_file=None):
191
+ """Write the policy. Returns a verdict dict; never overwrites silently."""
192
+ if os.path.exists(out_path) and not force:
193
+ return {"status": "exists", "exit_code": REFUSED,
194
+ "why": ("refusing to overwrite existing policy file %s -- it is "
195
+ "reviewed and committed, and may hold a lower ceiling "
196
+ "than this tool would derive. Re-run with --force to "
197
+ "replace it." % out_path),
198
+ "policy": None, "ceiling": None, "basis": None,
199
+ "out": out_path}
200
+
201
+ costs, corrupt, hist_path = measured_costs(workspace, history_file)
202
+ ceiling, basis = derive(costs)
203
+ policy = build_policy(ceiling)
204
+
205
+ try:
206
+ parent = os.path.dirname(os.path.abspath(out_path))
207
+ if parent:
208
+ os.makedirs(parent, exist_ok=True)
209
+ with open(out_path, "w", encoding="utf-8") as handle:
210
+ handle.write(json.dumps(policy, indent=2, sort_keys=True) + "\n")
211
+ except OSError as exc:
212
+ return {"status": "cannot_write", "exit_code": CANNOT,
213
+ "why": "could not write %s: %s" % (out_path, exc),
214
+ "policy": policy, "ceiling": ceiling, "basis": basis,
215
+ "out": out_path}
216
+
217
+ ok, message = validate(out_path)
218
+ if not ok:
219
+ # Generated and rejected by the real loader. Non-zero, loudly: a
220
+ # committed file the gate cannot load is a gate that never runs.
221
+ return {"status": "invalid", "exit_code": REFUSED,
222
+ "why": ("the generated policy failed policy-load.py: %s" % message),
223
+ "policy": policy, "ceiling": ceiling, "basis": basis,
224
+ "out": out_path}
225
+
226
+ return {"status": "written", "exit_code": OK, "why": None,
227
+ "policy": policy, "ceiling": ceiling, "basis": basis,
228
+ "out": out_path, "ci_gate_args": message,
229
+ "history_file": hist_path, "measured_runs": len(costs),
230
+ "corrupt_lines": corrupt}
231
+
232
+
233
+ def main(argv=None):
234
+ ap = argparse.ArgumentParser(
235
+ description="Generate a ci-gate policy file and the CI snippet that "
236
+ "enforces it.")
237
+ ap.add_argument("workspace", nargs="?", default=".",
238
+ help="workspace to derive a ceiling from; default .")
239
+ ap.add_argument("--out", default=DEFAULT_OUT,
240
+ help="policy file to write; default %s" % DEFAULT_OUT)
241
+ ap.add_argument("--force", action="store_true",
242
+ help="overwrite an existing policy file")
243
+ ap.add_argument("--print-workflow", action="store_true",
244
+ dest="print_workflow",
245
+ help="also print a starting-point CI snippet")
246
+ ap.add_argument("--history-file", default=None,
247
+ help="cost history JSONL; default <workspace>/.loki/cost-history.jsonl")
248
+ ap.add_argument("--json", action="store_true", dest="as_json",
249
+ help="emit the verdict as JSON")
250
+ args = ap.parse_args(argv)
251
+
252
+ verdict = generate(args.workspace, args.out, force=args.force,
253
+ history_file=args.history_file)
254
+
255
+ if args.as_json:
256
+ print(json.dumps(verdict, indent=2, sort_keys=True))
257
+ elif verdict["exit_code"] != OK:
258
+ print("gate-init: %s" % verdict["why"], file=sys.stderr)
259
+ else:
260
+ print("wrote %s" % verdict["out"])
261
+ print(json.dumps(verdict["policy"], indent=2, sort_keys=True))
262
+ if verdict["ceiling"] is not None:
263
+ print("\nmax_usd = %s, derived from: %s"
264
+ % (verdict["ceiling"], verdict["basis"]))
265
+ print("\nvalidated by policy-load.py; ci-gate args: %s"
266
+ % verdict.get("ci_gate_args", ""))
267
+
268
+ # OUTSIDE the format branch, deliberately. Under --json this note used to
269
+ # vanish entirely: the operator got "ceiling": null and a basis string that
270
+ # reads as a diagnostic field rather than an instruction, and a policy
271
+ # enforcing less than it appears to. A machine-readable format is a reason
272
+ # to render the warning differently, never a reason to drop it -- that is
273
+ # the same "looks configured, enforces nothing" shape the whole chain
274
+ # exists to prevent. stderr keeps stdout valid JSON for a pipe.
275
+ if verdict["exit_code"] == OK and verdict["ceiling"] is None:
276
+ print("\nNO CEILING WAS SET: %s." % verdict["basis"], file=sys.stderr)
277
+ print("The max_usd key is ABSENT rather than guessed. You must "
278
+ "choose a ceiling and add it, for example:\n"
279
+ ' "max_usd": 5.00\n'
280
+ "Record some runs first (python3 tools/cost-history.py "
281
+ "record <workspace>) and re-run with --force to derive one "
282
+ "from measured spend.", file=sys.stderr)
283
+
284
+ if args.print_workflow and verdict["exit_code"] == OK:
285
+ print()
286
+ print(workflow_snippet(verdict["out"]))
287
+
288
+ return verdict["exit_code"]
289
+
290
+
291
+ if __name__ == "__main__":
292
+ sys.exit(main())
@@ -362,7 +362,27 @@ def main(argv=None):
362
362
  ap.add_argument("--json", action="store_true", dest="as_json")
363
363
  args = ap.parse_args(argv)
364
364
 
365
- rep = build_replay(args.workspace)
365
+ # build_replay raises SystemExit on a no-data workspace, which bypassed
366
+ # --json entirely: the caller asked for machine output and got a bare text
367
+ # line, so `json.load(...)` crashed on the failure path -- unreadable to
368
+ # the exact automation this tool exists for. A structured error is still
369
+ # structured output; the exit code carries the verdict either way.
370
+ try:
371
+ rep = build_replay(args.workspace)
372
+ except SystemExit as exc:
373
+ code = exc.code if isinstance(exc.code, int) else EXIT_NO_DATA
374
+ if args.as_json:
375
+ print(json.dumps({
376
+ "status": "no_data",
377
+ "exit_code": code,
378
+ "workspace": args.workspace,
379
+ "why": "no replayable artifacts under this workspace; "
380
+ "see stderr for the specific path",
381
+ "iterations": [],
382
+ "skipped_lines": None,
383
+ }, indent=2))
384
+ return code
385
+
366
386
  if args.as_json:
367
387
  print(json.dumps(rep, indent=2))
368
388
  else: