loki-mode 8.78.0 → 8.80.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/dashboard/server.py +71 -7
- package/events/emit.sh +40 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +59 -1
- package/memory/rag_injector.py +17 -2
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/loader.sh +12 -4
- package/providers/opencode.sh +12 -4
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.80.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.80.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.80.0
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -7359,6 +7359,49 @@ def _get_model_pricing() -> dict:
|
|
|
7359
7359
|
return _MODEL_PRICING
|
|
7360
7360
|
|
|
7361
7361
|
|
|
7362
|
+
# The five fields whose presence makes ONE efficiency record a measurement.
|
|
7363
|
+
# Mirrors _MEASURED_FIELDS in autonomy/lib/efficiency_cost.py, which is the
|
|
7364
|
+
# canonical source. Kept as a local copy deliberately: the dashboard must not
|
|
7365
|
+
# sys.path-hack into autonomy/lib at request time just to read a constant.
|
|
7366
|
+
_MEASURED_FIELDS = (
|
|
7367
|
+
"cost_usd",
|
|
7368
|
+
"input_tokens",
|
|
7369
|
+
"output_tokens",
|
|
7370
|
+
"cache_read_tokens",
|
|
7371
|
+
"cache_creation_tokens",
|
|
7372
|
+
)
|
|
7373
|
+
|
|
7374
|
+
|
|
7375
|
+
def _record_is_measured(rec: Any) -> bool:
|
|
7376
|
+
"""True when ONE efficiency record actually carries an observed value.
|
|
7377
|
+
|
|
7378
|
+
Mirrors record_is_measured() in autonomy/lib/efficiency_cost.py. Same field
|
|
7379
|
+
list, same bool exclusion, same semantics -- read that docstring for the
|
|
7380
|
+
reasoning. Do not let the two drift.
|
|
7381
|
+
|
|
7382
|
+
A PRESENT FILE IS NOT A MEASUREMENT. A run that did work necessarily
|
|
7383
|
+
consumed tokens, so an all-zero record means we FAILED TO MEASURE, and
|
|
7384
|
+
unmeasured must read as unknown rather than as free. This is the same
|
|
7385
|
+
defect fixed on the receipt (v8.52.0), the prompt (v8.53.0), the verifier
|
|
7386
|
+
(v8.54.0), the cost summary (v8.69.0) and kpis.ts (v8.72.0/v8.74.0); the
|
|
7387
|
+
two dashboard cost readers were never audited for it.
|
|
7388
|
+
|
|
7389
|
+
Note the `and v` is a truthiness test on ONE field of ONE record, which is
|
|
7390
|
+
the intended rule (zero contributes no evidence). It is NOT a guard on the
|
|
7391
|
+
aggregate: a set of measured records summing to $0.00 is a real measured
|
|
7392
|
+
zero and must still render 0.0, never null.
|
|
7393
|
+
"""
|
|
7394
|
+
if not isinstance(rec, dict):
|
|
7395
|
+
return False
|
|
7396
|
+
for key in _MEASURED_FIELDS:
|
|
7397
|
+
v = rec.get(key)
|
|
7398
|
+
if isinstance(v, bool):
|
|
7399
|
+
continue
|
|
7400
|
+
if isinstance(v, (int, float)) and v:
|
|
7401
|
+
return True
|
|
7402
|
+
return False
|
|
7403
|
+
|
|
7404
|
+
|
|
7362
7405
|
def _calculate_model_cost(
|
|
7363
7406
|
model: str,
|
|
7364
7407
|
input_tokens: int,
|
|
@@ -7414,6 +7457,8 @@ def _compute_cost_snapshot() -> dict:
|
|
|
7414
7457
|
budget_limit = None
|
|
7415
7458
|
budget_used = 0.0
|
|
7416
7459
|
budget_remaining = None
|
|
7460
|
+
# Did ANY record carry an observed value? Not "was a file present".
|
|
7461
|
+
cost_recorded = False
|
|
7417
7462
|
|
|
7418
7463
|
# Read efficiency files (one JSON file per iteration/task).
|
|
7419
7464
|
# Use the iteration-*.json pattern so this reader sees the same
|
|
@@ -7430,6 +7475,8 @@ def _compute_cost_snapshot() -> dict:
|
|
|
7430
7475
|
# AttributeError. Skip such files rather than 500 the endpoint.
|
|
7431
7476
|
if not isinstance(data, dict):
|
|
7432
7477
|
continue
|
|
7478
|
+
if _record_is_measured(data):
|
|
7479
|
+
cost_recorded = True
|
|
7433
7480
|
|
|
7434
7481
|
inp = data.get("input_tokens", 0)
|
|
7435
7482
|
out = data.get("output_tokens", 0)
|
|
@@ -7483,6 +7530,10 @@ def _compute_cost_snapshot() -> dict:
|
|
|
7483
7530
|
total_input = totals.get("total_input", 0)
|
|
7484
7531
|
total_output = totals.get("total_output", 0)
|
|
7485
7532
|
if total_input > 0 or total_output > 0:
|
|
7533
|
+
# Real observed tokens from the context tracker: this IS a
|
|
7534
|
+
# measurement, even if the recorded USD total happens to
|
|
7535
|
+
# be 0.
|
|
7536
|
+
cost_recorded = True
|
|
7486
7537
|
estimated_cost = totals.get("total_cost_usd", 0.0)
|
|
7487
7538
|
# Rebuild by_model and by_phase from per_iteration data
|
|
7488
7539
|
for it in ctx.get("per_iteration", []):
|
|
@@ -7519,14 +7570,21 @@ def _compute_cost_snapshot() -> dict:
|
|
|
7519
7570
|
# expensive condition, so reporting it for a run with no data would send
|
|
7520
7571
|
# someone hunting a caching problem that does not exist.
|
|
7521
7572
|
_read_in = total_input + total_cache_read
|
|
7573
|
+
# Unmeasured reads as null, never as 0/$0.00. `cost_recorded` is True when
|
|
7574
|
+
# at least one record carried an OBSERVED value (_record_is_measured), so a
|
|
7575
|
+
# set of measured records that genuinely sums to zero still renders 0.0 --
|
|
7576
|
+
# the direction that would otherwise blank real data (the v8.72.0 trap).
|
|
7522
7577
|
return {
|
|
7523
|
-
"total_input_tokens": total_input,
|
|
7524
|
-
"total_output_tokens": total_output,
|
|
7525
|
-
"total_cache_read_tokens": total_cache_read,
|
|
7526
|
-
"total_cache_creation_tokens": total_cache_creation,
|
|
7527
|
-
"total_tokens":
|
|
7578
|
+
"total_input_tokens": total_input if cost_recorded else None,
|
|
7579
|
+
"total_output_tokens": total_output if cost_recorded else None,
|
|
7580
|
+
"total_cache_read_tokens": total_cache_read if cost_recorded else None,
|
|
7581
|
+
"total_cache_creation_tokens": total_cache_creation if cost_recorded else None,
|
|
7582
|
+
"total_tokens": (
|
|
7583
|
+
total_input + total_output + total_cache_read + total_cache_creation
|
|
7584
|
+
) if cost_recorded else None,
|
|
7528
7585
|
"cache_hit_ratio": round(total_cache_read / _read_in, 4) if _read_in > 0 else None,
|
|
7529
|
-
"estimated_cost_usd": round(estimated_cost, 6),
|
|
7586
|
+
"estimated_cost_usd": round(estimated_cost, 6) if cost_recorded else None,
|
|
7587
|
+
"cost_recorded": cost_recorded,
|
|
7530
7588
|
"by_phase": {k: {
|
|
7531
7589
|
"input_tokens": v["input_tokens"],
|
|
7532
7590
|
"output_tokens": v["output_tokens"],
|
|
@@ -7785,7 +7843,13 @@ def _compute_cost_timeline() -> dict:
|
|
|
7785
7843
|
records.sort(key=_iter_key)
|
|
7786
7844
|
cumulative = 0.0
|
|
7787
7845
|
for data in records:
|
|
7788
|
-
|
|
7846
|
+
# A PRESENT FILE IS NOT A MEASUREMENT. This previously flipped on
|
|
7847
|
+
# for any parseable record, so the all-zero records a pre-v8.51.0
|
|
7848
|
+
# codex run wrote reported total_usd $0.00 with cost_recorded True
|
|
7849
|
+
# -- the endpoint asserting the run was FREE. Same predicate as
|
|
7850
|
+
# /api/cost so the two cost readers cannot disagree.
|
|
7851
|
+
if _record_is_measured(data):
|
|
7852
|
+
cost_recorded = True
|
|
7789
7853
|
inp = data.get("input_tokens", 0) or 0
|
|
7790
7854
|
out = data.get("output_tokens", 0) or 0
|
|
7791
7855
|
# Cache tiers, same as the /api/cost path. This snapshot drives the
|
package/events/emit.sh
CHANGED
|
@@ -306,7 +306,47 @@ fi
|
|
|
306
306
|
# trailing newline. `|| true` keeps observability from ever aborting the emit
|
|
307
307
|
# under `set -e` (matches autonomy/run.sh:9896).
|
|
308
308
|
FLAT_EVENT="{\"timestamp\":\"$TIMESTAMP\",\"type\":\"$TYPE_ESC\",\"data\":$PAYLOAD}"
|
|
309
|
+
# An ABSENT size reading is the empty string, NOT 0 -- see the vacuity note
|
|
310
|
+
# below. A missing file legitimately measures 0 bytes; a `stat` that could not
|
|
311
|
+
# run measures NOTHING, and the two must not collapse to the same value.
|
|
312
|
+
_size_before=$(stat -f%z "$EVENTS_LOG" 2>/dev/null || stat -c%s "$EVENTS_LOG" 2>/dev/null || echo "")
|
|
313
|
+
[ -n "$_size_before" ] || _size_before=0
|
|
309
314
|
safe_append_event_jsonl "$EVENTS_LOG" "$FLAT_EVENT" 2>/dev/null || true
|
|
310
315
|
|
|
316
|
+
# VERIFY THE APPEND LANDED. A dropped event is an ABSENT MEASUREMENT, and an
|
|
317
|
+
# absent measurement read as a fact is how "0 events for a stage" gets reported
|
|
318
|
+
# as "the stage did not happen".
|
|
319
|
+
#
|
|
320
|
+
# The helper's exit code cannot be trusted for this: BOTH best-effort fallback
|
|
321
|
+
# paths (the flock-timeout branch at line 62 and the mkdir give-up branch at
|
|
322
|
+
# line 91) end in `printf ... || true; return 0`, so a failed append returns 0.
|
|
323
|
+
# Measured on an unwritable events.jsonl: emit.sh printed an id and exited 0
|
|
324
|
+
# having written ZERO bytes. Compare the file size instead -- it is the only
|
|
325
|
+
# signal that reflects what a downstream reader will actually see.
|
|
326
|
+
#
|
|
327
|
+
# NOT FATAL, BY CONSTRUCTION. emit.sh is fire-and-forget telemetry on the hot
|
|
328
|
+
# path of every run; nothing waits on its result and a wedged or aborted emit
|
|
329
|
+
# costs far more than a dropped line. So this only makes the loss VISIBLE:
|
|
330
|
+
# - the warning goes to STDERR, never stdout. Line 312's `echo "$EVENT_ID"`
|
|
331
|
+
# is the caller contract (`ID=$(bash emit.sh ...)`); anything else on
|
|
332
|
+
# stdout corrupts it.
|
|
333
|
+
# - the whole check sits in an `if`, so a failing `stat` cannot trip `set -e`.
|
|
334
|
+
# - the exit status is unchanged: a dropped event still exits 0.
|
|
335
|
+
# Set LOKI_EMIT_QUIET=1 to suppress the warning (the drop still happens; you
|
|
336
|
+
# are only choosing not to hear about it).
|
|
337
|
+
#
|
|
338
|
+
# GUARD AGAINST VACUITY. If `stat` cannot run, the old `|| echo 0` made BOTH
|
|
339
|
+
# readings 0, they compared equal, and every SUCCESSFUL emit warned. A warning
|
|
340
|
+
# that cries wolf on the hot path is worse than none -- it is exactly what
|
|
341
|
+
# trains people to filter the channel that was supposed to surface real loss.
|
|
342
|
+
# An empty reading means "not measured", and an absent measurement is never
|
|
343
|
+
# evidence of a drop, so we stay silent rather than guess. Verified: with a
|
|
344
|
+
# `stat` forced to exit 1, a healthy emit is silent and still writes its line.
|
|
345
|
+
_size_after=$(stat -f%z "$EVENTS_LOG" 2>/dev/null || stat -c%s "$EVENTS_LOG" 2>/dev/null || echo "")
|
|
346
|
+
if [ "${LOKI_EMIT_QUIET:-0}" != "1" ] && [ -n "$_size_after" ] && [ "$_size_after" = "$_size_before" ]; then
|
|
347
|
+
printf '[loki-events] WARNING: event %s (type=%s) was NOT recorded in %s -- downstream counts will under-report\n' \
|
|
348
|
+
"$EVENT_ID" "$TYPE" "$EVENTS_LOG" >&2
|
|
349
|
+
fi
|
|
350
|
+
|
|
311
351
|
# Output event ID
|
|
312
352
|
echo "$EVENT_ID"
|
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.80.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=2DD3A14345063CF864756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -29,6 +29,21 @@ from typing import Optional, List, Dict, Any
|
|
|
29
29
|
# Add parent directory to path for imports
|
|
30
30
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
31
31
|
|
|
32
|
+
# THE canonical "was this actually measured?" predicate, imported (never
|
|
33
|
+
# restated) from autonomy/lib/efficiency_cost.py. That module's docstring is
|
|
34
|
+
# explicit that a second copy of this rule is how the honesty rule drifts.
|
|
35
|
+
# autonomy/lib has no __init__.py, so it goes on sys.path by file-relative
|
|
36
|
+
# path -- the same pattern tests/test_bench_adapters.py uses. Both autonomy/
|
|
37
|
+
# and mcp/ ship in package.json files[], so this resolves in the npm tarball.
|
|
38
|
+
sys.path.insert(
|
|
39
|
+
0,
|
|
40
|
+
os.path.join(
|
|
41
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
42
|
+
"autonomy", "lib",
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
from efficiency_cost import record_is_measured # noqa: E402
|
|
46
|
+
|
|
32
47
|
# Import event bus for tool call events
|
|
33
48
|
try:
|
|
34
49
|
from events.bus import EventBus, EventType, EventSource, LokiEvent
|
|
@@ -1467,6 +1482,47 @@ async def loki_project_status() -> str:
|
|
|
1467
1482
|
return json.dumps({"error": str(e)})
|
|
1468
1483
|
|
|
1469
1484
|
|
|
1485
|
+
_EFFICIENCY_MEASURED_FIELDS = (
|
|
1486
|
+
"cost_usd",
|
|
1487
|
+
"input_tokens",
|
|
1488
|
+
"output_tokens",
|
|
1489
|
+
"cache_read_tokens",
|
|
1490
|
+
"cache_creation_tokens",
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def _honest_efficiency_record(rec):
|
|
1495
|
+
"""Blank cost/token fields on a record that carries NO observed value.
|
|
1496
|
+
|
|
1497
|
+
loki_agent_metrics is machine-consumed: another agent reads this and does
|
|
1498
|
+
budget accounting on it. Returning cost_usd=0 for a run we simply failed to
|
|
1499
|
+
measure asserts the run was FREE, which is a fabricated fact -- the same
|
|
1500
|
+
class that cost this repo six releases. An unmeasured value must read as
|
|
1501
|
+
null, never as zero.
|
|
1502
|
+
|
|
1503
|
+
The measured/unmeasured decision is delegated wholesale to
|
|
1504
|
+
record_is_measured() (autonomy/lib/efficiency_cost.py), so this surface and
|
|
1505
|
+
the receipt cannot drift apart.
|
|
1506
|
+
|
|
1507
|
+
A genuinely measured ZERO is preserved: record_is_measured() looks at ALL
|
|
1508
|
+
five fields, so a record with cost_usd=0.002 and input_tokens=0 is measured
|
|
1509
|
+
and is returned UNTOUCHED -- that 0 stays 0. The decision is therefore made
|
|
1510
|
+
once for the WHOLE record, never per field: 0 is falsy, so a per-field falsy
|
|
1511
|
+
test would blank exactly those genuine measured zeros (the v8.72.0 trap).
|
|
1512
|
+
"""
|
|
1513
|
+
if not isinstance(rec, dict):
|
|
1514
|
+
return rec
|
|
1515
|
+
if record_is_measured(rec):
|
|
1516
|
+
return rec
|
|
1517
|
+
out = dict(rec)
|
|
1518
|
+
for key in _EFFICIENCY_MEASURED_FIELDS:
|
|
1519
|
+
if key in out:
|
|
1520
|
+
out[key] = None
|
|
1521
|
+
# Explicit, so a consumer can tell "we did not measure" from "absent key".
|
|
1522
|
+
out["measured"] = False
|
|
1523
|
+
return out
|
|
1524
|
+
|
|
1525
|
+
|
|
1470
1526
|
@mcp.tool()
|
|
1471
1527
|
async def loki_agent_metrics() -> str:
|
|
1472
1528
|
"""
|
|
@@ -1486,7 +1542,9 @@ async def loki_agent_metrics() -> str:
|
|
|
1486
1542
|
if fname.endswith('.json'):
|
|
1487
1543
|
fpath = safe_path_join('.loki', 'metrics', 'efficiency', fname)
|
|
1488
1544
|
with safe_open(fpath, 'r') as f:
|
|
1489
|
-
metrics["agents"].append(
|
|
1545
|
+
metrics["agents"].append(
|
|
1546
|
+
_honest_efficiency_record(json.load(f))
|
|
1547
|
+
)
|
|
1490
1548
|
|
|
1491
1549
|
# Read token economics
|
|
1492
1550
|
econ_path = safe_path_join('.loki', 'metrics', 'token-economics.json')
|
package/memory/rag_injector.py
CHANGED
|
@@ -98,9 +98,24 @@ def build_rag_context(query, max_tokens=2000, knowledge_dir=None):
|
|
|
98
98
|
# Support both 'name'/'pattern' and 'description' fields.
|
|
99
99
|
# Every field is sanitized: memory entries are untrusted input and must
|
|
100
100
|
# not be able to inject instructions or break the prompt structure.
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
# `or` chaining, not .get(a, .get(b, default)): a present-but-empty
|
|
102
|
+
# 'description' would otherwise shadow a real 'correct_approach', and a
|
|
103
|
+
# row missing BOTH name keys would resolve to the literal string
|
|
104
|
+
# 'Unknown Pattern' -- truthy, so the substance check below could never
|
|
105
|
+
# fire on it.
|
|
106
|
+
name = _sanitize_field(p.get('name') or p.get('pattern') or '')
|
|
107
|
+
desc = _sanitize_field(p.get('description') or p.get('correct_approach') or '')
|
|
103
108
|
category = _sanitize_field(p.get('category', ''))
|
|
109
|
+
|
|
110
|
+
# Substance check: a pattern that matched only on its category (or
|
|
111
|
+
# whose fields sanitized away to nothing) has no memory to inject.
|
|
112
|
+
# Rendering it as "### Unknown Pattern" under the "patterns were found
|
|
113
|
+
# in the organization knowledge base" header presents a fabricated
|
|
114
|
+
# memory as a real one and burns prompt budget. Name OR description is
|
|
115
|
+
# enough -- category alone is a bucket label, not a memory, and a named
|
|
116
|
+
# pattern with no description is still genuine signal.
|
|
117
|
+
if not name and not desc:
|
|
118
|
+
continue
|
|
104
119
|
source_raw = p.get('_source_project', '')
|
|
105
120
|
# Path().name strips any directory traversal; sanitize the basename too.
|
|
106
121
|
source = _sanitize_field(Path(str(source_raw)).name) if source_raw else ''
|
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.80.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.80.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/providers/loader.sh
CHANGED
|
@@ -76,10 +76,18 @@ validate_provider_config() {
|
|
|
76
76
|
# Variables that must be defined but can be empty string.
|
|
77
77
|
#
|
|
78
78
|
# PROVIDER_AUTONOMOUS_FLAG moved here in v8.2.0: not every CLI needs a flag
|
|
79
|
-
# to run non-interactively
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
79
|
+
# to run non-interactively, so requiring a non-empty value could reject a
|
|
80
|
+
# valid provider as "incomplete". The variable must still be DEFINED -- an
|
|
81
|
+
# author who forgets it entirely is still caught -- but an intentional empty
|
|
82
|
+
# value is legal.
|
|
83
|
+
#
|
|
84
|
+
# The original justification cited `opencode run <prompt>` as already
|
|
85
|
+
# autonomous. That was WRONG and cost opencode users every run: `opencode run
|
|
86
|
+
# --help` (1.18.9) shows `--auto ... [default: false]`, so without it the run
|
|
87
|
+
# blocks on a permission prompt. opencode now declares "--auto". Keep this
|
|
88
|
+
# list for a provider that genuinely needs no flag, but VERIFY against the
|
|
89
|
+
# real CLI's --help before adding one -- an empty value here is a silent hang,
|
|
90
|
+
# not a validation error.
|
|
83
91
|
local allow_empty_vars=(
|
|
84
92
|
PROVIDER_PROMPT_FLAG
|
|
85
93
|
PROVIDER_AUTONOMOUS_FLAG
|
package/providers/opencode.sh
CHANGED
|
@@ -46,7 +46,15 @@ PROVIDER_CLI="opencode"
|
|
|
46
46
|
# CLI Invocation
|
|
47
47
|
# `run` takes the prompt POSITIONALLY; there is no -p/--prompt flag.
|
|
48
48
|
PROVIDER_SUBCOMMAND="run"
|
|
49
|
-
|
|
49
|
+
# --auto is REQUIRED for autonomous operation. Verified against the real CLI
|
|
50
|
+
# (opencode 1.18.9): `opencode run --help` documents
|
|
51
|
+
# --auto auto-approve permissions that are not explicitly denied [default: false]
|
|
52
|
+
# The earlier empty value assumed `opencode run <prompt>` was already
|
|
53
|
+
# non-interactive. It is not: without --auto the run stalls on the first
|
|
54
|
+
# permission prompt with no TTY to answer it, which is indistinguishable from a
|
|
55
|
+
# hang. This is opencode's equivalent of claude's --dangerously-skip-permissions
|
|
56
|
+
# and aider's --yes-always, both of which their providers already pass.
|
|
57
|
+
PROVIDER_AUTONOMOUS_FLAG="--auto"
|
|
50
58
|
PROVIDER_PROMPT_FLAG=""
|
|
51
59
|
PROVIDER_PROMPT_POSITIONAL=true
|
|
52
60
|
|
|
@@ -123,7 +131,7 @@ provider_invoke() {
|
|
|
123
131
|
shift
|
|
124
132
|
[ -n "$prompt" ] || return 1
|
|
125
133
|
command -v opencode >/dev/null 2>&1 || return 127
|
|
126
|
-
opencode run --model "$PROVIDER_MODEL_DEVELOPMENT" "$prompt" "$@"
|
|
134
|
+
opencode run --auto --model "$PROVIDER_MODEL_DEVELOPMENT" "$prompt" "$@"
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
# provider_invoke_with_tier <tier> <prompt>
|
|
@@ -138,7 +146,7 @@ provider_invoke_with_tier() {
|
|
|
138
146
|
command -v opencode >/dev/null 2>&1 || return 127
|
|
139
147
|
local model
|
|
140
148
|
model="$(provider_get_tier_param "$tier")"
|
|
141
|
-
opencode run --model "$model" "$prompt" "$@"
|
|
149
|
+
opencode run --auto --model "$model" "$prompt" "$@"
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
# provider_invoke_argv <tier> <prompt> -- see providers/claude.sh for rationale.
|
|
@@ -147,5 +155,5 @@ provider_invoke_argv() {
|
|
|
147
155
|
local prompt="${2:-}"
|
|
148
156
|
local model
|
|
149
157
|
model="$(provider_get_tier_param "$tier")"
|
|
150
|
-
_LOKI_INVOKE_ARGV=(opencode run --model "$model" "$prompt")
|
|
158
|
+
_LOKI_INVOKE_ARGV=(opencode run --auto --model "$model" "$prompt")
|
|
151
159
|
}
|