loki-mode 8.97.0 → 8.99.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.97.0
6
+ # Loki Mode v8.99.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.97.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.99.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.97.0
1
+ 8.99.0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.97.0"
10
+ __version__ = "8.99.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.97.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.99.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=B171B033D661F73464756E2164756E21
1235
+ //# debugId=AB32002F01ECEFDE64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.97.0'
78
+ __version__ = '8.99.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.97.0",
4
+ "version": "8.99.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.97.0",
5
+ "version": "8.99.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",
@@ -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:
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Zero-cost demonstration of the verification chain, on receipts it builds
4
+ # itself. The point a prospective user cannot otherwise see without paying for
5
+ # a full build: this system re-checks its own output, and says so honestly when
6
+ # a receipt does not hold up.
7
+ #
8
+ # tools/verify-demo.sh [--keep]
9
+ #
10
+ # WHAT MAKES THIS HONEST, AND WHY IT IS BUILT THIS WAY.
11
+ #
12
+ # Every verdict below is printed by the REAL tool, verbatim. Nothing here
13
+ # echoes a "VERIFIED" of its own. A demo that printed its own verdicts would
14
+ # keep printing them after the verifier broke -- it would be a claim about the
15
+ # product rather than an exercise of it, which is the same defect the tools it
16
+ # demonstrates exist to prevent. So each step runs the actual script, shows the
17
+ # actual output, and asserts the actual exit code. If the chain regresses, this
18
+ # fails; that is deliberate, and makes it a smoke test as well as a demo.
19
+ #
20
+ # The receipts are SYNTHETIC. A scratch git repo with one commit and one
21
+ # uncommitted edit is created in a tempdir, and the real proof generator is run
22
+ # over it. Synthetic means no build ran, no provider was contacted and no money
23
+ # was spent -- it does NOT mean the receipt is fake. It is a genuine receipt
24
+ # about a trivially small piece of real work, which is exactly what lets the
25
+ # real verifier reach a real VERIFIED instead of a hedge.
26
+ #
27
+ # The scratch repo is a fresh mktemp -d. Nothing is written inside the user's
28
+ # own .loki, and the demo never touches global git config.
29
+
30
+ set -uo pipefail
31
+
32
+ KEEP=0
33
+ for arg in "$@"; do
34
+ case "$arg" in
35
+ --keep) KEEP=1 ;;
36
+ -h|--help) sed -n '2,28p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'; exit 0 ;;
37
+ *) echo "verify-demo: unknown argument: $arg" >&2; exit 64 ;;
38
+ esac
39
+ done
40
+
41
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
42
+ ROOT="$(cd "$HERE/.." && pwd)"
43
+
44
+ GENERATOR="$ROOT/autonomy/lib/proof-generator.py"
45
+ ATTEST="$HERE/receipt-attest.py"
46
+ GATE="$HERE/ci-gate.py"
47
+
48
+ # Requirement: name the missing tool and stop. A demo that skipped a step and
49
+ # still reported success would be asserting a capability it never exercised.
50
+ missing=0
51
+ for tool in "$GENERATOR" "$ATTEST" "$GATE"; do
52
+ if [ ! -f "$tool" ]; then
53
+ echo "verify-demo: required tool missing: $tool" >&2
54
+ missing=1
55
+ fi
56
+ done
57
+ if ! command -v git >/dev/null 2>&1; then
58
+ echo "verify-demo: required tool missing: git" >&2
59
+ missing=1
60
+ fi
61
+ if [ "$missing" -ne 0 ]; then
62
+ echo "verify-demo: cannot demonstrate the verification chain with a tool absent" >&2
63
+ exit 1
64
+ fi
65
+
66
+ SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/loki-verify-demo-XXXXXX")"
67
+
68
+ cleanup() {
69
+ if [ "$KEEP" -eq 1 ]; then
70
+ echo ""
71
+ echo "scratch kept at: $SCRATCH"
72
+ else
73
+ rm -rf "$SCRATCH"
74
+ fi
75
+ }
76
+ trap cleanup EXIT
77
+
78
+ rule() { printf '%s\n' "------------------------------------------------------------"; }
79
+
80
+ step_failed() {
81
+ echo ""
82
+ echo "verify-demo: FAILED -- $1" >&2
83
+ echo "verify-demo: the verification chain did not behave as this demo claims." >&2
84
+ exit 1
85
+ }
86
+
87
+ cat <<BANNER
88
+ ============================================================
89
+ Loki Mode -- verification chain demo
90
+ ============================================================
91
+
92
+ THESE RECEIPTS ARE SYNTHETIC.
93
+
94
+ No build was run. No provider was contacted. No money was
95
+ spent. This creates a scratch git repository, generates a
96
+ real receipt over it with the real generator, and then runs
97
+ the real verification tools against that receipt.
98
+
99
+ Every verdict below is printed by the tool named above it.
100
+ Nothing in this script prints a verdict of its own.
101
+
102
+ scratch workspace: $SCRATCH
103
+ BANNER
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Build the synthetic workspace: a real repo, a real commit, a real edit.
107
+ # Small, but genuinely present on disk, which is what the verifier re-derives
108
+ # the receipt's git facts from.
109
+ # ---------------------------------------------------------------------------
110
+ cd "$SCRATCH" || step_failed "could not enter the scratch workspace"
111
+
112
+ git init -q . 2>/dev/null || step_failed "could not create the scratch git repository"
113
+ # Repo-local only. The user's global git identity is never touched.
114
+ git config user.name "loki-verify-demo" || step_failed "could not set scratch git identity"
115
+ git config user.email "verify-demo@loki.invalid" || step_failed "could not set scratch git identity"
116
+
117
+ printf 'print("hello")\n' > app.py
118
+ git add app.py || step_failed "could not stage the scratch file"
119
+ git commit -qm "synthetic baseline" || step_failed "could not commit the scratch baseline"
120
+ printf 'print("goodbye")\n' >> app.py
121
+
122
+ mkdir -p .loki/proofs
123
+
124
+ echo ""
125
+ rule
126
+ echo "STEP 1 of 3 -- generate a receipt over the synthetic workspace"
127
+ echo " \$ autonomy/lib/proof-generator.py --out-dir .loki/proofs/good"
128
+ rule
129
+ if ! python3 "$GENERATOR" --loki-dir .loki --out-dir .loki/proofs/good \
130
+ --provider claude --quiet; then
131
+ step_failed "the real proof generator did not produce a receipt"
132
+ fi
133
+ GOOD="$SCRATCH/.loki/proofs/good/proof.json"
134
+ [ -f "$GOOD" ] || step_failed "the generator reported success but wrote no proof.json"
135
+ echo "wrote $(basename "$GOOD") ($(wc -c < "$GOOD" | tr -d ' ') bytes)"
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # 2. The honest receipt verifies.
139
+ # ---------------------------------------------------------------------------
140
+ echo ""
141
+ rule
142
+ echo "STEP 2 of 3 -- verify the untouched receipt"
143
+ echo " \$ tools/receipt-attest.py .loki/proofs/good/proof.json"
144
+ rule
145
+ python3 "$ATTEST" "$GOOD"
146
+ good_exit=$?
147
+ if [ "$good_exit" -ne 0 ]; then
148
+ step_failed "an untouched receipt did not attest cleanly (exit $good_exit, expected 0)"
149
+ fi
150
+ echo ""
151
+ echo " -> exit 0. Every scored axis was checked here, and passed."
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # 3. Tamper with a recorded FACT, not the formatting. The integrity hash is
155
+ # taken over canonicalized bytes, so re-indenting alone proves nothing;
156
+ # changing a claim the receipt makes is the case worth showing.
157
+ # ---------------------------------------------------------------------------
158
+ mkdir -p "$SCRATCH/.loki/proofs/tampered"
159
+ python3 - "$GOOD" "$SCRATCH/.loki/proofs/tampered/proof.json" <<'PY' || step_failed "could not write the tampered receipt"
160
+ import json, sys
161
+ src, dst = sys.argv[1], sys.argv[2]
162
+ proof = json.loads(open(src).read())
163
+ # Inflate the recorded file count: the receipt now claims more work than the
164
+ # repository contains. This is the realistic forgery, and it is a value inside
165
+ # the hashed body rather than a formatting change.
166
+ proof["facts"]["git"]["diff"]["count"] = 999
167
+ open(dst, "w").write(json.dumps(proof, indent=2))
168
+ PY
169
+
170
+ echo ""
171
+ rule
172
+ echo "STEP 3 of 3 -- tamper with the receipt, then re-verify"
173
+ echo " edited facts.git.diff.count: 1 -> 999 (claims work that is not there)"
174
+ echo " \$ tools/receipt-attest.py .loki/proofs/tampered/proof.json"
175
+ rule
176
+ python3 "$ATTEST" "$SCRATCH/.loki/proofs/tampered/proof.json"
177
+ bad_exit=$?
178
+ if [ "$bad_exit" -eq 0 ]; then
179
+ step_failed "a tampered receipt attested as clean (exit 0) -- the verifier did not catch it"
180
+ fi
181
+ echo ""
182
+ echo " -> exit $bad_exit. Caught, with the reason printed above."
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # 4. The gate refuses. A verdict a human reads is not a gate; an exit code CI
186
+ # branches on is. Only the tampered receipt is left in place.
187
+ # ---------------------------------------------------------------------------
188
+ # Moved aside rather than deleted: the gate must see ONLY the tampered receipt
189
+ # (it attests the newest one under .loki/proofs/*), while --keep still leaves
190
+ # the good receipt on disk for anyone -- or any test -- wanting to re-hash it
191
+ # and confirm the digest printed above came from these exact bytes.
192
+ mkdir -p "$SCRATCH/generated"
193
+ mv "$SCRATCH/.loki/proofs/good/proof.json" "$SCRATCH/generated/proof.json" \
194
+ || step_failed "could not set aside the good receipt"
195
+ rm -rf "$SCRATCH/.loki/proofs/good"
196
+
197
+ echo ""
198
+ rule
199
+ echo "AND THE GATE REFUSES TO PASS"
200
+ echo " with only the tampered receipt present:"
201
+ echo " \$ tools/ci-gate.py . --require-receipt"
202
+ rule
203
+ python3 "$GATE" "$SCRATCH" --require-receipt
204
+ gate_exit=$?
205
+ if [ "$gate_exit" -eq 0 ]; then
206
+ step_failed "the CI gate passed (exit 0) on a tampered receipt"
207
+ fi
208
+ echo ""
209
+ echo " -> exit $gate_exit. A CI job branching on this exit code stops here."
210
+
211
+ cat <<'SUMMARY'
212
+
213
+ ============================================================
214
+ Every verdict above was printed by the real tool, over
215
+ receipts generated on this machine seconds ago.
216
+
217
+ Reminder: SYNTHETIC receipts. No build ran, no provider was
218
+ contacted, no money was spent. This demonstrates the
219
+ verification chain; it does not certify any real work.
220
+ ============================================================
221
+ SUMMARY
222
+
223
+ exit 0