loki-mode 9.37.0 → 9.39.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 v9.37.0
6
+ # Loki Mode v9.39.0
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.37.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.39.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.37.0
1
+ 9.39.0
@@ -127,6 +127,7 @@ def collect_efficiency(loki_dir):
127
127
  }
128
128
  model = ""
129
129
  collected = False
130
+ _records = []
130
131
  eff_dir = os.path.join(loki_dir, "metrics", "efficiency")
131
132
  try:
132
133
  names = sorted(os.listdir(eff_dir))
@@ -139,6 +140,7 @@ def collect_efficiency(loki_dir):
139
140
  if not isinstance(rec, dict):
140
141
  continue
141
142
  collected = True
143
+ _records.append(rec)
142
144
  cost["usd"] += _to_float(rec.get("cost_usd"))
143
145
  cost["input_tokens"] += _to_int(rec.get("input_tokens"))
144
146
  cost["output_tokens"] += _to_int(rec.get("output_tokens"))
@@ -171,11 +173,41 @@ def collect_efficiency(loki_dir):
171
173
  "cache_read_tokens": cost["cache_read_tokens"],
172
174
  "cache_creation_tokens": cost["cache_creation_tokens"],
173
175
  })
176
+ # COST AND TOKENS ARE MEASURED SEPARATELY, so they must be reported
177
+ # separately. `_observed` is true when EITHER a cost or a token count was
178
+ # seen, which is right for "did we measure anything" -- but it let a run with
179
+ # real token counts and NO priced record report usd=0.0 available=True. The
180
+ # receipt then asserts the run cost nothing while its own token counts prove
181
+ # work happened, which is a fabricated fact of exactly the kind this module
182
+ # exists to prevent. Measured, before this fix:
183
+ # [{"input_tokens":100,"output_tokens":200}] -> usd=0.0 available=True
184
+ # A partially-priced run has the same shape: an iteration missing cost_usd
185
+ # contributes 0.0 to the sum and silently understates total spend.
186
+ #
187
+ # So usd is now null unless at least one record actually carried a cost, and
188
+ # cost_partial marks the case where some did and some did not. Tokens are
189
+ # unaffected: they keep reporting whatever was observed.
190
+ # PRESENCE of the key, not its value. An explicit {"cost_usd": 0} is a
191
+ # GENUINE measured zero and must stay 0.0 -- a free cache-hit iteration is a
192
+ # real observation, and nulling it would be its own dishonesty (existing
193
+ # contract: test_genuine_zero_cost_stays_zero_not_null). What is unknown is a
194
+ # record that never carried the field at all.
195
+ _any_cost = any("cost_usd" in r for r in _records)
196
+ _missing_cost = sum(1 for r in _records if "cost_usd" not in r)
174
197
  if collected and _observed:
175
198
  # Round usd to a sane precision but keep it precise (anti-pattern:
176
199
  # round suspiciously-clean numbers). 4 decimals preserves odd values.
177
200
  cost["usd"] = round(cost["usd"], 4)
178
201
  cost["available"] = True
202
+ if not _any_cost:
203
+ # Tokens were observed but nothing was priced: unknown, not zero.
204
+ cost["usd"] = None
205
+ cost["cost_available"] = False
206
+ else:
207
+ cost["cost_available"] = True
208
+ # Some records priced, others not: the total is a LOWER BOUND.
209
+ cost["cost_partial"] = _missing_cost > 0
210
+ cost["cost_unpriced_records"] = _missing_cost
179
211
  else:
180
212
  # No record means unavailable, never an observed zero.
181
213
  for key in (
@@ -173,9 +173,21 @@ def _collect_council(loki_dir):
173
173
  ) + (" [round %s]" % round_tag if round_tag else ""),
174
174
  })
175
175
  continue
