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 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.78.0
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.78.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.80.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.78.0
1
+ 8.80.0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.78.0"
10
+ __version__ = "8.80.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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": total_input + total_output + total_cache_read + total_cache_creation,
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
- cost_recorded = True
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"
@@ -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.78.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.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=EB92D509ECECF70F64756E2164756E21
1235
+ //# debugId=2DD3A14345063CF864756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.78.0'
78
+ __version__ = '8.80.0'
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(json.load(f))
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')
@@ -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
- name = _sanitize_field(p.get('name', p.get('pattern', 'Unknown Pattern')))
102
- desc = _sanitize_field(p.get('description', p.get('correct_approach', '')))
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.78.0",
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.78.0",
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",
@@ -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. `opencode run <prompt>` is already autonomous, so
80
- # requiring a non-empty value rejected a perfectly valid provider as
81
- # "incomplete". The variable must still be DEFINED -- an author who forgets
82
- # it entirely is still caught -- but an intentional empty value is legal.
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
@@ -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
- PROVIDER_AUTONOMOUS_FLAG=""
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
  }