loki-mode 9.25.2 → 9.26.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 v9.25.2
6
+ # Loki Mode v9.26.1
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v9.25.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.26.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.25.2
1
+ 9.26.1
@@ -838,6 +838,83 @@ loki_config_generate_schema() {
838
838
  # refs, raw-secret literals (ERROR), and per-value validation failures. Returns
839
839
  # non-zero on ANY failure. Reads the file directly (format-aware) WITHOUT
840
840
  # exporting anything into the environment.
841
+ # Walk a JSON/YAML config's OWN key set and echo any dotted key that is not a
842
+ # member of LOKI_CONFIG_MAP, one per line. Used by validate only.
843
+ #
844
+ # Inert descriptive fields written by `loki init` (version/template/created) are
845
+ # allowlisted: they carry no behavior, so erroring on them would fail a file the
846
+ # product itself generated. Every other unmapped key is reported -- those are the
847
+ # ones a user expects to change behavior and that are silently dropped instead.
848
+ #
849
+ # Container keys are not reported: in {"dashboard":{"port":1}} the key
850
+ # "dashboard" is a parent of the mapped "dashboard.port", never a typo itself.
851
+ # A leaf is what a user actually mistypes.
852
+ loki_config_unknown_keys() {
853
+ local file="$1" fmt="$2"
854
+ command -v python3 >/dev/null 2>&1 || return 0
855
+
856
+ local map_str="" mapping
857
+ for mapping in "${LOKI_CONFIG_MAP[@]}"; do map_str+="${mapping%%:*}"$'\n'; done
858
+
859
+ _LOKI_UK_FILE="$file" _LOKI_UK_FMT="$fmt" _LOKI_UK_MAP="$map_str" python3 -c '
860
+ import json, os, sys
861
+
862
+ path = os.environ["_LOKI_UK_FILE"]
863
+ fmt = os.environ["_LOKI_UK_FMT"]
864
+
865
+ known = set()
866
+ for line in os.environ.get("_LOKI_UK_MAP", "").splitlines():
867
+ line = line.strip()
868
+ if line:
869
+ known.add(line)
870
+
871
+ # Parents of a mapped key are containers, not typos.
872
+ containers = set()
873
+ for k in known:
874
+ parts = k.split(".")
875
+ for i in range(1, len(parts)):
876
+ containers.add(".".join(parts[:i]))
877
+
878
+ # Inert metadata emitted by `loki init` -- descriptive, never behavioral.
879
+ ALLOW = {"version", "template", "created", "name", "description"}
880
+
881
+ try:
882
+ if fmt == "json":
883
+ with open(path) as f:
884
+ data = json.load(f)
885
+ else:
886
+ try:
887
+ import yaml
888
+ except ImportError:
889
+ sys.exit(0)
890
+ with open(path) as f:
891
+ data = yaml.safe_load(f)
892
+ except Exception:
893
+ # A malformed file is out of scope here -- the parsers report it.
894
+ sys.exit(0)
895
+
896
+ if not isinstance(data, dict):
897
+ sys.exit(0)
898
+
899
+ unknown = []
900
+
901
+ def walk(node, prefix):
902
+ for key, val in node.items():
903
+ dotted = prefix + key if not prefix else prefix + "." + key
904
+ if isinstance(val, dict) and val:
905
+ # Recurse into containers; report the leaves inside them.
906
+ walk(val, dotted)
907
+ continue
908
+ if dotted in known or dotted in containers or dotted in ALLOW:
909
+ continue
910
+ unknown.append(dotted)
911
+
912
+ walk(data, "")
913
+ for u in unknown:
914
+ print(u)
915
+ ' 2>/dev/null || return 0
916
+ }
917
+
841
918
  loki_config_validate_file() {
842
919
  local path="$1"
843
920
  local rc=0
@@ -907,6 +984,32 @@ loki_config_validate_file() {
907
984
  _loki_cfg_collect_pairs "$path" "$fmt"
908
985
  )"
909
986
 
987
+ # Unknown-key detection for JSON/YAML.
988
+ #
989
+ # The extraction above walks LOKI_CONFIG_MAP and pulls each KNOWN path out of
990
+ # the file, so a key the map does not contain is never emitted and cannot
991
+ # reach the pair loop below -- a misspelled key validated clean while the
992
+ # same typo in .env format was correctly rejected. Detection therefore has to
993
+ # walk the FILE's own key set and diff it against the map, which is what this
994
+ # block does. Kept in validate only: the load/emit paths are unchanged, so a
995
+ # config that runs today still runs.
996
+ case "$fmt" in
997
+ (json|yaml)
998
+ local unknown_keys
999
+ unknown_keys="$(loki_config_unknown_keys "$path" "$fmt")" || unknown_keys=""
1000
+ if [ -n "$unknown_keys" ]; then
1001
+ local ukey
1002
+ while IFS= read -r ukey; do
1003
+ [ -n "$ukey" ] || continue
1004
+ printf 'loki: config validate: ERROR unknown key %s (not a recognized config key -- typo?)\n' "$ukey" >&2
1005
+ rc=1
1006
+ done <<UNKNOWN_KEYS
1007
+ $unknown_keys
1008
+ UNKNOWN_KEYS
1009
+ fi
1010
+ ;;
1011
+ esac
1012
+
910
1013
  local env_var value expanded
911
1014
  while IFS=$'\t' read -r env_var value; do
912
1015
  [ -n "$env_var" ] || continue
@@ -69,6 +69,151 @@ VERIFY_EXIT_ERROR=3
69
69
  VERIFY_SCHEMA_VERSION="1.0"
70
70
 
71
71
  # Resolve tool version from the VERSION file shipped alongside the repo.
72
+ # ---------------------------------------------------------------------------
73
+ # LLM review stage (v9.26.0). Phase 2 of the spec; the deterministic MVP shipped
74
+ # without it and the evidence document said so honestly.
75
+ #
76
+ # WHY IT LIVES HERE AND NOT IN THE COUNCIL. This module deliberately does not
77
+ # source completion-council.sh (see the header): those functions are welded to
78
+ # the iteration loop's globals and diff base. Instead this calls the same
79
+ # raw-SDK bridge the council uses under the hood -- `loki internal sdk-judge`,
80
+ # a pure-HTTPS judge that prints JSON on exit 0 and NOTHING on any failure.
81
+ #
82
+ # FAIL-CLOSED, NEVER FAIL-QUIET. Every failure path records an honest status
83
+ # (unavailable, with the reason) rather than a pass. A verifier that reports
84
+ # "reviewed, no issues" when the reviewer never ran is worse than one that
85
+ # never had a reviewer.
86
+ #
87
+ # ADVISORY IN THIS RELEASE. The verdict and exit code are computed exactly as
88
+ # before; this only adds a section to the evidence document. `loki verify`
89
+ # exits 0/1/2 on the same conditions it always did, so a CI job gating on exit
90
+ # 0 sees no behavior change. Verdict influence is a separate, later flag.
91
+ _verify_llm_review() {
92
+ local out_dir="$1" merge_base="$2" head_sha="$3"
93
+
94
+ if [ "${VERIFY_NO_LLM:-0}" = "1" ]; then
95
+ printf '%s\t%s\t%s\t%s\t%s\n' "skipped" "--no-llm requested" "" "0" ""
96
+ return 0
97
+ fi
98
+
99
+ local loki_bin
100
+ loki_bin="$(command -v loki 2>/dev/null || true)"
101
+ if [ -z "$loki_bin" ]; then
102
+ printf '%s\t%s\t%s\t%s\t%s\n' "unavailable" "the loki CLI is not on PATH, so the SDK judge bridge cannot be reached" "" "0" ""
103
+ return 0
104
+ fi
105
+
106
+ # Bound the diff. A judge prompt is an input cost and an unbounded diff is
107
+ # both expensive and useless -- past some size the model cannot reason about
108
+ # it anyway. The cap is explicit in the reason string when it bites, so a
109
+ # truncated review is never silently presented as a whole-diff review.
110
+ local diff_cap="${LOKI_VERIFY_LLM_DIFF_BYTES:-200000}"
111
+ local diff_file; diff_file="$(mktemp "${TMPDIR:-/tmp}/loki-verify-llm-diff.XXXXXX")" || {
112
+ printf '%s\t%s\t%s\t%s\t%s\n' "unavailable" "could not create a temp file for the diff" "" "0" ""
113
+ return 0
114
+ }
115
+ git diff --function-context "${merge_base}..${head_sha}" > "$diff_file" 2>/dev/null || true
116
+ local diff_bytes; diff_bytes=$(wc -c < "$diff_file" 2>/dev/null | tr -d ' ')
117
+ diff_bytes="${diff_bytes:-0}"
118
+ local truncated=""
119
+ if [ "$diff_bytes" -gt "$diff_cap" ]; then
120
+ head -c "$diff_cap" "$diff_file" > "${diff_file}.cut" 2>/dev/null && mv -f "${diff_file}.cut" "$diff_file"
121
+ truncated=" (diff truncated to ${diff_cap} bytes of ${diff_bytes})"
122
+ fi
123
+ if [ "$diff_bytes" -eq 0 ]; then
124
+ rm -f "$diff_file" 2>/dev/null || true
125
+ printf '%s\t%s\t%s\t%s\t%s\n' "skipped" "no diff to review between the merge base and HEAD" "" "0" ""
126
+ return 0
127
+ fi
128
+
129
+ local pf sf
130
+ pf="$(mktemp "${TMPDIR:-/tmp}/loki-verify-llm-prompt.XXXXXX")" || { rm -f "$diff_file"; return 0; }
131
+ sf="$(mktemp "${TMPDIR:-/tmp}/loki-verify-llm-schema.XXXXXX")" || { rm -f "$diff_file" "$pf"; return 0; }
132
+
133
+ # Context first: the diff is the bulk of the prompt and identical across any
134
+ # retry, so leading with it keeps the cacheable prefix stable.
135
+ {
136
+ printf 'Review this diff for correctness defects only.\n\n'
137
+ cat "$diff_file"
138
+ printf '\n\nYou are a code reviewer on a verification service. Report ONLY defects you can point at in this diff:\n'
139
+ printf -- '- a bug that produces a wrong result or a crash, with the input that triggers it\n'
140
+ printf -- '- a security hole reachable from untrusted input\n'
141
+ printf -- '- data loss or corruption\n\n'
142
+ printf 'Do NOT report style, naming, formatting, test coverage, or speculative refactors.\n'
143
+ printf 'If the diff has no such defect, return an empty findings array. An empty result is a\n'
144
+ printf 'legitimate and common answer; do not invent a finding to appear useful.\n'
145
+ } > "$pf"
146
+
147
+ cat > "$sf" <<'SCHEMA'
148
+ {
149
+ "type": "object",
150
+ "properties": {
151
+ "summary": { "type": "string" },
152
+ "findings": {
153
+ "type": "array",
154
+ "items": {
155
+ "type": "object",
156
+ "properties": {
157
+ "severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] },
158
+ "file": { "type": "string" },
159
+ "message": { "type": "string" },
160
+ "why_it_breaks": { "type": "string" }
161
+ },
162
+ "required": ["severity", "message", "why_it_breaks"]
163
+ }
164
+ }
165
+ },
166
+ "required": ["summary", "findings"]
167
+ }
168
+ SCHEMA
169
+
170
+ model="${LOKI_VERIFY_LLM_MODEL:-claude-sonnet-5}"
171
+ local to_s="${LOKI_VERIFY_LLM_TIMEOUT_S:-120}"
172
+ local wrap=""
173
+ if command -v timeout >/dev/null 2>&1; then wrap="timeout $(( to_s + 15 ))"
174
+ elif command -v gtimeout >/dev/null 2>&1; then wrap="gtimeout $(( to_s + 15 ))"; fi
175
+
176
+ local out rc=0
177
+ out="$($wrap "$loki_bin" internal sdk-judge \
178
+ --prompt-file "$pf" --schema-file "$sf" \
179
+ --model "$model" --effort high \
180
+ --timeout-ms "$(( to_s * 1000 ))" 2>/dev/null)" || rc=$?
181
+ rm -f "$diff_file" "$pf" "$sf" 2>/dev/null || true
182
+
183
+ if [ "$rc" -ne 0 ] || [ -z "$out" ]; then
184
+ printf '%s\t%s\t%s\t%s\t%s\n' "unavailable" "the SDK judge returned no result (no API key, transport failure, or timeout)${truncated}" "" "0" "$model"
185
+ return 0
186
+ fi
187
+
188
+ # Parse defensively: a malformed payload is "unavailable", never a pass.
189
+ local parsed
190
+ parsed="$(printf '%s' "$out" | python3 -c '
191
+ import json, sys
192
+ try:
193
+ d = json.load(sys.stdin)
194
+ fs = d.get("findings") or []
195
+ if not isinstance(fs, list):
196
+ raise ValueError("findings is not a list")
197
+ print("%d\t%s" % (len(fs), (d.get("summary") or "").replace("\t", " ").replace("\n", " ")[:300]))
198
+ except Exception as exc:
199
+ print("ERR\t%s" % type(exc).__name__)
200
+ ' 2>/dev/null)" || parsed="ERR\tparse"
201
+
202
+ case "$parsed" in
203
+ ERR*)
204
+ printf '%s\t%s\t%s\t%s\t%s\n' "unavailable" "the SDK judge returned a payload that did not parse${truncated}" "" "0" "$model" ;;
205
+ *)
206
+ findings_n="${parsed%%\t*}"
207
+ summary="${parsed#*\t}"
208
+ printf '%s\t%s\t%s\t%s\t%s\n' "reviewed" "" "$summary" "$findings_n" "$model"
209
+ # Keep the raw payload beside the evidence document so a reader can
210
+ # check the review rather than take the summary on faith.
211
+ printf '%s' "$out" > "${out_dir}/llm-review.json" 2>/dev/null || true
212
+ ;;
213
+ esac
214
+ return 0
215
+ }
216
+
72
217
  _verify_tool_version() {
73
218
  local here
74
219
  here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -1917,6 +2062,23 @@ verify_emit_evidence() {
1917
2062
  repo_name="$(git config --get remote.origin.url 2>/dev/null | sed -E 's#.*[:/]([^/]+/[^/]+)(\.git)?$#\1#' || echo "local")"
1918
2063
  [ -z "$repo_name" ] && repo_name="local"
1919
2064
 
2065
+ # LLM review (advisory this release: it does not change VERIFY_VERDICT or
2066
+ # VERIFY_EXIT, both already computed above).
2067
+ local _llm_line _llm_status _llm_reason _llm_summary _llm_n _llm_model
2068
+ _llm_line="$(_verify_llm_review "$out_dir" "${VERIFY_MERGE_BASE:-}" "${VERIFY_HEAD_SHA:-HEAD}" 2>/dev/null || true)"
2069
+ _llm_status="$(printf '%s' "$_llm_line" | cut -f1)"
2070
+ _llm_reason="$(printf '%s' "$_llm_line" | cut -f2)"
2071
+ _llm_summary="$(printf '%s' "$_llm_line" | cut -f3)"
2072
+ _llm_n="$(printf '%s' "$_llm_line" | cut -f4)"
2073
+ _llm_model="$(printf '%s' "$_llm_line" | cut -f5)"
2074
+ [ -n "$_llm_status" ] || _llm_status="unavailable"
2075
+ [ -n "$_llm_n" ] || _llm_n=0
2076
+
2077
+ _V_LLM_STATUS="$_llm_status" \
2078
+ _V_LLM_REASON="$_llm_reason" \
2079
+ _V_LLM_SUMMARY="$_llm_summary" \
2080
+ _V_LLM_N="$_llm_n" \
2081
+ _V_LLM_MODEL="$_llm_model" \
1920
2082
  _VERIFY_OUT_DIR="$out_dir" \
1921
2083
  _VERIFY_FINDINGS="$_VERIFY_FINDINGS_FILE" \
1922
2084
  _VERIFY_GATES="$_VERIFY_GATES_FILE" \
@@ -2019,9 +2181,21 @@ doc = {
2019
2181
  },
2020
2182
  "deterministic_gates": gates,
2021
2183
  "llm_review": {
2022
- "status": "skipped",
2023
- "reason": "deterministic-only MVP (30-day cut); single-reviewer LLM stage and blind council are deferred to Phase 2",
2184
+ # status is one of: reviewed | skipped | unavailable.
2185
+ # "unavailable" is deliberately NOT "skipped": a reviewer that could not
2186
+ # run is a different fact from one that was not asked to, and collapsing
2187
+ # them would let a broken key read as a clean pass.
2188
+ "status": os.environ.get("_V_LLM_STATUS", "unavailable"),
2189
+ "reason": os.environ.get("_V_LLM_REASON", "") or None,
2190
+ "summary": os.environ.get("_V_LLM_SUMMARY", "") or None,
2191
+ "finding_count": int(os.environ.get("_V_LLM_N", "0") or 0),
2192
+ "model": os.environ.get("_V_LLM_MODEL", "") or None,
2193
+ # An LLM review is not reproducible the way a runner exit code is, and
2194
+ # the document says so rather than implying determinism it lacks.
2024
2195
  "reproducible": False,
2196
+ # Advisory in this release: recorded, but never folded into the verdict
2197
+ # or the exit code. Promoting it is a separate, flagged change.
2198
+ "affects_verdict": False,
2025
2199
  },
2026
2200
  "findings": findings,
2027
2201
  "suppressed": [],
@@ -2102,7 +2276,15 @@ lines.append("# Autonomi Verify report")
2102
2276
  lines.append("")
2103
2277
  lines.append("Verdict: **%s** (exit %d)" % (doc["verdict"], doc["exit_code"]))
2104
2278
  lines.append("")
2105
- lines.append("Tool: loki verify %s | deterministic-only MVP (no LLM review)" % doc["produced_by"]["tool_version"])
2279
+ _llm = doc.get("llm_review") or {}
2280
+ _llm_status = _llm.get("status", "unavailable")
2281
+ if _llm_status == "reviewed":
2282
+ _llm_note = "LLM review: %d finding(s)" % _llm.get("finding_count", 0)
2283
+ elif _llm_status == "skipped":
2284
+ _llm_note = "LLM review: skipped"
2285
+ else:
2286
+ _llm_note = "LLM review: unavailable"
2287
+ lines.append("Tool: loki verify %s | %s" % (doc["produced_by"]["tool_version"], _llm_note))
2106
2288
  lines.append("")
2107
2289
  s = doc["subject"]
2108
2290
  lines.append("## Subject")
@@ -2140,7 +2322,10 @@ else:
2140
2322
  lines.append("")
2141
2323
  lines.append("## LLM review")
2142
2324
  lines.append("")
2143
- lines.append("Skipped: %s" % doc["llm_review"]["reason"])
2325
+ if _llm.get("reason"):
2326
+ lines.append("LLM review not run: %s" % _llm["reason"])
2327
+ elif _llm_status == "reviewed" and _llm.get("summary"):
2328
+ lines.append("LLM review: %s" % _llm["summary"])
2144
2329
  lines.append("")
2145
2330
  lines.append("Evidence JSON: %s" % ev_path)
2146
2331
  lines.append("")
@@ -2183,7 +2368,9 @@ OPTIONS:
2183
2368
  --block-on <list> Comma list of severities that BLOCK.
2184
2369
  Default: critical,high (one notch looser than the
2185
2370
  Loki build loop, which also blocks on medium).
2186
- --no-llm Accepted for forward-compat; LLM is already off in MVP.
2371
+ --no-llm Skip the LLM review stage. The deterministic gates and the
2372
+ verdict are unchanged either way -- the review is advisory
2373
+ in this release and never alters the exit code.
2187
2374
  --json Emit the evidence document to stdout so it can be piped
2188
2375
  (`loki verify --json | jq .verdict`). The same document
2189
2376
  is still written to <out>/evidence.json. The human
@@ -2793,6 +2980,9 @@ verify_main() {
2793
2980
  --block-on)
2794
2981
  block_on="$(printf '%s' "${2:-}" | tr '[:upper:]' '[:lower:]')"; shift 2 ;;
2795
2982
  --no-llm)
2983
+ # Was a no-op accepted "for forward-compat" while no LLM stage
2984
+ # existed. Now it does what its name always implied.
2985
+ VERIFY_NO_LLM=1
2796
2986
  shift ;;
2797
2987
  --json)
