loki-mode 8.90.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 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.90.0
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.90.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.92.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.90.0
1
+ 8.92.0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.90.0"
10
+ __version__ = "8.92.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.90.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.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=43086F19FDAA53B864756E2164756E21
1235
+ //# debugId=E465E6ADABBF4BDF64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.90.0'
78
+ __version__ = '8.92.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.90.0",
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.90.0",
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",
@@ -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())
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """List the tools that ship with loki, and say which ones a user can reach.
3
+
4
+ WHY THIS EXISTS. Eleven tools live under tools/ and NONE of them appears in
5
+ `loki help`. Every one was built, tested, packaged -- and left undiscoverable.
6
+ That is the same shape this repo has now paid for seven times: auto-detection
7
+ sat unwired from v5.0.0 to v8.64.0; the receipt's own `--human` explanation
8
+ shipped in v8.73.0 and reached no CLI path until v8.84.0; `preflight.sh` was
9
+ released in v8.87.0 into a directory that is not in package.json `files[]`, so
10
+ no npm user could run it at all.
11
+
12
+ A capability nobody can find is worth exactly as much as one that does not
13
+ exist. This is the cheapest possible fix for that class: a single honest index.
14
+
15
+ THE HONESTY RULES, which this repo has spent 13+ surfaces establishing and
16
+ which apply to an index just as much as to a cost figure:
17
+
18
+ - A tool with no description reads "(no description)", never an invented one.
19
+ Guessing a summary from a filename is fabrication; the reader cannot tell a
20
+ guess from a fact, so the guess is worse than the gap.
21
+
22
+ - SHIPPED status is derived from package.json `files[]`, the same source npm
23
+ uses. A tool present in the repo but absent from the tarball is reported as
24
+ NOT SHIPPED rather than silently listed alongside the reachable ones. That
25
+ is exactly the v8.87.0 defect, and an index that hid it would have helped
26
+ the release lie.
27
+
28
+ - An empty tools/ directory says so and exits non-zero. Zero tools is not a
29
+ successful listing.
30
+
31
+ Reads the filesystem only. Starts nothing, spends nothing, contacts nothing.
32
+ """
33
+
34
+ import argparse
35
+ import ast
36
+ import json
37
+ import os
38
+ import sys
39
+
40
+ _HERE = os.path.dirname(os.path.abspath(__file__))
41
+ _ROOT = os.path.dirname(_HERE)
42
+
43
+ NO_DESC = "(no description)"
44
+
45
+
46
+ def _first_sentence(text):
47
+ """First line of a docstring or header block, trimmed.
48
+
49
+ Deliberately does NOT reflow or summarise: the author's own first line is
50
+ reported verbatim, so nothing here can invent a claim the tool does not make.
51
+ """
52
+ for raw in (text or "").splitlines():
53
+ line = raw.strip()
54
+ if line:
55
+ return line if len(line) <= 100 else line[:97] + "..."
56
+ return ""
57
+
58
+
59
+ def describe(path):
60
+ """Pull a one-line description from a tool, or report its absence.
61
+
62
+ Python: the module docstring, parsed rather than regexed, so a `#` inside a
63
+ string literal cannot be mistaken for a comment.
64
+ Shell: the first comment block under the shebang.
65
+ """
66
+ try:
67
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
68
+ src = fh.read()
69
+ except OSError:
70
+ return NO_DESC
71
+
72
+ if path.endswith(".py"):
73
+ try:
74
+ doc = ast.get_docstring(ast.parse(src))
75
+ except SyntaxError:
76
+ doc = None
77
+ return _first_sentence(doc) or NO_DESC
78
+
79
+ lines = []
80
+ for raw in src.splitlines():
81
+ if raw.startswith("#!"):
82
+ continue
83
+ if raw.startswith("#"):
84
+ lines.append(raw.lstrip("#").strip())
85
+ continue
86
+ if lines:
87
+ break
88
+ if raw.strip():
89
+ break
90
+ return _first_sentence("\n".join(lines)) or NO_DESC
91
+
92
+
93
+ def _files_globs():
94
+ """The `files[]` entries from package.json, or None when unreadable.
95
+
96
+ None means UNKNOWN, never "nothing ships": an unreadable manifest is an
97
+ absent measurement, and reporting every tool as unshipped on that basis
98
+ would be its own fabrication.
99
+ """
100
+ try:
101
+ with open(os.path.join(_ROOT, "package.json"), encoding="utf-8") as fh:
102
+ return json.load(fh).get("files") or []
103
+ except (OSError, ValueError):
104
+ return None
105
+
106
+
107
+ def ships(rel, globs):
108
+ """True/False/None -- shipped, not shipped, or manifest unknown."""
109
+ if globs is None:
110
+ return None
111
+ for entry in globs:
112
+ if rel == entry:
113
+ return True
114
+ if entry.endswith("/") and rel.startswith(entry):
115
+ return True
116
+ return False
117
+
118
+
119
+ def collect(tools_dir=None):
120
+ tools_dir = tools_dir or os.path.join(_ROOT, "tools")
121
+ globs = _files_globs()
122
+ out = []
123
+ try:
124
+ names = sorted(os.listdir(tools_dir))
125
+ except OSError:
126
+ return out
127
+ for name in names:
128
+ if not (name.endswith(".py") or name.endswith(".sh")):
129
+ continue
130
+ full = os.path.join(tools_dir, name)
131
+ if not os.path.isfile(full):
132
+ continue
133
+ out.append({
134
+ "name": name,
135
+ "path": "tools/" + name,
136
+ "description": describe(full),
137
+ "shipped": ships("tools/" + name, globs),
138
+ })
139
+ return out
140
+
141
+
142
+ def render(rows):
143
+ lines = ["loki tools", ""]
144
+ width = max([len(r["name"]) for r in rows] + [4])
145
+ for r in rows:
146
+ if r["shipped"] is True:
147
+ mark = " "
148
+ elif r["shipped"] is False:
149
+ mark = "[dev]"
150
+ else:
151
+ mark = "[ ? ]"
152
+ lines.append(" %s %-*s %s" % (mark, width, r["name"], r["description"]))
153
+ lines.append("")
154
+ unshipped = [r["name"] for r in rows if r["shipped"] is False]
155
+ if unshipped:
156
+ lines.append("[dev] = present in this checkout but NOT in the npm package")
157
+ lines.append(" (not in package.json files[]): " + ", ".join(unshipped))
158
+ if any(r["shipped"] is None for r in rows):
159
+ lines.append("[ ? ] = package.json could not be read, so shipping is UNKNOWN")
160
+ lines.append("Run one with: python3 tools/<name> (this index reads files only)")
161
+ return "\n".join(lines)
162
+
163
+
164
+ def main(argv=None):
165
+ ap = argparse.ArgumentParser(description="List loki's bundled tools.")
166
+ ap.add_argument("--json", action="store_true")
167
+ ap.add_argument("--tools-dir", default=None)
168
+ args = ap.parse_args(argv)
169
+
170
+ rows = collect(args.tools_dir)
171
+ if not rows:
172
+ # Zero tools is not a successful listing.
173
+ msg = "no tools found under tools/ -- nothing to list"
174
+ if args.json:
175
+ print(json.dumps({"tools": [], "error": msg}, indent=2))
176
+ else:
177
+ print(msg, file=sys.stderr)
178
+ return 2
179
+
180
+ if args.json:
181
+ print(json.dumps({"tools": rows, "count": len(rows)}, indent=2))
182
+ else:
183
+ print(render(rows))
184
+ return 0
185
+
186
+
187
+ if __name__ == "__main__":
188
+ sys.exit(main())