loki-mode 8.93.0 → 8.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/tools/receipt-attest.py +42 -3
- package/tools/run-replay.py +374 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v8.
|
|
6
|
+
# Loki Mode v8.95.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
-
**v8.
|
|
472
|
+
**v8.95.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.95.0
|
package/dashboard/__init__.py
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.
|
|
2
|
+
var m_=Object.create;var{getPrototypeOf:u_,defineProperty:eK,getOwnPropertyNames:p_}=Object;var d_=Object.prototype.hasOwnProperty;function c_(Z){return this[Z]}var l_,i_,a_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?l_??=new WeakMap:i_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?m_(u_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of p_(Z))if(!d_.call(K,$))eK(K,$,{get:c_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var s_=(Z)=>Z;function n_(Z,X){this[Z]=s_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:n_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as o_}from"url";import{existsSync as UQ}from"fs";import{homedir as r_}from"os";function t_(){let Z=RO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(r_(),".loki")}var RO,i0;var H8=p(()=>{RO=Z$(o_(import.meta.url));i0=t_()});import{readFileSync as e_}from"fs";import{resolve as Zf,dirname as Xf}from"path";import{fileURLToPath as Qf}from"url";function h3(){if(h5!==null)return h5;let Z="8.95.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Xf(Qf(import.meta.url)),Q=X$(X);h5=e_(Zf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var bO={};l0(bO,{runOrThrow:()=>jf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Tf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>yO});async function NQ(Z,X=yO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function jf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Mf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Mf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Tf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return wf?"":Z}var wf,L0,F8,p0,zV0,a0,W8,Q9,v;var S6=p(()=>{wf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),zV0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),W8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as bf}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(bf(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Kh});import{existsSync as Y9,readFileSync as g3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as rf}from"path";import{homedir as tf}from"os";function sO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*LQ/X);if(J>LQ)J=LQ;let z=LQ-J,K=F8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${W8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function Zh(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -1232,4 +1232,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1232
1232
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (h_(),f_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1233
1233
|
`),process.stderr.write(v_),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var uW0=await mW0(Bun.argv.slice(2));process.exit(uW0);
|
|
1234
1234
|
|
|
1235
|
-
//# debugId=
|
|
1235
|
+
//# debugId=FD5A9EB9795BBFB164756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.95.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.95.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",
|
package/tools/receipt-attest.py
CHANGED
|
@@ -334,11 +334,50 @@ def render(record):
|
|
|
334
334
|
return "\n".join(lines)
|
|
335
335
|
|
|
336
336
|
|
|
337
|
+
_USAGE = """usage: receipt-attest.py <proof.json> [--json]
|
|
338
|
+
|
|
339
|
+
Turn a receipt into a portable ATTESTATION a third party can check without the
|
|
340
|
+
original workspace.
|
|
341
|
+
|
|
342
|
+
--json emit the attestation record as JSON
|
|
343
|
+
--help show this message
|
|
344
|
+
|
|
345
|
+
States, which are never collapsed into one another:
|
|
346
|
+
VERIFIED the axis was checked and passed
|
|
347
|
+
FAILED the axis was checked and failed
|
|
348
|
+
UNVERIFIABLE the axis could NOT be checked here, with the reason
|
|
349
|
+
|
|
350
|
+
Exit: 0 all scored axes verified, 1 something FAILED, 2 something was
|
|
351
|
+
UNVERIFIABLE, 64 usage error.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
|
|
337
355
|
def main(argv):
|
|
338
|
-
|
|
339
|
-
|
|
356
|
+
rest = argv[1:]
|
|
357
|
+
|
|
358
|
+
# --help must print usage, NOT be read as a filename. It previously fell
|
|
359
|
+
# through to the positional slot and produced
|
|
360
|
+
# "UNVERIFIABLE -- proof file not found: --help", which is a verification
|
|
361
|
+
# verdict about a file the user never named: a fabricated answer to a
|
|
362
|
+
# question they did not ask.
|
|
363
|
+
if "--help" in rest or "-h" in rest:
|
|
364
|
+
sys.stdout.write(_USAGE)
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
as_json = "--json" in rest
|
|
368
|
+
args = [a for a in rest if a != "--json"]
|
|
369
|
+
|
|
370
|
+
# An unrecognized flag is an ERROR, never a path. Treating "--jsonn" as a
|
|
371
|
+
# proof file reports UNVERIFIABLE for a typo, which reads as a finding
|
|
372
|
+
# about the receipt rather than about the command line.
|
|
373
|
+
unknown = [a for a in args if a.startswith("-")]
|
|
374
|
+
if unknown:
|
|
375
|
+
sys.stderr.write("unknown option(s): %s\n" % " ".join(unknown))
|
|
376
|
+
sys.stderr.write(_USAGE)
|
|
377
|
+
return 64
|
|
378
|
+
|
|
340
379
|
if len(args) != 1:
|
|
341
|
-
sys.stderr.write(
|
|
380
|
+
sys.stderr.write(_USAGE)
|
|
342
381
|
return 64
|
|
343
382
|
record = attest(args[0])
|
|
344
383
|
print(json.dumps(record, indent=2, sort_keys=True) if as_json
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reconstruct what a completed run actually did, iteration by iteration.
|
|
3
|
+
|
|
4
|
+
WHY. Unit cases prove the RULE. Only the recorded artifact proves the CASE.
|
|
5
|
+
A run leaves a flight recorder behind -- .loki/events.jsonl plus the
|
|
6
|
+
per-iteration efficiency records -- and until now reading it back meant either
|
|
7
|
+
hand-parsing JSONL or running scripts/measure-run.sh, which renders ONE
|
|
8
|
+
aggregate stage table for the whole run. An aggregate cannot answer the
|
|
9
|
+
questions that matter after a bad run: did cost climb between iterations, did
|
|
10
|
+
the same gate fail twice, which iteration was the slow one.
|
|
11
|
+
|
|
12
|
+
This reads ONLY what a run already wrote. It starts nothing, spends nothing,
|
|
13
|
+
and never contacts a provider. Replay is free.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
tools/run-replay.py [workspace] # default: .
|
|
17
|
+
tools/run-replay.py --json [ws] # machine-readable
|
|
18
|
+
|
|
19
|
+
THE HONESTY RULES, which are the point of this tool:
|
|
20
|
+
|
|
21
|
+
1. A truncated or unparseable line is COUNTED and REPORTED, never silently
|
|
22
|
+
dropped. measure-run.sh skips bad lines by design, so a corrupt tail
|
|
23
|
+
disappears without a trace and the replay reports a cleaner run than
|
|
24
|
+
happened. A replay that quietly loses data is worse than no replay.
|
|
25
|
+
2. An unmeasured cost reads UNKNOWN, never $0.00 -- via the single shared
|
|
26
|
+
record_is_measured() from autonomy/lib/efficiency_cost.py. Free and
|
|
27
|
+
unmeasured are different claims and only one of them is honest.
|
|
28
|
+
3. A stage that never emitted stage_complete reads "not recorded", NOT 0s.
|
|
29
|
+
0s reads as "instant", which is a different claim. Note a genuine
|
|
30
|
+
duration_s of 0 is real data (fast gates emit it) and is preserved.
|
|
31
|
+
4. An empty or missing events.jsonl says so and exits non-zero. An
|
|
32
|
+
empty-but-successful-looking replay certifies a run that never happened.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
sys.dont_write_bytecode = True
|
|
41
|
+
|
|
42
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
43
|
+
_REPO_ROOT = os.path.dirname(_HERE)
|
|
44
|
+
# Resolved from __file__, never from the workspace argument: the workspace
|
|
45
|
+
# being replayed is a different tree and has no autonomy/lib.
|
|
46
|
+
sys.path.insert(0, os.path.join(_REPO_ROOT, "autonomy", "lib"))
|
|
47
|
+
|
|
48
|
+
from efficiency_cost import record_is_measured # noqa: E402
|
|
49
|
+
|
|
50
|
+
EXIT_NO_DATA = 66 # matches the missing-workspace convention in measure-run.sh
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_events(path):
|
|
54
|
+
"""Return (events, skipped_line_count).
|
|
55
|
+
|
|
56
|
+
Every line that does not parse into a dict is counted. The count is the
|
|
57
|
+
deliverable, not a debug aid: it is the only evidence that the replay is
|
|
58
|
+
reading a whole recording.
|
|
59
|
+
"""
|
|
60
|
+
events = []
|
|
61
|
+
skipped = 0
|
|
62
|
+
with open(path, errors="replace") as fh:
|
|
63
|
+
for line in fh:
|
|
64
|
+
if not line.strip():
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
e = json.loads(line)
|
|
68
|
+
except Exception:
|
|
69
|
+
# MUTATION PROBE TARGET. The increment below is the probe's
|
|
70
|
+
# find-string and must stay the ONLY compound-assignment
|
|
71
|
+
# spelling of it in this file -- which is why the sibling
|
|
72
|
+
# branch writes the increment out longhand, and why this
|
|
73
|
+
# comment does not quote it. A probe whose find-string is
|
|
74
|
+
# ambiguous hits whichever copy comes first (here, a comment)
|
|
75
|
+
# and reports MUTATION SURVIVED, which is indistinguishable
|
|
76
|
+
# from a test that checks nothing.
|
|
77
|
+
skipped += 1
|
|
78
|
+
continue
|
|
79
|
+
if not isinstance(e, dict):
|
|
80
|
+
# A bare JSON scalar parses fine but is not a record.
|
|
81
|
+
skipped = skipped + 1
|
|
82
|
+
continue
|
|
83
|
+
events.append(e)
|
|
84
|
+
return events, skipped
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _iter_key(value):
|
|
88
|
+
"""Iteration number as an int, or None when unusable."""
|
|
89
|
+
if isinstance(value, bool):
|
|
90
|
+
return None
|
|
91
|
+
if isinstance(value, int):
|
|
92
|
+
return value
|
|
93
|
+
if isinstance(value, float):
|
|
94
|
+
return int(value)
|
|
95
|
+
try:
|
|
96
|
+
return int(str(value).strip())
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_cost(workspace, iteration):
|
|
102
|
+
"""Per-iteration cost dict, or None when nothing was measured.
|
|
103
|
+
|
|
104
|
+
Read per FILE, not via collect_efficiency(): that sums the whole directory
|
|
105
|
+
into one aggregate, which cannot answer "did cost climb between
|
|
106
|
+
iterations". record_is_measured() is the shared rule and is applied here
|
|
107
|
+
unchanged -- a second copy of that predicate is how the honesty rule
|
|
108
|
+
drifts.
|
|
109
|
+
"""
|
|
110
|
+
path = os.path.join(
|
|
111
|
+
workspace, ".loki", "metrics", "efficiency",
|
|
112
|
+
"iteration-%d.json" % iteration,
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
with open(path) as fh:
|
|
116
|
+
rec = json.load(fh)
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
if not isinstance(rec, dict) or not record_is_measured(rec):
|
|
120
|
+
return None
|
|
121
|
+
return {
|
|
122
|
+
"usd": float(rec.get("cost_usd") or 0.0),
|
|
123
|
+
"input_tokens": int(rec.get("input_tokens") or 0),
|
|
124
|
+
"output_tokens": int(rec.get("output_tokens") or 0),
|
|
125
|
+
"cache_read_tokens": int(rec.get("cache_read_tokens") or 0),
|
|
126
|
+
"cache_creation_tokens": int(rec.get("cache_creation_tokens") or 0),
|
|
127
|
+
"model": str(rec.get("model") or "") or None,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_replay(workspace):
|
|
132
|
+
"""Reconstruct the run. Returns a dict, or raises SystemExit on no data."""
|
|
133
|
+
events_path = os.path.join(workspace, ".loki", "events.jsonl")
|
|
134
|
+
if not os.path.isfile(events_path):
|
|
135
|
+
print("no events at %s -- pass the workspace directory of a "
|
|
136
|
+
"completed run" % events_path, file=sys.stderr)
|
|
137
|
+
raise SystemExit(EXIT_NO_DATA)
|
|
138
|
+
|
|
139
|
+
events, skipped = _load_events(events_path)
|
|
140
|
+
|
|
141
|
+
# An events file that exists but yields nothing usable is a no-data run.
|
|
142
|
+
# The skipped count still gets reported: a FULLY corrupt file is the worst
|
|
143
|
+
# case of the bug rule 1 guards, so it must never exit silently.
|
|
144
|
+
if not events:
|
|
145
|
+
print("events.jsonl at %s has no parseable records "
|
|
146
|
+
"(%d unparseable line(s) skipped) -- nothing to replay"
|
|
147
|
+
% (events_path, skipped), file=sys.stderr)
|
|
148
|
+
raise SystemExit(EXIT_NO_DATA)
|
|
149
|
+
|
|
150
|
+
# stages[iteration][stage] = {"duration_s": .., "status": ..}
|
|
151
|
+
stages = {}
|
|
152
|
+
iteration_status = {}
|
|
153
|
+
for e in events:
|
|
154
|
+
etype = e.get("type") or e.get("event")
|
|
155
|
+
data = e.get("data") if isinstance(e.get("data"), dict) else {}
|
|
156
|
+
if etype == "stage_complete":
|
|
157
|
+
it = _iter_key(data.get("iteration"))
|
|
158
|
+
name = data.get("stage")
|
|
159
|
+
secs = data.get("duration_s")
|
|
160
|
+
if it is None or not name:
|
|
161
|
+
continue
|
|
162
|
+
# isinstance guard, not truthiness: duration_s of 0 is REAL data
|
|
163
|
+
# from a fast gate. Filtering on falsy would convert a genuine 0
|
|
164
|
+
# into "not recorded" and break the absent-stage rule the other
|
|
165
|
+
# way round.
|
|
166
|
+
if isinstance(secs, bool) or not isinstance(secs, (int, float)):
|
|
167
|
+
continue
|
|
168
|
+
stages.setdefault(it, {})[str(name)] = {
|
|
169
|
+
"duration_s": float(secs),
|
|
170
|
+
"status": str(data.get("status") or "unknown"),
|
|
171
|
+
}
|
|
172
|
+
elif etype == "iteration_complete":
|
|
173
|
+
it = _iter_key(data.get("iteration"))
|
|
174
|
+
if it is not None:
|
|
175
|
+
iteration_status[it] = str(data.get("status") or "unknown")
|
|
176
|
+
|
|
177
|
+
# An events.jsonl truncated at a LINE BOUNDARY -- what a killed run
|
|
178
|
+
# leaves -- parses cleanly, so skipped_lines is 0 and the recording looks
|
|
179
|
+
# whole while a whole iteration is missing. The efficiency records are a
|
|
180
|
+
# second, independent witness to which iterations existed; without them
|
|
181
|
+
# the missing iteration's cost silently vanishes from the total and from
|
|
182
|
+
# "largest cost contributor", with no signal at all. A killed run is
|
|
183
|
+
# exactly the case this tool exists for.
|
|
184
|
+
numbers = set(stages) | set(iteration_status)
|
|
185
|
+
try:
|
|
186
|
+
for name in os.listdir(os.path.join(
|
|
187
|
+
workspace, ".loki", "metrics", "efficiency")):
|
|
188
|
+
if name.startswith("iteration-") and name.endswith(".json"):
|
|
189
|
+
n = _iter_key(name[len("iteration-"):-len(".json")])
|
|
190
|
+
if n is not None:
|
|
191
|
+
numbers.add(n)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
numbers = sorted(numbers)
|
|
195
|
+
if not numbers:
|
|
196
|
+
print("no iteration or stage records in %s "
|
|
197
|
+
"(%d unparseable line(s) skipped) -- nothing to replay"
|
|
198
|
+
% (events_path, skipped), file=sys.stderr)
|
|
199
|
+
raise SystemExit(EXIT_NO_DATA)
|
|
200
|
+
|
|
201
|
+
# The stage vocabulary is the union actually observed, never a hardcoded
|
|
202
|
+
# list: a fixed list rots, and would claim stages were missing on runs
|
|
203
|
+
# that never had them.
|
|
204
|
+
all_stages = sorted({s for per in stages.values() for s in per})
|
|
205
|
+
|
|
206
|
+
iterations = []
|
|
207
|
+
prev_cost = None
|
|
208
|
+
prev_failed = set()
|
|
209
|
+
for n in numbers:
|
|
210
|
+
seen = stages.get(n, {})
|
|
211
|
+
cost = _read_cost(workspace, n)
|
|
212
|
+
failed = {s for s, v in seen.items() if v["status"] == "fail"}
|
|
213
|
+
|
|
214
|
+
# Only comparable when BOTH ends were measured. A delta against an
|
|
215
|
+
# unmeasured neighbour is a fabricated comparison.
|
|
216
|
+
delta = None
|
|
217
|
+
if cost is not None and prev_cost is not None:
|
|
218
|
+
delta = round(cost["usd"] - prev_cost["usd"], 6)
|
|
219
|
+
|
|
220
|
+
iterations.append({
|
|
221
|
+
"iteration": n,
|
|
222
|
+
"status": iteration_status.get(n),
|
|
223
|
+
"stages": {
|
|
224
|
+
s: (seen[s] if s in seen else None) for s in all_stages
|
|
225
|
+
},
|
|
226
|
+
"stages_not_recorded": [s for s in all_stages if s not in seen],
|
|
227
|
+
"cost": cost,
|
|
228
|
+
"cost_delta_usd": delta,
|
|
229
|
+
"cost_comparable": cost is not None and prev_cost is not None,
|
|
230
|
+
"gates_failed": sorted(failed),
|
|
231
|
+
"gates_failed_again": sorted(failed & prev_failed),
|
|
232
|
+
})
|
|
233
|
+
# NOT `cost if cost is not None else prev_cost`: carrying the last
|
|
234
|
+
# measured value across an unmeasured gap makes measured/unmeasured/
|
|
235
|
+
# measured compare iteration 3 against iteration 1 while labelling it
|
|
236
|
+
# "vs previous iteration". That is a fabricated comparison wearing a
|
|
237
|
+
# true-sounding label.
|
|
238
|
+
prev_cost = cost
|
|
239
|
+
prev_failed = failed
|
|
240
|
+
|
|
241
|
+
# Slowest stage overall, summed across iterations that recorded it.
|
|
242
|
+
totals = {}
|
|
243
|
+
for it in iterations:
|
|
244
|
+
for s, v in it["stages"].items():
|
|
245
|
+
if v is not None:
|
|
246
|
+
totals[s] = totals.get(s, 0.0) + v["duration_s"]
|
|
247
|
+
slowest = None
|
|
248
|
+
if totals:
|
|
249
|
+
name = max(totals, key=lambda k: totals[k])
|
|
250
|
+
slowest = {"stage": name, "total_s": round(totals[name], 1)}
|
|
251
|
+
|
|
252
|
+
measured = [it for it in iterations if it["cost"] is not None]
|
|
253
|
+
largest = None
|
|
254
|
+
if measured:
|
|
255
|
+
top = max(measured, key=lambda it: it["cost"]["usd"])
|
|
256
|
+
largest = {
|
|
257
|
+
"iteration": top["iteration"],
|
|
258
|
+
"usd": round(top["cost"]["usd"], 6),
|
|
259
|
+
"measured_iterations": len(measured),
|
|
260
|
+
"total_iterations": len(iterations),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
"workspace": os.path.abspath(workspace),
|
|
265
|
+
"read_only": True,
|
|
266
|
+
"skipped_lines": skipped,
|
|
267
|
+
"iterations": iterations,
|
|
268
|
+
"slowest_stage": slowest,
|
|
269
|
+
"largest_cost_contributor": largest,
|
|
270
|
+
"total_cost_usd": (
|
|
271
|
+
round(sum(it["cost"]["usd"] for it in measured), 6)
|
|
272
|
+
if measured else None
|
|
273
|
+
),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _fmt_cost(cost):
|
|
278
|
+
if cost is None:
|
|
279
|
+
return "cost not recorded"
|
|
280
|
+
return "$%.4f in %d / out %d / cache-read %d tok%s" % (
|
|
281
|
+
cost["usd"], cost["input_tokens"], cost["output_tokens"],
|
|
282
|
+
cost["cache_read_tokens"],
|
|
283
|
+
(" [%s]" % cost["model"]) if cost["model"] else "",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def render(rep):
|
|
288
|
+
out = []
|
|
289
|
+
out.append("RUN REPLAY -- reconstructed from artifacts only.")
|
|
290
|
+
out.append("Nothing was started and nothing was spent; this reads "
|
|
291
|
+
".loki/ and exits.")
|
|
292
|
+
out.append(" workspace: %s" % rep["workspace"])
|
|
293
|
+
if rep["skipped_lines"]:
|
|
294
|
+
out.append(" UNPARSEABLE LINES SKIPPED: %d "
|
|
295
|
+
"(counted, not dropped -- this replay is reading an "
|
|
296
|
+
"incomplete recording)" % rep["skipped_lines"])
|
|
297
|
+
else:
|
|
298
|
+
out.append(" unparseable lines skipped: 0 (whole recording read)")
|
|
299
|
+
out.append("")
|
|
300
|
+
|
|
301
|
+
for i, it in enumerate(rep["iterations"]):
|
|
302
|
+
head = "ITERATION %d" % it["iteration"]
|
|
303
|
+
if it["status"]:
|
|
304
|
+
head += " [%s]" % it["status"]
|
|
305
|
+
out.append(head)
|
|
306
|
+
out.append("-" * 60)
|
|
307
|
+
for name, v in it["stages"].items():
|
|
308
|
+
if v is None:
|
|
309
|
+
out.append(" %-24s not recorded" % name)
|
|
310
|
+
else:
|
|
311
|
+
out.append(" %-24s %6.0fs %s"
|
|
312
|
+
% (name, v["duration_s"], v["status"]))
|
|
313
|
+
out.append(" %-24s %s" % ("cost", _fmt_cost(it["cost"])))
|
|
314
|
+
if it["cost_delta_usd"] is not None:
|
|
315
|
+
direction = "climbed" if it["cost_delta_usd"] > 0 else "fell"
|
|
316
|
+
out.append(" %-24s %s $%+.4f vs previous iteration"
|
|
317
|
+
% ("change", direction, it["cost_delta_usd"]))
|
|
318
|
+
elif it["cost"] is None:
|
|
319
|
+
out.append(" %-24s cannot compare, cost not recorded"
|
|
320
|
+
% "change")
|
|
321
|
+
elif i > 0:
|
|
322
|
+
out.append(" %-24s cannot compare, previous iteration's cost "
|
|
323
|
+
"not recorded" % "change")
|
|
324
|
+
if it["gates_failed"]:
|
|
325
|
+
out.append(" %-24s %s" % ("gates failed",
|
|
326
|
+
", ".join(it["gates_failed"])))
|
|
327
|
+
if it["gates_failed_again"]:
|
|
328
|
+
out.append(" %-24s %s <-- failed in the previous iteration too"
|
|
329
|
+
% ("REPEAT FAILURE",
|
|
330
|
+
", ".join(it["gates_failed_again"])))
|
|
331
|
+
out.append("")
|
|
332
|
+
|
|
333
|
+
out.append("=" * 60)
|
|
334
|
+
if rep["slowest_stage"]:
|
|
335
|
+
out.append("slowest stage overall : %s (%.0fs across the run)"
|
|
336
|
+
% (rep["slowest_stage"]["stage"],
|
|
337
|
+
rep["slowest_stage"]["total_s"]))
|
|
338
|
+
else:
|
|
339
|
+
out.append("slowest stage overall : no stage durations recorded")
|
|
340
|
+
|
|
341
|
+
lg = rep["largest_cost_contributor"]
|
|
342
|
+
if lg is None:
|
|
343
|
+
out.append("largest cost : cost not recorded for any "
|
|
344
|
+
"iteration")
|
|
345
|
+
else:
|
|
346
|
+
out.append("largest cost : iteration %d at $%.4f"
|
|
347
|
+
% (lg["iteration"], lg["usd"]))
|
|
348
|
+
if lg["measured_iterations"] < lg["total_iterations"]:
|
|
349
|
+
# A claim over a subset must say it is a claim over a subset.
|
|
350
|
+
out.append(" (largest among the %d of %d "
|
|
351
|
+
"iterations that recorded cost)"
|
|
352
|
+
% (lg["measured_iterations"], lg["total_iterations"]))
|
|
353
|
+
out.append("total measured cost : $%.4f" % rep["total_cost_usd"])
|
|
354
|
+
return "\n".join(out)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def main(argv=None):
|
|
358
|
+
ap = argparse.ArgumentParser(
|
|
359
|
+
description="Replay a completed run from its artifacts. Reads only; "
|
|
360
|
+
"starts nothing, spends nothing.")
|
|
361
|
+
ap.add_argument("workspace", nargs="?", default=".")
|
|
362
|
+
ap.add_argument("--json", action="store_true", dest="as_json")
|
|
363
|
+
args = ap.parse_args(argv)
|
|
364
|
+
|
|
365
|
+
rep = build_replay(args.workspace)
|
|
366
|
+
if args.as_json:
|
|
367
|
+
print(json.dumps(rep, indent=2))
|
|
368
|
+
else:
|
|
369
|
+
print(render(rep))
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
374
|
+
sys.exit(main())
|