2798
2988
  # Emit the evidence document to STDOUT so a caller can pipe it.
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.25.2"
10
+ __version__ = "9.26.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v9.25.2
5
+ **Version:** v9.26.1
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var St=Object.create;var{getPrototypeOf:yt,defineProperty:jG,getOwnPropertyNames:bt}=Object;var ft=Object.prototype.hasOwnProperty;function _t($){return this[$]}var vt,ht,gt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?vt??=new WeakMap:ht??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?St(yt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of bt($))if(!ft.call(J,q))jG(J,q,{get:_t.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var mt=($)=>$;function ut($,X){this[$]=mt.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:ut.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var YR={};B1(YR,{lokiDir:()=>h0,homeLokiDir:()=>HQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as dt}from"url";import{existsSync as Zq}from"fs";import{homedir as pt}from"os";function ct(){let $=VR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(VR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function HQ(){return h2(pt(),".loki")}var VR,L1;var k1=s(()=>{VR=LG(dt(import.meta.url));L1=ct()});import{readFileSync as lt}from"fs";import{resolve as it,dirname as at}from"path";import{fileURLToPath as ot}from"url";function j9(){if(n3!==null)return n3;let $="9.25.2";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=at(ot(import.meta.url)),Q=AG(X);n3=lt(it(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var GR={};B1(GR,{runOrThrow:()=>Ue,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>We,commandExists:()=>g5,ShellError:()=>CG,MAX_STDOUT_BYTES:()=>WR});async function Jq($,X=WR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Ue($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new CG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=He($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function He($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function We($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var WR=16777216,CG;var y8=s(()=>{CG=class CG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Ge?"":$}var Ge,p0,$5,q1,L61,A1,f1,m5,r;var t7=s(()=>{Ge=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),L61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as De}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(De($))return GQ=$,$;let X=await g5("python3.12");if(X)return GQ=X,X;let Q=await g5("python3");return GQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var GQ;var m2=s(()=>{y8()});var yR={};B1(yR,{runStatus:()=>oe});import{existsSync as u5,readFileSync as A9,readdirSync as RR,statSync as IR}from"fs";import{resolve as A5,basename as ge}from"path";import{homedir as me}from"os";function wR($){let X=Math.trunc($);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 PR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=wR($),Y=wR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function de(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
2
+ var St=Object.create;var{getPrototypeOf:yt,defineProperty:jG,getOwnPropertyNames:bt}=Object;var ft=Object.prototype.hasOwnProperty;function _t($){return this[$]}var vt,ht,gt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?vt??=new WeakMap:ht??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?St(yt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of bt($))if(!ft.call(J,q))jG(J,q,{get:_t.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var mt=($)=>$;function ut($,X){this[$]=mt.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:ut.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var YR={};B1(YR,{lokiDir:()=>h0,homeLokiDir:()=>HQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as dt}from"url";import{existsSync as Zq}from"fs";import{homedir as pt}from"os";function ct(){let $=VR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(VR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function HQ(){return h2(pt(),".loki")}var VR,L1;var k1=s(()=>{VR=LG(dt(import.meta.url));L1=ct()});import{readFileSync as lt}from"fs";import{resolve as it,dirname as at}from"path";import{fileURLToPath as ot}from"url";function j9(){if(n3!==null)return n3;let $="9.26.1";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=at(ot(import.meta.url)),Q=AG(X);n3=lt(it(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var GR={};B1(GR,{runOrThrow:()=>Ue,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>We,commandExists:()=>g5,ShellError:()=>CG,MAX_STDOUT_BYTES:()=>WR});async function Jq($,X=WR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Ue($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new CG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=He($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function He($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function We($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var WR=16777216,CG;var y8=s(()=>{CG=class CG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Ge?"":$}var Ge,p0,$5,q1,L61,A1,f1,m5,r;var t7=s(()=>{Ge=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),L61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as De}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(De($))return GQ=$,$;let X=await g5("python3.12");if(X)return GQ=X,X;let Q=await g5("python3");return GQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var GQ;var m2=s(()=>{y8()});var yR={};B1(yR,{runStatus:()=>oe});import{existsSync as u5,readFileSync as A9,readdirSync as RR,statSync as IR}from"fs";import{resolve as A5,basename as ge}from"path";import{homedir as me}from"os";function wR($){let X=Math.trunc($);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 PR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=wR($),Y=wR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function de(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
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)
@@ -1334,4 +1334,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1334
1334
  `),2}case"start":{let{runStart:z}=await Promise.resolve().then(() => (Et(),Pt));return z(Q)}default:return process.stderr.write(`Unknown command: ${X}
1335
1335
  `),process.stderr.write(xt),2}}DR();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var X61=await $61(Bun.argv.slice(2));process.exit(X61);
1336
1336
 
1337
- //# debugId=D041EDC92463BFEDF7B5C026608DA371
1337
+ //# debugId=B98225BA72BA8822593D774679C4FF5C
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.25.2'
78
+ __version__ = '9.26.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": "9.25.2",
4
+ "version": "9.26.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, opencode).",
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": "9.25.2",
5
+ "version": "9.26.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",