176
+ # Skip non-reviewer records. The nested path above already refuses a row
177
+ # with a blank role AND blank vote ("noise, not a reviewer"); this flat
178
+ # path did not, so any council/*.json that is not a verdict file was
179
+ # rendered as a phantom reviewer. Observed on a real run: the glob picked
180
+ # up `evidence-gate-details.json` and `state.json`, inflating a genuine
181
+ # 3-voter council to a larger roster of mostly-empty rows. The receipt's
182
+ # council block is the central trust signal, so a padded roster
183
+ # overstates how much independent review actually happened.
184
+ _flat_role = str(rec.get("role") or rec.get("reviewer") or "")
185
+ _flat_vote = str(rec.get("vote") or rec.get("decision") or "")
186
+ if not _flat_role and not _flat_vote:
187
+ continue
176
188
  reviewers.append({
177
- "role": str(rec.get("role") or rec.get("reviewer") or ""),
178
- "vote": str(rec.get("vote") or rec.get("decision") or ""),
189
+ "role": _flat_role,
190
+ "vote": _flat_vote,
179
191
  # Full text here; truncation to <=300 happens AFTER redaction so a
180
192
  # secret straddling the cap cannot be sliced into a sub-pattern
181
193
  # fragment that escapes the redactor.
@@ -1025,7 +1037,25 @@ def _empty_tree_sha(repo_dir):
1025
1037
 
1026
1038
  Diffing against this yields "everything that currently exists", which is the
1027
1039
  truthful baseline for a run that started from a repo with no commits.
1040
+
1041
+ REQUIRES AN ACTUAL REPOSITORY. `git hash-object -t tree /dev/null` is a pure
1042
+ hash computation: it succeeds OUTSIDE a repo too, returning the same
1043
+ constant 4b825dc6... So without this guard a run in a non-git directory got
1044
+ a well-formed, plausible-looking base_sha implying a real baseline had been
1045
+ captured, when no repository existed at all -- and every other git fact in
1046
+ the receipt (head_sha, tree_sha256, diff) was simultaneously empty. A
1047
+ fabricated-looking identifier beside empty siblings is worse than an honest
1048
+ empty string, because a reader checks the SHA and finds it valid.
1028
1049
  """
1050
+ try:
1051
+ inside = subprocess.run(
1052
+ ["git", "rev-parse", "--is-inside-work-tree"],
1053
+ cwd=repo_dir, capture_output=True, text=True, timeout=10,
1054
+ )
1055
+ if inside.returncode != 0:
1056
+ return ""
1057
+ except Exception:
1058
+ return ""
1029
1059
  try:
1030
1060
  out = subprocess.run(
1031
1061
  ["git", "hash-object", "-t", "tree", os.devnull],
package/autonomy/loki CHANGED
@@ -24582,6 +24582,20 @@ except Exception as e:
24582
24582
  fi
24583
24583
  local _settings="$HOME/.claude/settings.json"
24584
24584
  local _hook_script="${SKILL_DIR:-$(pwd)}/claude/hooks/loki-session-end.sh"
24585
+ # FAIL CLOSED on a missing script. This path was built unguarded and
24586
+ # written into the user's REAL ~/.claude/settings.json, then reported
24587
+ # green "installed" -- while claude/ was absent from package.json
24588
+ # files[], so for every npm user the hook pointed at a file that did
24589
+ # not exist. The install claimed success and transcripts were never
24590
+ # ingested; the SessionEnd failure surfaces nowhere the user reads.
24591
+ # Writing a settings entry we cannot honor is worse than not writing
24592
+ # one, so refuse and say why instead of mutating their config.
24593
+ if [ ! -f "$_hook_script" ]; then
24594
+ echo -e "${RED}Cannot install the SessionEnd hook: script not found${NC}" >&2
24595
+ echo -e "${YELLOW} expected: $_hook_script${NC}" >&2
24596
+ echo -e "${YELLOW} ~/.claude/settings.json was NOT modified.${NC}" >&2
24597
+ return 1
24598
+ fi
24585
24599
  mkdir -p "$(dirname "$_settings")" 2>/dev/null || true
24586
24600
  [ ! -f "$_settings" ] && echo '{}' > "$_settings"
24587
24601
  _LOKI_HOOK_SCRIPT="$_hook_script" _LOKI_SETTINGS="$_settings" python3 -c "
@@ -29440,6 +29454,14 @@ USER TASK: ${prompt}"
29440
29454
  aider)
29441
29455
  aider --message "$full_prompt" --yes-always --no-auto-commits < /dev/null 2>&1 || agent_exit=$?
29442
29456
  ;;
29457
+ opencode)
29458
+ # Mirrors the working arm in the phase dispatcher (see the
29459
+ # opencode case alongside aider there). opencode was listed
29460
+ # as an active provider and accepted by `loki provider set`,
29461
+ # but this case had no arm, so `loki agent run` printed the
29462
+ # persona banner and then died with "Unknown provider".
29463
+ (source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$full_prompt" 2>&1) || agent_exit=$?
29464
+ ;;
29443
29465
  *)
29444
29466
  echo -e "${RED}Unknown provider: $provider${NC}"
29445
29467
  return 1
@@ -29565,6 +29587,8 @@ $diff"
29565
29587
  claude) claude -p "$review_prompt" 2>&1 ;;
29566
29588
  codex) codex exec --sandbox workspace-write "$review_prompt" 2>&1 ;;
29567
29589
  cline) cline -y "$review_prompt" 2>&1 ;;
29590
+ aider) aider --message "$review_prompt" --yes-always --no-auto-commits < /dev/null 2>&1 ;;
29591
+ opencode) (source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$review_prompt" 2>&1) ;;
29568
29592
  *) echo -e "${RED}Unknown provider: $provider${NC}"; return 1 ;;
29569
29593
  esac
29570
29594
  ;;
package/autonomy/run.sh CHANGED
@@ -831,6 +831,17 @@ print(catalog["providers"]["claude"]["cli_aliases"].get(os.environ["_LOKI_SELECT
831
831
 
832
832
  export LOKI_PHASE_UNIT_TESTS LOKI_PHASE_E2E_TESTS
833
833
  export LOKI_PHASE_CODE_REVIEW LOKI_PHASE_SECURITY LOKI_PHASE_ACCESSIBILITY
834
+ # EXPORT THE PHASES THIS PROFILE TURNS OFF, not only the ones it leaves on.
835
+ # The receipt derives quality_gates.disabled_phases by scanning the
836
+ # environment for LOKI_PHASE_* set to false (proof-generator.py:470-478), so
837
+ # a phase that is disabled but never exported is INVISIBLE to it: the receipt
838
+ # reported disabled_phases [] and all_phases_enabled true while six phases
839
+ # were switched off. That defeats the field's stated purpose -- "a receipt
840
+ # must be able to say what was NOT checked" -- and makes a narrowed run look
841
+ # identical to a full one. The sibling loki_apply_scoped_change_profile
842
+ # already exports the phases it changes; this one exported only its enables.
843
+ export LOKI_PHASE_API_TESTS LOKI_PHASE_INTEGRATION LOKI_PHASE_PERFORMANCE
844
+ export LOKI_PHASE_REGRESSION LOKI_PHASE_UAT LOKI_PHASE_WEB_RESEARCH
834
845
  export LOKI_COUNCIL_ENABLED LOKI_EVIDENCE_GATE LOKI_PROOF_GATE LOKI_PROOF
835
846
  export LOKI_DASHBOARD LOKI_PARALLEL_MODE LOKI_MAX_PARALLEL_AGENTS
836
847
  export LOKI_MAX_ITERATIONS LOKI_MAX_RETRIES LOKI_BASE_WAIT LOKI_MAX_WAIT
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # v7.7.18 sample Claude Code SessionEnd hook (MANUAL install only).
3
+ #
4
+ # Pipes the Claude Code session transcript through `loki memory ingest`
5
+ # so the project's .loki/memory/ store accumulates real episodes from
6
+ # every Claude Code session (not just `loki start <prd>` sessions).
7
+ #
8
+ # v7.7.18 council fix (Opus 2): supports BOTH (a) the documented JSON-
9
+ # on-stdin payload format with `transcript_path` key, AND (b) the legacy
10
+ # $CLAUDE_TRANSCRIPT_PATH env var as fallback. Whichever Claude Code
11
+ # version is running, one of them works.
12
+ #
13
+ # Manual installation (recommended -- automated installer deferred to
14
+ # v7.7.19 pending empirical schema verification):
15
+ # 1. Add to your ~/.claude/settings.json:
16
+ # {
17
+ # "hooks": {
18
+ # "SessionEnd": [
19
+ # {
20
+ # "matcher": "clear",
21
+ # "hooks": [
22
+ # {
23
+ # "type": "command",
24
+ # "command": "/absolute/path/to/loki-session-end.sh"
25
+ # }
26
+ # ]
27
+ # }
28
+ # ]
29
+ # }
30
+ # }
31
+ # 2. Reload Claude Code (or open a new session).
32
+ # 3. End a session with `/clear` to trigger the hook.
33
+ #
34
+ # Note: SessionEnd only fires on /clear, NOT on normal exits per the
35
+ # Claude Code documentation. To capture EVERY session, alternative
36
+ # event types may be needed (researched in v7.7.19+).
37
+ #
38
+ # Escape hatches:
39
+ # LOKI_MEMORY_CAPTURE_DISABLED=true # blocks ingest at the engine
40
+ #
41
+ # Privacy: the ingester scrubs credential keywords + high-entropy
42
+ # token shapes (sk-, ghp_/ghs_, xox*, AIza, AKIA) before writing.
43
+ # Path-aware scrubbing for sensitive directories added in v7.7.18.
44
+ set -u
45
+
46
+ if [ "${LOKI_MEMORY_CAPTURE_DISABLED:-}" = "true" ]; then
47
+ exit 0
48
+ fi
49
+
50
+ # Resolve transcript path from EITHER stdin JSON OR env var.
51
+ TRANSCRIPT=""
52
+
53
+ # 1. Try stdin JSON (documented Claude Code hook payload format).
54
+ # Only read stdin if it's not a TTY (i.e. there's actual data).
55
+ if [ ! -t 0 ]; then
56
+ # Read up to 16KB of stdin (JSON payloads are tiny)
57
+ STDIN_DATA=$(head -c 16384 2>/dev/null || true)
58
+ if [ -n "$STDIN_DATA" ] && command -v python3 >/dev/null 2>&1; then
59
+ TRANSCRIPT=$(printf '%s' "$STDIN_DATA" | python3 -c "
60
+ import json, sys
61
+ try:
62
+ d = json.loads(sys.stdin.read())
63
+ print(d.get('transcript_path', ''), end='')
64
+ except Exception:
65
+ pass
66
+ " 2>/dev/null || true)
67
+ fi
68
+ fi
69
+
70
+ # 2. Fallback to env var (legacy / undocumented variant).
71
+ if [ -z "$TRANSCRIPT" ]; then
72
+ TRANSCRIPT="${CLAUDE_TRANSCRIPT_PATH:-}"
73
+ fi
74
+
75
+ if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then
76
+ # Nothing to ingest -- silent exit so we never block SessionEnd.
77
+ exit 0
78
+ fi
79
+
80
+ # Find the loki binary; silent skip if not installed.
81
+ if ! command -v loki >/dev/null 2>&1; then
82
+ exit 0
83
+ fi
84
+
85
+ # Fire-and-forget ingest. Backgrounded so SessionEnd is not blocked.
86
+ loki memory ingest --from-claude-transcript "$TRANSCRIPT" >/dev/null 2>&1 &
87
+ disown 2>/dev/null || true
88
+
89
+ exit 0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.37.0"
10
+ __version__ = "9.39.0"
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.37.0
5
+ **Version:** v9.39.0
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var _t=Object.create;var{getPrototypeOf:vt,defineProperty:jG,getOwnPropertyNames:ht}=Object;var gt=Object.prototype.hasOwnProperty;function mt($){return this[$]}var ut,dt,pt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?ut??=new WeakMap:dt??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?_t(vt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of ht($))if(!gt.call(J,q))jG(J,q,{get:mt.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var ct=($)=>$;function lt($,X){this[$]=ct.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:lt.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var UR={};B1(UR,{lokiDir:()=>h0,homeLokiDir:()=>WQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as it}from"url";import{existsSync as Zq}from"fs";import{homedir as at}from"os";function ot(){let $=YR;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(YR,"..","..","..")}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 WQ(){return h2(at(),".loki")}var YR,L1;var k1=s(()=>{YR=LG(it(import.meta.url));L1=ot()});import{readFileSync as st}from"fs";import{resolve as nt,dirname as rt}from"path";import{fileURLToPath as tt}from"url";function j9(){if(n3!==null)return n3;let $="9.37.0";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=rt(tt(import.meta.url)),Q=AG(X);n3=st(nt(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var OR={};B1(OR,{runOrThrow:()=>Be,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>Me,commandExists:()=>g5,ShellError:()=>TG,MAX_STDOUT_BYTES:()=>MR});async function Jq($,X=MR){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 Be($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new TG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=Ne($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Ne($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function Me($,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 MR=16777216,TG;var y8=s(()=>{TG=class TG 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 Oe?"":$}var Oe,p0,$5,q1,D61,A1,f1,m5,r;var t7=s(()=>{Oe=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),D61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as we}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(we($))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 vR={};B1(vR,{runStatus:()=>te});import{existsSync as u5,readFileSync as C9,readdirSync as ER,statSync as xR}from"fs";import{resolve as A5,basename as pe}from"path";import{homedir as ce}from"os";function kR($){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 SR($,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=kR($),Y=kR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function ie(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
2
+ var _t=Object.create;var{getPrototypeOf:vt,defineProperty:jG,getOwnPropertyNames:ht}=Object;var gt=Object.prototype.hasOwnProperty;function mt($){return this[$]}var ut,dt,pt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?ut??=new WeakMap:dt??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?_t(vt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of ht($))if(!gt.call(J,q))jG(J,q,{get:mt.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var ct=($)=>$;function lt($,X){this[$]=ct.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:lt.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var UR={};B1(UR,{lokiDir:()=>h0,homeLokiDir:()=>WQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as it}from"url";import{existsSync as Zq}from"fs";import{homedir as at}from"os";function ot(){let $=YR;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(YR,"..","..","..")}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 WQ(){return h2(at(),".loki")}var YR,L1;var k1=s(()=>{YR=LG(it(import.meta.url));L1=ot()});import{readFileSync as st}from"fs";import{resolve as nt,dirname as rt}from"path";import{fileURLToPath as tt}from"url";function j9(){if(n3!==null)return n3;let $="9.39.0";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=rt(tt(import.meta.url)),Q=AG(X);n3=st(nt(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var OR={};B1(OR,{runOrThrow:()=>Be,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>Me,commandExists:()=>g5,ShellError:()=>TG,MAX_STDOUT_BYTES:()=>MR});async function Jq($,X=MR){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 Be($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new TG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=Ne($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Ne($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function Me($,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 MR=16777216,TG;var y8=s(()=>{TG=class TG 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 Oe?"":$}var Oe,p0,$5,q1,D61,A1,f1,m5,r;var t7=s(()=>{Oe=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),D61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as we}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(we($))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 vR={};B1(vR,{runStatus:()=>te});import{existsSync as u5,readFileSync as C9,readdirSync as ER,statSync as xR}from"fs";import{resolve as A5,basename as pe}from"path";import{homedir as ce}from"os";function kR($){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 SR($,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=kR($),Y=kR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function ie(){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)
@@ -1337,4 +1337,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1337
1337
  `),2}case"start":{let{runStart:z}=await Promise.resolve().then(() => (yt(),St));return z(Q)}default:return process.stderr.write(`Unknown command: ${X}
1338
1338
  `),process.stderr.write(bt),2}}wR();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var K61=await Z61(Bun.argv.slice(2));process.exit(K61);
1339
1339
 
1340
- //# debugId=DBEFF219B28673BB40CF832D5C8A4D9F
1340
+ //# debugId=F2D05F5DF5DCE17BC281BF37D76CAB57
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.37.0'
78
+ __version__ = '9.39.0'
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.37.0",
4
+ "version": "9.39.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, opencode).",
6
6
  "keywords": [
7
7
  "agent",
@@ -69,6 +69,7 @@
69
69
  "tools/",
70
70
  "plugins/",
71
71
  ".claude-plugin/marketplace.json",
72
+ "claude/hooks/",
72
73
  "autonomy/",
73
74
  "providers/",
74
75
  "vendor/autonomi-verify/",
@@ -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.37.0",
5
+ "version": "9.39.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",