loki-mode 9.18.2 → 9.18.4

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.18.2
6
+ # Loki Mode v9.18.4
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.18.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.18.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.18.2
1
+ 9.18.4
package/autonomy/loki CHANGED
@@ -684,7 +684,7 @@ loki_remote_submit() {
684
684
  last="$status"
685
685
  fi
686
686
  case "$status" in
687
- fired|error|rejected*)
687
+ passed|failed|unknown|error|rejected*)
688
688
  break
689
689
  ;;
690
690
  esac
@@ -701,16 +701,22 @@ loki_remote_submit() {
701
701
  return 1
702
702
  fi
703
703
 
704
- # ponytail: the server reports DISPATCH success ("fired" = the pod launched
705
- # the build), not the build's own exit code -- run_loki_command returns true
706
- # for a --detach launch and for the detach-timeout path. So a non-"fired"
707
- # status is a real failure, but "fired" is not yet proof the build passed.
708
- # Upgrade path: have trigger-server record the build's exit code and expose
709
- # a terminal status here, then gate CI on that instead.
704
+ # Gate on the BUILD's outcome, not on "a build was launched". The server
705
+ # reports passed / failed / unknown as distinct terminal statuses (see
706
+ # JOB_TERMINAL_STATUSES in trigger-server.py). Anything that is not
707
+ # "passed" exits non-zero so a CI pipeline cannot go green on a failed
708
+ # build. "unknown" -- the build detached and its exit code was never
709
+ # observed -- fails CLOSED: an outcome nobody observed is not a pass.
710
710
  case "$status" in
711
- fired) return 0 ;;
712
- "") echo -e "${RED}loki: job $job_id did not reach a terminal status${NC}" >&2; return 1 ;;
713
- *) echo -e "${RED}loki: remote job $job_id failed: $status${NC}" >&2; return 1 ;;
711
+ passed) return 0 ;;
712
+ unknown)
713
+ echo -e "${RED}loki: remote job $job_id outcome UNKNOWN${NC}" >&2
714
+ echo " The build detached and its exit code was never observed." >&2
715
+ echo " An unobserved outcome is not a pass; treating it as a failure." >&2
716
+ return 1
717
+ ;;
718
+ "") echo -e "${RED}loki: job $job_id did not reach a terminal status${NC}" >&2; return 1 ;;
719
+ *) echo -e "${RED}loki: remote job $job_id failed: $status${NC}" >&2; return 1 ;;
714
720
  esac
715
721
  }
716
722
 
