loki-mode 8.8.0 → 8.8.1

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.8.0
6
+ # Loki Mode v8.8.1
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.8.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.8.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.8.0
1
+ 8.8.1
package/autonomy/run.sh CHANGED
@@ -773,6 +773,35 @@ print(catalog["providers"]["claude"]["cli_aliases"].get(os.environ["_LOKI_SELECT
773
773
  }
774
774
  loki_apply_build_profile
775
775
 
776
+ # Default hang guard for EVERY build, not just simple-web.
777
+ #
778
+ # The two timeouts above are set inside loki_apply_build_profile(), which
779
+ # returns immediately unless LOKI_BUILD_PROFILE=simple-web. So on a normal
780
+ # build both resolved to 0, and 0 means no guard at all -- verified by running
781
+ # the deadline helper directly: `deadline.py 0 0 3 -- sleep 5` runs to
782
+ # completion unkilled. A provider that hung had nothing to stop it.
783
+ #
784
+ # IDLE only, and no retry. That is what keeps this compatible with the standing
785
+ # objection recorded above (search: "former invoke_with_timeout"), whose two
786
+ # reasons remain correct:
787
+ #
788
+ # 1. "No safe generous default" applies to a fixed TOTAL timeout, which
789
+ # cannot tell a long legitimate iteration from a hang. An idle timeout
790
+ # can: it measures silence, not duration. Verified both directions --
791
+ # `sleep 600` under a 120s idle cap dies, while a process emitting output
792
+ # every second survives indefinitely. A coding agent streams constantly;
793
+ # one silent for two minutes is not working.
794
+ # 2. "Wrong retry semantics" stands, so nothing here retries. The call is
795
+ # killed, and the existing failure path handles it. Re-running an agent
796
+ # that may have already edited files remains off the table.
797
+ #
798
+ # 7200s hard ceiling is a backstop against a process that streams forever
799
+ # without converging; the idle cap is the load-bearing guard. Both are
800
+ # overridable, and setting either to 0 restores the old unguarded behaviour.
801
+ : "${LOKI_PROVIDER_IDLE_TIMEOUT:=120}"
802
+ : "${LOKI_PROVIDER_CALL_TIMEOUT:=7200}"
803
+ export LOKI_PROVIDER_IDLE_TIMEOUT LOKI_PROVIDER_CALL_TIMEOUT
804
+
776
805
  loki_background_services_enabled() {
777
806
  ! loki_is_supervised_simple_web
778
807
  }
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.8.0"
10
+ __version__ = "8.8.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -678,6 +678,37 @@ async def _push_loki_state_loop() -> None:
678
678
  except (json.JSONDecodeError, KeyError):
679
679
  pass
680
680
 
681
+ # Third source: the .loki/pids/ registry, which a
682
+ # CLI-started background run DOES write. Without this
683
+ # the dashboard reported STOPPED for a healthy build:
684
+ # `loki start` writes neither loki.pid nor session.json
685
+ # (run.sh only UPDATES session.json when it already
686
+ # exists), so both checks above failed and every such
687
+ # run fell through to "stopped" while it was actively
688
+ # working. Confirmed against a live build: STATUS.txt
689
+ # said BUILDING and iterations were advancing while the
690
+ # dashboard showed STOPPED with 0 agents.
691
+ #
692
+ # Liveness is proven with os.kill(pid, 0), never by the
693
+ # file's presence -- a stale entry from a crashed run
694
+ # must NOT read as alive, which is the same
695
+ # anti-stale rule BUG-NEW-006 established above.
696
+ if not _pid_alive:
697
+ try:
698
+ _pid_dir = loki_dir / "pids"
699
+ for _entry in _pid_dir.glob("*.json"):
700
+ _rec = _safe_json_read(_entry, {})
701
+ if _rec.get("kind") not in ("wrapper", "runner"):
702
+ continue
703
+ try:
704
+ os.kill(int(_rec.get("pid", 0)), 0)
705
+ except (ValueError, OSError, ProcessLookupError):
706
+ continue
707
+ _pid_alive = True
708
+ break
709
+ except OSError:
710
+ pass
711
+
681
712
  status_str = raw.get("mode", "autonomous")
682
713
  # Control files are the AUTHORITY, and they are checked
683
714
  # first. dashboard-state.json's "mode" is written by the
@@ -2925,7 +2956,7 @@ def _provider_model_offers(provider: str) -> list[dict]:
2925
2956
  Every other provider is offered the generic tiers (small/medium/high), which
2926
2957
  are provider-independent, each annotated with the concrete model id the
2927
2958
  catalog says that provider dispatches. That is what makes the picker read
2928
- "medium -> gpt-5.3-codex" on codex and "medium -> claude-sonnet-5" on claude
2959
+ "medium -> gpt-5.6-terra" on codex and "medium -> claude-sonnet-5" on claude
2929
2960
  without the frontend knowing a single model id.
2930
2961
  """
2931
2962
  if provider == "claude":
@@ -7267,6 +7298,15 @@ _DEFAULT_PRICING = {
7267
7298
  "haiku": {"input": 1.00, "output": 5.00},
7268
7299
  # OpenAI Codex
7269
7300
  "gpt-5.3-codex": {"input": 1.50, "output": 12.00},
7301
+ # gpt-5.6 line: sol (high) / terra (medium, default) / luna (small).
7302
+ # UNVERIFIED RATES. The model IDs are confirmed against
7303
+ # developers.openai.com/api/docs/models, but OpenAI's published per-token
7304
+ # prices for this line were not, so these are placeholders scaled from the
7305
+ # gpt-5.3 rate. They drive a display estimate only, never a gate. Replace
7306
+ # from the pricing page; tools/probe-model-catalog.py is the refresh path.
7307
+ "gpt-5.6-sol": {"input": 2.50, "output": 20.00},
7308
+ "gpt-5.6-terra": {"input": 1.50, "output": 12.00},
7309
+ "gpt-5.6-luna": {"input": 0.50, "output": 4.00},
7270
7310
  }
7271
7311
 
7272
7312
  # Active pricing - starts with defaults, updated from .loki/pricing.json
@@ -7843,6 +7883,9 @@ _PROVIDER_LABELS = {
7843
7883
  "sonnet": "Sonnet 5",
7844
7884
  "haiku": "Haiku 4.5",
7845
7885
  "gpt-5.3-codex": "GPT-5.3 Codex",
7886
+ "gpt-5.6-sol": "GPT-5.6 Sol",
7887
+ "gpt-5.6-terra": "GPT-5.6 Terra",
7888
+ "gpt-5.6-luna": "GPT-5.6 Luna",
7846
7889
  }
7847
7890
 
7848
7891
  # Display-only pricing notes, keyed by model. These annotate the pricing table in
@@ -1,13 +1,38 @@
1
1
  {
2
- "$schema_version": 1,
3
- "_comment": "Rolling pricing table consumed by loki-ts/src/runner/budget.ts. Update this file when Anthropic / OpenAI / others publish new prices; no code change required. Pricing is USD per 1 million tokens. Aliases (opus/sonnet/haiku) point to the latest model of that family per providers/model_catalog.json.",
4
- "_updated": "2026-06-30",
5
- "_source": "https://www.anthropic.com/pricing + provider docs. sonnet=claude-sonnet-5 (list $3/$15; intro $2/$10 through 2026-08-31). opus=claude-opus-4-8. codex=gpt-5.3-codex standard tier.",
6
- "pricing": {
7
- "fable": { "input": 10.0, "output": 50.0 },
8
- "opus": { "input": 5.0, "output": 25.0 },
9
- "sonnet": { "input": 3.0, "output": 15.0 },
10
- "haiku": { "input": 1.0, "output": 5.0 },
11
- "gpt-5.3-codex": { "input": 1.75, "output": 14.0 }
2
+ "$schema_version": 1,
3
+ "_comment": "Rolling pricing table consumed by loki-ts/src/runner/budget.ts. Update this file when Anthropic / OpenAI / others publish new prices; no code change required. Pricing is USD per 1 million tokens. Aliases (opus/sonnet/haiku) point to the latest model of that family per providers/model_catalog.json.",
4
+ "_updated": "2026-06-30",
5
+ "_source": "https://www.anthropic.com/pricing + provider docs. sonnet=claude-sonnet-5 (list $3/$15; intro $2/$10 through 2026-08-31). opus=claude-opus-4-8. codex=gpt-5.3-codex standard tier. Cache tiers: read 0.1x input, write 1.25x input (Anthropic + OpenAI published multipliers).",
6
+ "pricing": {
7
+ "fable": {
8
+ "input": 10.0,
9
+ "output": 50.0,
10
+ "cache_read": 1.0,
11
+ "cache_write": 12.5
12
+ },
13
+ "opus": {
14
+ "input": 5.0,
15
+ "output": 25.0,
16
+ "cache_read": 0.5,
17
+ "cache_write": 6.25
18
+ },
19
+ "sonnet": {
20
+ "input": 3.0,
21
+ "output": 15.0,
22
+ "cache_read": 0.3,
23
+ "cache_write": 3.75
24
+ },
25
+ "haiku": {
26
+ "input": 1.0,
27
+ "output": 5.0,
28
+ "cache_read": 0.1,
29
+ "cache_write": 1.25
30
+ },
31
+ "gpt-5.3-codex": {
32
+ "input": 1.75,
33
+ "output": 14.0,
34
+ "cache_read": 0.175,
35
+ "cache_write": 2.1875
12
36
  }
37
+ }
13
38
  }
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var __=Object.create;var{getPrototypeOf:f_,defineProperty:rK,getOwnPropertyNames:h_}=Object;var v_=Object.prototype.hasOwnProperty;function g_(Z){return this[Z]}var m_,u_,p_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?m_??=new WeakMap:u_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?__(f_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of h_(Z))if(!v_.call(K,$))rK(K,$,{get:g_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var qQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var d_=(Z)=>Z;function c_(Z,X){this[Z]=d_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:c_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var RO={};l0(RO,{lokiDir:()=>j0,homeLokiDir:()=>I4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>o0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as l_}from"url";import{existsSync as GQ}from"fs";import{homedir as i_}from"os";function a_(){let Z=PO;for(let X=0;X<6;X++){if(GQ(n7(Z,"VERSION"))&&GQ(n7(Z,"autonomy/run.sh")))return Z;let Q=tK(Z);if(Q===Z)break;Z=Q}return n7(PO,"..","..","..")}function eK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(GQ(n7(X,"VERSION"))&&GQ(n7(X,"autonomy/run.sh")))return X;let Y=tK(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function I4(){return n7(i_(),".loki")}var PO,o0;var G8=p(()=>{PO=tK(l_(import.meta.url));o0=a_()});import{readFileSync as s_}from"fs";import{resolve as n_,dirname as o_}from"path";import{fileURLToPath as r_}from"url";function _3(){if(h5!==null)return h5;let Z="8.8.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=o_(r_(import.meta.url)),Q=eK(X);h5=s_(n_(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var HQ=p(()=>{G8()});var yO={};l0(yO,{runOrThrow:()=>Uf,run:()=>E0,readStreamCapped:()=>UQ,commandVersion:()=>Of,commandExists:()=>X9,ShellError:()=>Z$,MAX_STDOUT_BYTES:()=>SO});async function UQ(Z,X=SO){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([UQ(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 Uf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Z$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Bf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Bf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Of(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 SO=16777216,Z$;var x9=p(()=>{Z$=class Z$ 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 Lf?"":Z}var Lf,L0,k8,p0,rW0,i0,H8,Q9,v;var S6=p(()=>{Lf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),k8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),rW0=o7("\x1B[0;34m"),i0=o7("\x1B[0;36m"),H8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as Pf}from"fs";async function D7(){if(R4!==void 0)return R4;let Z="/opt/homebrew/bin/python3.12";if(Pf(Z))return R4=Z,Z;let X=await X9("python3.12");if(X)return R4=X,X;let Q=await X9("python3");return R4=Q,Q}async function E7(Z,X={}){let Q=await D7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var R4;var r7=p(()=>{x9()});var eO={};l0(eO,{runStatus:()=>Xh});import{existsSync as Y9,readFileSync as h3,readdirSync as lO,statSync as iO}from"fs";import{resolve as h8,basename as lf}from"path";import{homedir as af}from"os";function aO(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 sO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*NQ/X);if(J>NQ)J=NQ;let z=NQ-J,K=k8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=aO(Z),V=aO(X);return` ${H8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function nf(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
2
+ var __=Object.create;var{getPrototypeOf:f_,defineProperty:rK,getOwnPropertyNames:h_}=Object;var v_=Object.prototype.hasOwnProperty;function g_(Z){return this[Z]}var m_,u_,p_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?m_??=new WeakMap:u_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?__(f_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of h_(Z))if(!v_.call(K,$))rK(K,$,{get:g_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var qQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var d_=(Z)=>Z;function c_(Z,X){this[Z]=d_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:c_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var RO={};l0(RO,{lokiDir:()=>j0,homeLokiDir:()=>I4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>o0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as l_}from"url";import{existsSync as GQ}from"fs";import{homedir as i_}from"os";function a_(){let Z=PO;for(let X=0;X<6;X++){if(GQ(n7(Z,"VERSION"))&&GQ(n7(Z,"autonomy/run.sh")))return Z;let Q=tK(Z);if(Q===Z)break;Z=Q}return n7(PO,"..","..","..")}function eK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(GQ(n7(X,"VERSION"))&&GQ(n7(X,"autonomy/run.sh")))return X;let Y=tK(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function I4(){return n7(i_(),".loki")}var PO,o0;var G8=p(()=>{PO=tK(l_(import.meta.url));o0=a_()});import{readFileSync as s_}from"fs";import{resolve as n_,dirname as o_}from"path";import{fileURLToPath as r_}from"url";function _3(){if(h5!==null)return h5;let Z="8.8.1";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=o_(r_(import.meta.url)),Q=eK(X);h5=s_(n_(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var HQ=p(()=>{G8()});var yO={};l0(yO,{runOrThrow:()=>Uf,run:()=>E0,readStreamCapped:()=>UQ,commandVersion:()=>Of,commandExists:()=>X9,ShellError:()=>Z$,MAX_STDOUT_BYTES:()=>SO});async function UQ(Z,X=SO){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([UQ(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 Uf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Z$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Bf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Bf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Of(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 SO=16777216,Z$;var x9=p(()=>{Z$=class Z$ 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 Lf?"":Z}var Lf,L0,k8,p0,rW0,i0,H8,Q9,v;var S6=p(()=>{Lf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),k8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),rW0=o7("\x1B[0;34m"),i0=o7("\x1B[0;36m"),H8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as Pf}from"fs";async function D7(){if(R4!==void 0)return R4;let Z="/opt/homebrew/bin/python3.12";if(Pf(Z))return R4=Z,Z;let X=await X9("python3.12");if(X)return R4=X,X;let Q=await X9("python3");return R4=Q,Q}async function E7(Z,X={}){let Q=await D7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var R4;var r7=p(()=>{x9()});var eO={};l0(eO,{runStatus:()=>Xh});import{existsSync as Y9,readFileSync as h3,readdirSync as lO,statSync as iO}from"fs";import{resolve as h8,basename as lf}from"path";import{homedir as af}from"os";function aO(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 sO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*NQ/X);if(J>NQ)J=NQ;let z=NQ-J,K=k8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=aO(Z),V=aO(X);return` ${H8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function nf(){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)
@@ -434,7 +434,7 @@ Start a session with: loki start <prd>`}}let J=Kh(Y);return{exitCode:0,stdout:X?
434
434
  `),0}async function Eh(Z){let X=!1;for(let Q of Z)if(Q==="--json")X=!0;else if(Q==="--help"||Q==="-h")return Fh(),0;else return process.stderr.write(`${L0}Unknown option: ${Q}${v}
435
435
  `),process.stderr.write(`Usage: loki doctor [--json]
436
436
  `),1;if(X){let Q=await BL();return process.stdout.write(JSON.stringify(Q,null,2)+`
437
- `),0}return Dh()}var Uh,MQ,Oh,Lh,Th=90;var OL=p(()=>{G8();x9();r7();S6();HQ();Uh=/(\d+\.\d+(?:\.\d+)*)/;MQ={fn:U$};Oh=[{name:"Claude Code",dir:".claude/skills/loki-mode"},{name:"Codex CLI",dir:".codex/skills/loki-mode"},{name:"Cline CLI",dir:".cline/skills/loki-mode"},{name:"Aider CLI",dir:".aider/skills/loki-mode"}];Lh=[{displayName:"Node.js (>= 18)",jsonName:"Node.js",cmd:"node",required:"required",min:"18.0"},{displayName:"Python 3 (>= 3.8)",jsonName:"Python 3",cmd:"python3",required:"required",min:"3.8"},{displayName:"jq",jsonName:"jq",cmd:"jq",required:"required"},{displayName:"git",jsonName:"git",cmd:"git",required:"required"},{displayName:"curl",jsonName:"curl",cmd:"curl",required:"required"},{displayName:"bash (>= 4.0)",jsonName:"bash",cmd:"bash",required:"recommended",min:"4.0"},{displayName:"Bun (>= 1.3)",jsonName:"Bun",cmd:"bun",required:"recommended",min:"1.3"},{displayName:"Claude CLI",jsonName:"Claude CLI",cmd:"claude",required:"optional"},{displayName:"Codex CLI",jsonName:"Codex CLI",cmd:"codex",required:"optional"},{displayName:"Cline CLI",jsonName:"Cline CLI",cmd:"cline",required:"optional"},{displayName:"Aider CLI",jsonName:"Aider CLI",cmd:"aider",required:"optional"}]});var TL={};l0(TL,{writeBudgetState:()=>j$,readEfficiencyDir:()=>wQ,readBudgetState:()=>fh,parseRetryAfter:()=>uh,isRateLimited:()=>mh,checkBudgetLimitForRunner:()=>dh,checkBudgetLimit:()=>ML,calculateRateLimitBackoff:()=>ph,calculateCostFromRecords:()=>TQ,PRICING:()=>A$});import{existsSync as M$,mkdirSync as O$,readdirSync as Ih,readFileSync as T$,renameSync as Ph,writeFileSync as L$}from"fs";import{dirname as w$,join as Z5,resolve as Rh}from"path";import{fileURLToPath as kh}from"url";function xh(){try{let Z=w$(kh(import.meta.url)),X=Rh(Z,"..","..","data","model-pricing.json");if(!M$(X))return S4;let Y=JSON.parse(T$(X,"utf8")).pricing;if(!Y||typeof Y!=="object")return S4;let J={};for(let[z,K]of Object.entries(Y))if(K!==null&&typeof K==="object"&&typeof K.input==="number"&&typeof K.output==="number")J[z]={input:K.input,output:K.output};for(let z of Object.keys(S4))if(!(z in J))return S4;return J}catch{return S4}}function yh(Z){return Math.round((Z+Number.EPSILON)*1e4)/1e4}function bh(Z){let X=(Z??LL).toLowerCase();return A$[X]??A$[LL]}function TQ(Z){let X=0;for(let Q of Z){if(typeof Q.cost_usd==="number"&&Number.isFinite(Q.cost_usd)){X+=Q.cost_usd;continue}let Y=bh(Q.model),J=typeof Q.input_tokens==="number"?Q.input_tokens:0,z=typeof Q.output_tokens==="number"?Q.output_tokens:0;X+=J/1e6*Y.input+z/1e6*Y.output}return yh(X)}function wQ(Z){if(!M$(Z))return[];let X=[],Q;try{Q=Ih(Z)}catch{return[]}for(let Y of Q){if(!Y.endsWith(".json"))continue;let J=Z5(Z,Y);try{let z=T$(J,"utf8"),K=JSON.parse(z);if(K&&typeof K==="object")X.push(K)}catch{}}return X}function _h(Z){if(Z===null||typeof Z!=="object"||Array.isArray(Z))return!1;let X=Z;if(typeof X.limit!=="number"||!Number.isFinite(X.limit))return!1;if(typeof X.budget_limit!=="number"||!Number.isFinite(X.budget_limit))return!1;if(typeof X.budget_used!=="number"||!Number.isFinite(X.budget_used))return!1;if(typeof X.exceeded!=="boolean")return!1;if(X.exceeded_at!==void 0&&typeof X.exceeded_at!=="string")return!1;if(X.created_at!==void 0&&typeof X.created_at!=="string")return!1;return!0}function fh(Z){let X=Z??Z5(j0(),"metrics","budget.json");if(!M$(X))return null;try{let Q=T$(X,"utf8"),Y=JSON.parse(Q);if(!_h(Y))return console.warn(`[budget] discarding malformed budget state at ${X}`),null;return Y}catch{return null}}function j$(Z,X){let Q=X??Z5(j0(),"metrics","budget.json");O$(w$(Q),{recursive:!0});let Y=[];if(Y.push("{"),Y.push(` "limit": ${N$(Z.limit)},`),Y.push(` "budget_limit": ${N$(Z.budget_limit)},`),Y.push(` "budget_used": ${N$(Z.budget_used)},`),Z.exceeded){Y.push(' "exceeded": true,');let K=Z.exceeded_at??jL();Y.push(` "exceeded_at": "${K}"`)}else Y.push(' "exceeded": false');Y.push("}");let J=Y.join(`
437
+ `),0}return Dh()}var Uh,MQ,Oh,Lh,Th=90;var OL=p(()=>{G8();x9();r7();S6();HQ();Uh=/(\d+\.\d+(?:\.\d+)*)/;MQ={fn:U$};Oh=[{name:"Claude Code",dir:".claude/skills/loki-mode"},{name:"Codex CLI",dir:".codex/skills/loki-mode"},{name:"Cline CLI",dir:".cline/skills/loki-mode"},{name:"Aider CLI",dir:".aider/skills/loki-mode"}];Lh=[{displayName:"Node.js (>= 18)",jsonName:"Node.js",cmd:"node",required:"required",min:"18.0"},{displayName:"Python 3 (>= 3.8)",jsonName:"Python 3",cmd:"python3",required:"required",min:"3.8"},{displayName:"jq",jsonName:"jq",cmd:"jq",required:"required"},{displayName:"git",jsonName:"git",cmd:"git",required:"required"},{displayName:"curl",jsonName:"curl",cmd:"curl",required:"required"},{displayName:"bash (>= 4.0)",jsonName:"bash",cmd:"bash",required:"recommended",min:"4.0"},{displayName:"Bun (>= 1.3)",jsonName:"Bun",cmd:"bun",required:"recommended",min:"1.3"},{displayName:"Claude CLI",jsonName:"Claude CLI",cmd:"claude",required:"optional"},{displayName:"Codex CLI",jsonName:"Codex CLI",cmd:"codex",required:"optional"},{displayName:"Cline CLI",jsonName:"Cline CLI",cmd:"cline",required:"optional"},{displayName:"Aider CLI",jsonName:"Aider CLI",cmd:"aider",required:"optional"}]});var TL={};l0(TL,{writeBudgetState:()=>j$,readEfficiencyDir:()=>wQ,readBudgetState:()=>fh,parseRetryAfter:()=>uh,isRateLimited:()=>mh,checkBudgetLimitForRunner:()=>dh,checkBudgetLimit:()=>ML,calculateRateLimitBackoff:()=>ph,calculateCostFromRecords:()=>TQ,PRICING:()=>A$});import{existsSync as M$,mkdirSync as O$,readdirSync as Ih,readFileSync as T$,renameSync as Ph,writeFileSync as L$}from"fs";import{dirname as w$,join as Z5,resolve as Rh}from"path";import{fileURLToPath as kh}from"url";function xh(){try{let Z=w$(kh(import.meta.url)),X=Rh(Z,"..","..","data","model-pricing.json");if(!M$(X))return S4;let Y=JSON.parse(T$(X,"utf8")).pricing;if(!Y||typeof Y!=="object")return S4;let J={};for(let[z,K]of Object.entries(Y))if(K!==null&&typeof K==="object"&&typeof K.input==="number"&&typeof K.output==="number")J[z]={input:K.input,output:K.output};for(let z of Object.keys(S4))if(!(z in J))return S4;return J}catch{return S4}}function yh(Z){return Math.round((Z+Number.EPSILON)*1e4)/1e4}function bh(Z){let X=(Z??LL).toLowerCase();return A$[X]??A$[LL]}function TQ(Z){let X=0;for(let Q of Z){if(typeof Q.cost_usd==="number"&&Number.isFinite(Q.cost_usd)){X+=Q.cost_usd;continue}let Y=bh(Q.model),J=typeof Q.input_tokens==="number"?Q.input_tokens:0,z=typeof Q.output_tokens==="number"?Q.output_tokens:0,K=typeof Q.cache_read_tokens==="number"?Q.cache_read_tokens:0,$=typeof Q.cache_creation_tokens==="number"?Q.cache_creation_tokens:0,W=Y.cache_read??Y.input,V=Y.cache_write??Y.input;X+=J/1e6*Y.input+z/1e6*Y.output+K/1e6*W+$/1e6*V}return yh(X)}function wQ(Z){if(!M$(Z))return[];let X=[],Q;try{Q=Ih(Z)}catch{return[]}for(let Y of Q){if(!Y.endsWith(".json"))continue;let J=Z5(Z,Y);try{let z=T$(J,"utf8"),K=JSON.parse(z);if(K&&typeof K==="object")X.push(K)}catch{}}return X}function _h(Z){if(Z===null||typeof Z!=="object"||Array.isArray(Z))return!1;let X=Z;if(typeof X.limit!=="number"||!Number.isFinite(X.limit))return!1;if(typeof X.budget_limit!=="number"||!Number.isFinite(X.budget_limit))return!1;if(typeof X.budget_used!=="number"||!Number.isFinite(X.budget_used))return!1;if(typeof X.exceeded!=="boolean")return!1;if(X.exceeded_at!==void 0&&typeof X.exceeded_at!=="string")return!1;if(X.created_at!==void 0&&typeof X.created_at!=="string")return!1;return!0}function fh(Z){let X=Z??Z5(j0(),"metrics","budget.json");if(!M$(X))return null;try{let Q=T$(X,"utf8"),Y=JSON.parse(Q);if(!_h(Y))return console.warn(`[budget] discarding malformed budget state at ${X}`),null;return Y}catch{return null}}function j$(Z,X){let Q=X??Z5(j0(),"metrics","budget.json");O$(w$(Q),{recursive:!0});let Y=[];if(Y.push("{"),Y.push(` "limit": ${N$(Z.limit)},`),Y.push(` "budget_limit": ${N$(Z.budget_limit)},`),Y.push(` "budget_used": ${N$(Z.budget_used)},`),Z.exceeded){Y.push(' "exceeded": true,');let K=Z.exceeded_at??jL();Y.push(` "exceeded_at": "${K}"`)}else Y.push(' "exceeded": false');Y.push("}");let J=Y.join(`
438
438
  `)+`
439
439
  `,z=`${Q}.tmp.${process.pid}`;L$(z,J),Ph(z,Q)}function N$(Z){if(Number.isInteger(Z))return Z.toString();return Z.toString()}function jL(Z=new Date){return Z.toISOString().replace(/\.\d{3}Z$/,"Z")}function hh(Z){if(Z===null||Z===void 0||Z==="")return null;if(typeof Z==="number")return Number.isFinite(Z)?Z:null;let X=Z.replace(/[^0-9.]/g,"");if(!X)return null;let Q=Number(X);return Number.isFinite(Q)?Q:null}function ML(Z={}){let X=j0(),Q=hh(Z.budgetLimit??process.env.BUDGET_LIMIT??null);if(Q===null)return{exceeded:!1,warn:!1,current_cost:0,limit:null};let Y=Z.efficiencyDir??Z5(X,"metrics","efficiency"),J=Z.budgetFile??Z5(X,"metrics","budget.json"),z=Z.pauseFile??Z5(X,"PAUSE"),K=Z.signalsDir??Z5(X,"signals"),$=Z.now??(()=>new Date),W=wQ(Y),V=TQ(W);if(V>=Q){let H=jL($());O$(w$(z),{recursive:!0}),L$(z,""),O$(K,{recursive:!0});let B={type:"BUDGET_EXCEEDED",limit:Q,current:V,timestamp:H};return L$(Z5(K,"BUDGET_EXCEEDED"),JSON.stringify(B)),j$({limit:Q,budget_limit:Q,budget_used:V,exceeded:!0,exceeded_at:H},J),{exceeded:!0,warn:!1,current_cost:V,limit:Q}}if(V>0)j$({limit:Q,budget_limit:Q,budget_used:V,exceeded:!1},J);return{exceeded:!1,warn:V>=Sh*Q,current_cost:V,limit:Q}}function mh(Z){let X=Array.isArray(Z)?Z.join(`
440
440
  `):Z;if(!X)return!1;if(vh.test(X))return!0;if(gh.test(X))return!0;return!1}function uh(Z){if(!Z)return 0;AL.lastIndex=0;let X=0,Q;while((Q=AL.exec(Z))!==null){let Y=Q[1];if(Y!==void 0){let J=Number.parseInt(Y,10);if(Number.isFinite(J))X=J}}return X}function ph(Z,X){if(typeof Z==="number"&&Z>0)return Z;let Q=typeof X==="number"&&X>0?X:50,Y=Math.floor(7200/Q);if(Y<60)Y=60;if(Y>300)Y=300;return Y}async function dh(Z){return ML({budgetLimit:Z.budgetLimit,iteration:Z.iterationCount,efficiencyDir:`${Z.lokiDir}/metrics/efficiency`,budgetFile:`${Z.lokiDir}/metrics/budget.json`,pauseFile:`${Z.lokiDir}/PAUSE`,signalsDir:`${Z.lokiDir}/signals`}).exceeded}var S4,A$,LL="sonnet",Sh=0.8,vh,gh,AL;var C$=p(()=>{G8();S4={fable:{input:10,output:50},opus:{input:5,output:25},sonnet:{input:3,output:15},haiku:{input:1,output:5},"gpt-5.3-codex":{input:1.5,output:12}};A$=Object.freeze(xh());vh=/(429|rate.?limit|too many requests|quota exceeded|request limit|retry.?after)/i,gh=/resets [0-9]+[ap]m/,AL=/retry.?after:?\s*([0-9]+)/gi});import{existsSync as CQ,readdirSync as ch,readFileSync as lh,statSync as ih}from"fs";import{join as FQ}from"path";function ah(Z){let X=[],Q=FQ(Z,"votes");if(!CQ(Q))return X;let Y;try{Y=ch(Q)}catch{return X}for(let J of Y){if(!J.startsWith("round-")||!J.endsWith(".json"))continue;try{let z=FQ(Q,J);if(!ih(z).isFile())continue;let K=JSON.parse(lh(z,"utf8"));X.push({iteration:typeof K.iteration==="number"?K.iteration:void 0,verdict:typeof K.verdict==="string"?K.verdict:void 0,complete_votes:typeof K.complete_votes==="number"?K.complete_votes:void 0,total_members:typeof K.total_members==="number"?K.total_members:void 0,threshold:typeof K.threshold==="number"?K.threshold:void 0})}catch{}}return X}function sh(){return{iteration_count:0,total_cost_usd:0,avg_cost_per_iteration:null,total_input_tokens:0,total_output_tokens:0,total_duration_ms:0,avg_duration_ms_per_iteration:null,model_breakdown:{},phase_breakdown:{},status_breakdown:{}}}function nh(){return{council_rounds:0,unanimous_rate:null,approval_rate:null,iteration_success_rate:null}}function oh(Z){let X=sh();if(Z.length===0)return X;X.iteration_count=Z.length,X.total_cost_usd=Math.round(TQ(Z)*1e4)/1e4;for(let Q of Z){if(typeof Q.input_tokens==="number")X.total_input_tokens+=Q.input_tokens;if(typeof Q.output_tokens==="number")X.total_output_tokens+=Q.output_tokens;let Y=Q;if(typeof Y.duration_ms==="number")X.total_duration_ms+=Y.duration_ms;if(typeof Q.model==="string")X.model_breakdown[Q.model]=(X.model_breakdown[Q.model]??0)+1;if(typeof Y.phase==="string")X.phase_breakdown[Y.phase]=(X.phase_breakdown[Y.phase]??0)+1;if(typeof Y.status==="string")X.status_breakdown[Y.status]=(X.status_breakdown[Y.status]??0)+1}return X.avg_cost_per_iteration=Math.round(X.total_cost_usd/X.iteration_count*1e4)/1e4,X.avg_duration_ms_per_iteration=Math.round(X.total_duration_ms/X.iteration_count),X}function rh(Z,X,Q){let Y=nh();if(Y.council_rounds=Z.length,Z.length>0){let J=0,z=0;for(let K of Z){if(typeof K.complete_votes==="number"&&typeof K.total_members==="number"&&K.total_members>0&&K.complete_votes===K.total_members)J+=1;if(K.verdict==="COMPLETE")z+=1}Y.unanimous_rate=Math.round(J/Z.length*1e4)/1e4,Y.approval_rate=Math.round(z/Z.length*1e4)/1e4}if(Q>0)Y.iteration_success_rate=Math.round(X/Q*1e4)/1e4;return Y}function wL(Z){let X=[],Q=FQ(Z,"metrics","efficiency"),Y=FQ(Z,"council"),J=CQ(Q)?wQ(Q):[];if(!CQ(Q))X.push("no .loki/metrics/efficiency/ dir (efficiency KPIs zeroed)");else if(J.length===0)X.push(".loki/metrics/efficiency/ exists but no iteration files found");let z=ah(Y);if(!CQ(Y))X.push("no .loki/council/ dir (accuracy KPIs zeroed)");else if(z.length===0)X.push(".loki/council/ exists but no round-N.json files found");let K=oh(J),$=K.status_breakdown.success??0,W=rh(z,$,K.iteration_count);return{schema_version:1,generated_at:new Date().toISOString(),loki_dir:Z,efficiency:K,accuracy:W,notes:X}}function CL(Z){return JSON.stringify(Z,null,2)}function FL(Z){let X=[];X.push(`Loki Mode KPIs (snapshot at ${Z.generated_at})`),X.push(`Source: ${Z.loki_dir}`),X.push(""),X.push("Efficiency"),X.push(` Iterations: ${Z.efficiency.iteration_count}`),X.push(` Total cost USD: ${Z.efficiency.total_cost_usd}`),X.push(` Avg cost per iter: ${Z.efficiency.avg_cost_per_iteration??"n/a"}`),X.push(` Total input tokens: ${Z.efficiency.total_input_tokens}`),X.push(` Total output tokens: ${Z.efficiency.total_output_tokens}`),X.push(` Total duration (ms): ${Z.efficiency.total_duration_ms}`),X.push(` Avg duration / iter: ${Z.efficiency.avg_duration_ms_per_iteration??"n/a"}`);let Q=Object.entries(Z.efficiency.model_breakdown).sort((z,K)=>z[0].localeCompare(K[0]));if(Q.length>0)X.push(` Model breakdown: ${Q.map(([z,K])=>`${z}=${K}`).join(", ")}`);let Y=Object.entries(Z.efficiency.phase_breakdown).sort((z,K)=>z[0].localeCompare(K[0]));if(Y.length>0)X.push(` Phase breakdown: ${Y.map(([z,K])=>`${z}=${K}`).join(", ")}`);let J=Object.entries(Z.efficiency.status_breakdown).sort((z,K)=>z[0].localeCompare(K[0]));if(J.length>0)X.push(` Status breakdown: ${J.map(([z,K])=>`${z}=${K}`).join(", ")}`);if(X.push(""),X.push("Accuracy"),X.push(` Council rounds: ${Z.accuracy.council_rounds}`),X.push(` Unanimous rate: ${Z.accuracy.unanimous_rate??"n/a"}`),X.push(` Approval rate: ${Z.accuracy.approval_rate??"n/a"}`),X.push(` Iter success rate: ${Z.accuracy.iteration_success_rate??"n/a"}`),Z.notes.length>0){X.push(""),X.push("Notes");for(let z of Z.notes)X.push(` - ${z}`)}return X.push(""),X.push("See also: loki trust (trust trajectory across runs)"),X.join(`
@@ -1220,4 +1220,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1220
1220
  `),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (S_(),x_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
1221
1221
  `),process.stderr.write(y_),2}}dO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var yW0=await SW0(Bun.argv.slice(2));process.exit(yW0);
1222
1222
 
1223
- //# debugId=62106D5EFF46A15964756E2164756E21
1223
+ //# debugId=CC7FBAFAA3CFFD5964756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.8.0'
78
+ __version__ = '8.8.1'
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.8.0",
4
+ "version": "8.8.1",
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.8.0",
5
+ "version": "8.8.1",
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",
@@ -132,9 +132,27 @@ _codex_validate_model() {
132
132
  # fallback is validated, since it may carry Claude aliases (opus/sonnet/haiku)
133
133
  # that are invalid for Codex. Validating the whole chain silently downgraded a
134
134
  # trusted LOKI_CODEX_MODEL to the default (BUG-PROV-003 fix).
135
- PROVIDER_MODEL_PLANNING="${LOKI_CODEX_MODEL:-$(_codex_validate_model "${LOKI_MODEL_PLANNING:-$CODEX_DEFAULT_MODEL}")}"
136
- PROVIDER_MODEL_DEVELOPMENT="${LOKI_CODEX_MODEL:-$(_codex_validate_model "${LOKI_MODEL_DEVELOPMENT:-$CODEX_DEFAULT_MODEL}")}"
137
- PROVIDER_MODEL_FAST="${LOKI_CODEX_MODEL:-$(_codex_validate_model "${LOKI_MODEL_FAST:-$CODEX_DEFAULT_MODEL}")}"
135
+ #
136
+ # Each tier resolves from the catalog, which is the single source of truth for
137
+ # what "small / medium / high" means per provider. Before this, all three tiers
138
+ # fell back to CODEX_DEFAULT_MODEL (empty), so a codex run sent no --model at
139
+ # all and `LOKI_SESSION_MODEL=high` selected nothing -- the tier vocabulary was
140
+ # inert on codex and a user had to name a model by hand.
141
+ #
142
+ # The empty default is still the last resort: if the catalog lookup yields
143
+ # nothing, we send no --model and let Codex pick, which is the safe outcome for
144
+ # a ChatGPT-account user rather than a guessed name.
145
+ _codex_tier_model() {
146
+ local tier="$1" resolved=""
147
+ if command -v loki_latest_model >/dev/null 2>&1; then
148
+ resolved="$(loki_latest_model codex "$tier" 2>/dev/null)"
149
+ fi
150
+ printf '%s' "${resolved:-$CODEX_DEFAULT_MODEL}"
151
+ }
152
+
153
+ PROVIDER_MODEL_PLANNING="${LOKI_CODEX_MODEL:-$(_codex_validate_model "${LOKI_MODEL_PLANNING:-$(_codex_tier_model planning)}")}"
154
+ PROVIDER_MODEL_DEVELOPMENT="${LOKI_CODEX_MODEL:-$(_codex_validate_model "${LOKI_MODEL_DEVELOPMENT:-$(_codex_tier_model development)}")}"
155
+ PROVIDER_MODEL_FAST="${LOKI_CODEX_MODEL:-$(_codex_validate_model "${LOKI_MODEL_FAST:-$(_codex_tier_model fast)}")}"
138
156
 
139
157
  # Effort levels (Codex-specific: maps to reasoning time, not model capability)
140
158
  PROVIDER_EFFORT_PLANNING="xhigh"
@@ -49,17 +49,29 @@
49
49
  ]
50
50
  },
51
51
  "codex": {
52
- "latest_planning": "gpt-5.3-codex",
53
- "latest_development": "gpt-5.3-codex",
54
- "latest_fast": "gpt-5.3-codex",
55
- "tier_fallback": {
56
- "development": "planning",
57
- "fast": "planning"
58
- },
52
+ "latest_planning": "gpt-5.6-sol",
53
+ "latest_development": "gpt-5.6-terra",
54
+ "latest_fast": "gpt-5.6-luna",
59
55
  "models": [
56
+ {
57
+ "id": "gpt-5.6-sol",
58
+ "tier": "planning",
59
+ "notes": "high tier. OpenAI: 'Frontier model for complex professional work'. Verified against developers.openai.com/api/docs/models."
60
+ },
61
+ {
62
+ "id": "gpt-5.6-terra",
63
+ "tier": "development",
64
+ "notes": "medium tier, the DEFAULT. OpenAI: balances performance with affordability."
65
+ },
66
+ {
67
+ "id": "gpt-5.6-luna",
68
+ "tier": "fast",
69
+ "notes": "small tier. OpenAI: budget-conscious, high-volume."
70
+ },
60
71
  {
61
72
  "id": "gpt-5.3-codex",
62
- "tier": "planning"
73
+ "tier": "legacy",
74
+ "notes": "Prior default, superseded by the gpt-5.6 line. Kept discoverable, not dispatched."
63
75
  },
64
76
  {
65
77
  "id": "o3",