@@ -832,6 +838,17 @@ loki_remote_verify_receipt() {
832
838
  return 1
833
839
  fi
834
840
 
841
+ # Drift is REPORTED, never a verdict. This receipt was produced in a cluster
842
+ # pod and is being checked against a different tree, so drift is the normal
843
+ # result and says nothing about tampering -- docs/VERIFICATION-COST.md:
844
+ # hash_ok is integrity, `ok` folds in tree_drift and false-alarms here.
845
+ # Stated rather than suppressed: silently dropping a true fact because it is
846
+ # inconvenient is the same error as folding UNCHECKED into UNSIGNED.
847
+ if [ "$(printf '%s' "$out" | jq -r '[.diff_drift,.tree_drift]|index(true)!=null' 2>/dev/null)" = "true" ]; then
848
+ echo " Drift: this tree differs from the one the receipt records. Expected --"
849
+ echo " the build ran elsewhere. Not tampering, and not part of the verdict."
850
+ fi
851
+
835
852
  # gpg_ok=false covers TWO facts that must never be conflated: the signature
836
853
  # is cryptographically bad over this content (the receipt was ALTERED), and
837
854
  # gpg could not evaluate it at all (we do not hold the public key). Only the
@@ -105,6 +105,19 @@ JOB_EVENT = "loki_job"
105
105
  # storm cannot grow memory without limit.
106
106
  DEFAULT_JOB_HISTORY = 1024
107
107
 
108
+ # Terminal job statuses a remote client can gate on. "queued" and "running"
109
+ # are deliberately absent: a client must be able to tell "not done yet" from
110
+ # "done and failed", which a single non-passed value could not express.
111
+ #
112
+ # Only JOB_STATUS_PASSED means the build itself succeeded. JOB_STATUS_UNKNOWN
113
+ # means the build detached past our wait window and its exit code was never
114
+ # observed -- an unobserved outcome is NOT a pass, and the client exits
115
+ # non-zero on it (fail closed).
116
+ JOB_STATUS_PASSED = "passed"
117
+ JOB_STATUS_FAILED = "failed"
118
+ JOB_STATUS_UNKNOWN = "unknown"
119
+ JOB_TERMINAL_STATUSES = (JOB_STATUS_PASSED, JOB_STATUS_FAILED, JOB_STATUS_UNKNOWN)
120
+
108
121
  # A run id names a directory under .loki/proofs/. run.sh mints it as
109
122
  # "run-<utc>-<pid>-<rand>" or "proof-<utc>-<pid>-<rand>"; this is the alphabet
110
123
  # those forms use. Applied to the pointer file's contents (never to a request
@@ -338,21 +351,30 @@ def _reap_child(proc):
338
351
  pass
339
352
 
340
353
 
341
- def run_loki_command(args, dry_run=False):
342
- """Run a loki command synchronously and reap it; or print it if dry_run.
354
+ # Outcome of a dispatch, as distinct from "did it launch".
355
+ #
356
+ # run_loki_command already distinguished all three of these and then threw two
357
+ # of them away by returning a bool. Collapsing "exited 0" and "still detached"
358
+ # into True is what made a remotely-submitted build that STARTS and then FAILS
359
+ # report success: the client had no value to gate on. UNKNOWN is not a pass --
360
+ # an outcome we could not observe must fail closed.
361
+ # ponytail: the outcome IS the terminal job status, so they are the same three
362
+ # strings rather than two enums plus a mapping table.
363
+ OUTCOME_PASSED = JOB_STATUS_PASSED # ran to completion and exited 0
364
+ OUTCOME_FAILED = JOB_STATUS_FAILED # launch failed, or exited non-zero
365
+ OUTCOME_UNKNOWN = JOB_STATUS_UNKNOWN # detached; exit code never observed
343
366
 
344
- Returns True if the command was launched and exited 0 (or backgrounded
345
- cleanly within the wait window), False if the launch failed or it exited
346
- non-zero. The child is always waited on, so no zombies accumulate. stderr
347
- is captured on failure so a broken dispatch is diagnosable.
348
367
 
349
- This is invoked from worker threads, so blocking here does not block the
350
- HTTP listener.
368
+ def run_loki_outcome(args, dry_run=False):
369
+ """Run a loki command and report WHICH of the three outcomes occurred.
370
+
371
+ Returns one of OUTCOME_PASSED / OUTCOME_FAILED / OUTCOME_UNKNOWN. This is
372
+ the honest version of run_loki_command, which answers only "did it launch".
351
373
  """
352
374
  cmd = ["loki"] + args
353
375
  if dry_run:
354
376
  logging.info("[DRY-RUN] Would run: %s", " ".join(cmd))
355
- return True
377
+ return OUTCOME_PASSED
356
378
  logging.info("Running: %s", " ".join(cmd))
357
379
  try:
358
380
  proc = subprocess.Popen(
@@ -362,18 +384,14 @@ def run_loki_command(args, dry_run=False):
362
384
  )
363
385
  except (FileNotFoundError, OSError) as e:
364
386
  logging.error("Failed to launch %s: %s", " ".join(cmd), e)
365
- return False
387
+ return OUTCOME_FAILED
366
388
 
367
389
  try:
368
- # Wait (and thereby reap) the child. A --detach launch returns quickly;
369
- # this bound only guards against a wedged launch.
370
390
  _, stderr = proc.communicate(timeout=DISPATCH_WAIT_SECONDS)
371
391
  except subprocess.TimeoutExpired:
372
- # The dispatch is still running past our wait window. We stop blocking
373
- # the worker thread, but the child is NOT abandoned: a one-shot daemon
374
- # reaper thread waits on it so it is always reaped (no zombie) while
375
- # THIS process is alive, and the OS reparents it after we exit. This
376
- # keeps the "always waited on, no zombies" guarantee honest.
392
+ # Detached past the wait window. The reaper collects the child, but we
393
+ # never see its exit code, so the outcome is genuinely UNKNOWN -- NOT a
394
+ # pass. Reporting success here is exactly the defect this replaces.
377
395
  logging.info(
378
396
  "Dispatch pid=%d still running after %ds; reaping in background",
379
397
  proc.pid,
@@ -385,11 +403,11 @@ def run_loki_command(args, dry_run=False):
385
403
  name="loki-trigger-reaper-%d" % proc.pid,
386
404
  daemon=True,
387
405
  ).start()
388
- return True
406
+ return OUTCOME_UNKNOWN
389
407
 
390
408
  if proc.returncode == 0:
391
409
  logging.info("Dispatch pid=%d completed (exit 0)", proc.pid)
392
- return True
410
+ return OUTCOME_PASSED
393
411
 
394
412
  stderr_text = ""
395
413
  if stderr:
@@ -400,7 +418,28 @@ def run_loki_command(args, dry_run=False):
400
418
  proc.returncode,
401
419
  stderr_text or "(no stderr)",
402
420
  )
403
- return False
421
+ return OUTCOME_FAILED
422
+
423
+
424
+ def run_loki_command(args, dry_run=False):
425
+ """Run a loki command synchronously and reap it; or print it if dry_run.
426
+
427
+ Returns True if the command was launched and exited 0 (or backgrounded
428
+ cleanly within the wait window), False if the launch failed or it exited
429
+ non-zero. The child is always waited on, so no zombies accumulate. stderr
430
+ is captured on failure so a broken dispatch is diagnosable.
431
+
432
+ This is invoked from worker threads, so blocking here does not block the
433
+ HTTP listener.
434
+
435
+ Kept as the bool view for the GitHub webhook handlers, which only care
436
+ whether a dispatch started. A caller that must know whether the BUILD
437
+ passed wants run_loki_outcome instead. Behaviour is unchanged: a detached
438
+ dispatch (UNKNOWN) still reads as True here, exactly as before.
439
+ """
440
+ return run_loki_outcome(args, dry_run=dry_run) in (
441
+ OUTCOME_PASSED, OUTCOME_UNKNOWN,
442
+ )
404
443
 
405
444
 
406
445
  def handle_issues_event(payload, dry_run=False):
@@ -496,9 +535,11 @@ def handle_job_event(payload, dry_run=False):
496
535
  return None, "rejected (invalid spec)"
497
536
  args = ["start", spec.strip(), "--detach"]
498
537
  summary = "job %s: %s" % (payload.get("job_id", "?"), spec.strip())
499
- success = run_loki_command(args, dry_run=dry_run)
500
- status = "fired" if success else "error"
501
- if success:
538
+ # A remote submitter gates CI on this, so report the BUILD's outcome, not
539
+ # merely that a build was launched. "fired" (launched) and "passed" must
540
+ # never share a value.
541
+ status = run_loki_outcome(args, dry_run=dry_run)
542
+ if status != OUTCOME_FAILED:
502
543
  send_notification("Trigger fired: %s" % summary)
503
544
  return summary, status
504
545
 
@@ -732,7 +773,16 @@ class Dispatcher:
732
773
  continue
733
774
  try:
734
775
  event_type, payload = item
735
- job_id = payload.get("job_id") if isinstance(payload, dict) else None
776
+ # Honour job_id ONLY for a remotely-submitted job. A GitHub
777
+ # payload carries no job_id of its own, so accepting one from
778
+ # any payload let a holder of the WEBHOOK HMAC write into the
779
+ # /jobs status store -- overwriting a real job's terminal
780
+ # status (e.g. "passed" -> "fired") and re-introducing the
781
+ # false-green. That is a webhook credential reaching a /jobs
782
+ # capability, which the separate-credential design forbids.
783
+ job_id = (payload.get("job_id")
784
+ if event_type == JOB_EVENT and isinstance(payload, dict)
785
+ else None)
736
786
  if job_id:
737
787
  self.record_job(job_id, "running")
738
788
  # Counted for every dispatch, including webhook builds that
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.18.2"
10
+ __version__ = "9.18.4"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.18.2";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){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 V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(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 bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(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 oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
2
+ var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.18.4";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){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 V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(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 bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(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 oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
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)
@@ -1236,4 +1236,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1236
1236
  `),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (g_(),v_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
1237
1237
  `),process.stderr.write(m_),2}}lO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var dV0=await pV0(Bun.argv.slice(2));process.exit(dV0);
1238
1238
 
1239
- //# debugId=B34A84808461BB0C64756E2164756E21
1239
+ //# debugId=FEE1C42CA5DB4B5764756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.18.2'
78
+ __version__ = '9.18.4'
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.18.2",
4
+ "version": "9.18.4",
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": "9.18.2",
5
+ "version": "9.18.4",
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",