loki-mode 8.55.0 → 8.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v8.55.0
6
+ # Loki Mode v8.63.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
469
469
 
470
470
  ---
471
471
 
472
- **v8.55.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.63.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.55.0
1
+ 8.63.0
package/autonomy/run.sh CHANGED
@@ -21379,7 +21379,23 @@ except Exception:
21379
21379
  # shellcheck disable=SC1090
21380
21380
  . "${SCRIPT_DIR}/spec-interrogation.sh" 2>/dev/null || true
21381
21381
  if type spec_interrogation_run &>/dev/null; then
21382
+ # TIMED. Startup was completely unmeasured: on a real run, 128
21383
+ # SECONDS elapsed between session_start and iteration_start -- over
21384
+ # two minutes in which the user sees nothing and no agent work has
21385
+ # begun. Nothing in .loki/events.jsonl accounted for any of it, so
21386
+ # the interval could not be attributed, let alone optimised.
21387
+ #
21388
+ # This step calls the provider, so it is the prime suspect for the
21389
+ # bulk of that window. Naming it turns "startup is slow" into a
21390
+ # number, the same way stage timings turned "the run is slow" into
21391
+ # "the agent call is 93% of wall clock".
21392
+ #
21393
+ # Uses the existing emit_stage_complete channel, so measure-run.sh
21394
+ # and every other consumer pick it up with no new plumbing.
21395
+ local _si_t0
21396
+ _si_t0=$(date +%s 2>/dev/null || echo 0)
21382
21397
  spec_interrogation_run "$prd_path" || true
21398
+ emit_stage_complete "spec_interrogation" "pass" "$_si_t0" 2>/dev/null || true
21383
21399
  fi
21384
21400
  # #87: no-HITL fast-fail on an unresolved spec-INTERNAL contradiction.
21385
21401
  # A contradiction (class=contradictory) is NEVER auto-acked (P2-4) and only
@@ -22301,6 +22317,11 @@ def process_stream():
22301
22317
  # message. Stays False when partial messages are off (no stream_event lines).
22302
22318
  streamed_text_blocks = False
22303
22319
 
22320
+ # Per-turn usage samples for the context-growth record (L1). Appended on
22321
+ # every assistant message; written once at the result event. Bounded below
22322
+ # so a pathological run cannot grow this without limit.
22323
+ _turn_usage = []
22324
+
22304
22325
  for line in sys.stdin:
22305
22326
  line = line.strip()
22306
22327
  if not line:
@@ -22336,6 +22357,43 @@ def process_stream():
22336
22357
  # Extract and print assistant text
22337
22358
  message = data.get("message", {})
22338
22359
  content = message.get("content", [])
22360
+
22361
+ # PER-TURN CONTEXT GROWTH (read-only instrumentation, L1).
22362
+ #
22363
+ # WHY. One measured iteration re-sent 10,651,759 cached-read
22364
+ # tokens to produce 34,729 output tokens -- a 307:1 ratio, in a
22365
+ # SINGLE provider call (one iteration_start, one
22366
+ # result-cost-1.json, so cross-iteration reuse is ruled out).
22367
+ # That call was 100% of measured stage time.
22368
+ #
22369
+ # The provider cache already saved us 10x ($31.96 -> $3.20 of a
22370
+ # $4.74 iteration). We are not missing a cache; the ORDER being
22371
+ # discounted is enormous, and cached reads are still 67% of the
22372
+ # bill. Those tokens are prefill the model must process serially
22373
+ # before emitting a character, so this is the only measured lever
22374
+ # that touches BOTH cost and the 744s.
22375
+ #
22376
+ # "the tool loop re-accumulates history" is INFERRED from the
22377
+ # ratio, not observed. Trimming context on an inference is how
22378
+ # you ship an agent that forgets what it already tried and redoes
22379
+ # the work -- raising iterations and costing more than it saves.
22380
+ # So this MEASURES per turn and trims nothing. The cut is a
22381
+ # separate decision, gated on iterations-to-done rather than on
22382
+ # a token count.
22383
+ try:
22384
+ _tu = (message.get("usage") or {})
22385
+ _tcr = _tu.get("cache_read_input_tokens")
22386
+ if isinstance(_tcr, int) and _tcr >= 0:
22387
+ _turn_usage.append({
22388
+ "turn": len(_turn_usage) + 1,
22389
+ "cache_read_tokens": _tcr,
22390
+ "input_tokens": _tu.get("input_tokens", 0) or 0,
22391
+ "output_tokens": _tu.get("output_tokens", 0) or 0,
22392
+ "cache_creation_tokens":
22393
+ _tu.get("cache_creation_input_tokens", 0) or 0,
22394
+ })
22395
+ except Exception:
22396
+ pass
22339
22397
  for item in content:
22340
22398
  if item.get("type") == "text":
22341
22399
  text = item.get("text", "")
@@ -22488,6 +22546,44 @@ def process_stream():
22488
22546
  "cache_read_tokens": _u.get("cache_read_input_tokens", 0),
22489
22547
  "cache_creation_tokens": _u.get("cache_creation_input_tokens", 0),
22490
22548
  }
22549
+ # CONTEXT-GROWTH RECORD (L1). Written whenever turns were
22550
+ # observed, independently of whether cost was reported --
22551
+ # the growth shape is the finding, and tying it to
22552
+ # total_cost_usd would lose it on every provider that does
22553
+ # not report dollars (codex reports tokens, never cost).
22554
+ if _turn_usage:
22555
+ try:
22556
+ os.makedirs(".loki/metrics", exist_ok=True)
22557
+ _first = _turn_usage[0]["cache_read_tokens"]
22558
+ _last = _turn_usage[-1]["cache_read_tokens"]
22559
+ _growth = {
22560
+ "iteration": _iter,
22561
+ "turns": len(_turn_usage),
22562
+ "first_turn_cache_read": _first,
22563
+ "last_turn_cache_read": _last,
22564
+ # The headline: how much bigger the context got
22565
+ # between the first and last turn of ONE call.
22566
+ "growth_factor": (round(_last / _first, 2)
22567
+ if _first > 0 else None),
22568
+ "total_cache_read": sum(
22569
+ t["cache_read_tokens"] for t in _turn_usage),
22570
+ "total_output": sum(
22571
+ t["output_tokens"] for t in _turn_usage),
22572
+ # Bounded sample: the shape is visible in the
22573
+ # first and last few turns, and an unbounded
22574
+ # array would make this file grow with the run.
22575
+ "sample": (_turn_usage[:5] + _turn_usage[-5:]
22576
+ if len(_turn_usage) > 10
22577
+ else _turn_usage),
22578
+ }
22579
+ _gp = ".loki/metrics/context-growth-" + str(_iter) + ".json"
22580
+ _gt = _gp + ".tmp"
22581
+ with open(_gt, "w") as _gf:
22582
+ json.dump(_growth, _gf)
22583
+ os.replace(_gt, _gp)
22584
+ except Exception:
22585
+ pass
22586
+
22491
22587
  if _rec["total_cost_usd"] is not None:
22492
22588
  os.makedirs(".loki/metrics", exist_ok=True)
22493
22589
  _p = ".loki/metrics/result-cost-" + str(_iter) + ".json"
@@ -22635,6 +22731,33 @@ if __name__ == "__main__":
22635
22731
  # costs zero extra subprocesses -- we pass the existing epoch through.
22636
22732
  emit_stage_complete "agent" "$([ "$exit_code" -eq 0 ] 2>/dev/null && echo pass || echo fail)" "$start_time"
22637
22733
 
22734
+ # AGENT PROMPT SIZE. The call this brackets is 93% of a run's wall clock
22735
+ # (1814s of 1941s measured), and its INPUT was never measured -- every
22736
+ # reviewer logs its prompt bytes, the dominant call logged nothing.
22737
+ #
22738
+ # Prompt size is the input side of that 93% and one of the few levers we
22739
+ # actually control: we cannot make the provider faster, but we can send
22740
+ # it less. Without the number, "the prompt got bigger" is invisible
22741
+ # until it shows up as latency and cost with no attributable cause --
22742
+ # the same gap W1 closed for tokens.
22743
+ #
22744
+ # Costs one `wc -c` on a string already in memory: no subprocess for the
22745
+ # provider, no extra file read. Emitted on the existing event channel so
22746
+ # measure-run.sh and the receipt pick it up with no new plumbing.
22747
+ if [ -n "${prompt:-}" ]; then
22748
+ local _agent_prompt_bytes
22749
+ _agent_prompt_bytes=$(printf '%s' "$prompt" | wc -c 2>/dev/null | tr -d ' ')
22750
+ case "$_agent_prompt_bytes" in
22751
+ ''|*[!0-9]*) ;; # unmeasurable -> emit nothing, never a zero
22752
+ *)
22753
+ emit_event_json "agent_prompt" \
22754
+ "bytes=$_agent_prompt_bytes" \
22755
+ "iteration=${ITERATION_COUNT:-0}" \
22756
+ "duration_s=$duration" 2>/dev/null || true
22757
+ ;;
22758
+ esac
22759
+ fi
22760
+
22638
22761
  # TIME TO FIRST ARTIFACT. The companion to seconds_to_first_preview, for
22639
22762
  # the case that has no preview at all.
22640
22763
  #
@@ -22959,6 +23082,14 @@ if __name__ == "__main__":
22959
23082
  local tc_count
22960
23083
  tc_count=$(track_gate_failure "test_coverage")
22961
23084
  gate_failures="${gate_failures}test_coverage,"
23085
+ # Fourth dead branch, found by deriving the handled-gate set
23086
+ # from the writer instead of hardcoding it: test_coverage
23087
+ # maps to quality/test-results.json and had no caller either.
23088
+ if [ "$(gate_failure_disposition "$tc_count")" != "block" ]; then
23089
+ local _tc_thresh="$GATE_CLEAR_LIMIT"
23090
+ [ "$GATE_ESCALATE_LIMIT" -lt "$_tc_thresh" ] && _tc_thresh="$GATE_ESCALATE_LIMIT"
23091
+ write_gate_escalation_guidance "test_coverage" "$tc_count" "$_tc_thresh" || true
23092
+ fi
22962
23093
  # P0-1 Fix A: distinguish a coverage-only block (tests passed,
22963
23094
  # enforced coverage below threshold) from a genuine tests-red
22964
23095
  # block in the log so the operator is not misled.
@@ -22994,6 +23125,23 @@ if __name__ == "__main__":
22994
23125
  mk_count=$(track_gate_failure "mock_integrity")
22995
23126
  gate_failures="${gate_failures}mock_integrity,"
22996
23127
  log_warn "Mock integrity gate FAILED ($mk_count consecutive) - CRITICAL/HIGH mock problems"
23128
+ # Escalation guidance was DEAD for this gate.
23129
+ # write_gate_escalation_guidance already handles
23130
+ # mock_integrity, mutation_integrity and test_coverage by
23131
+ # name -- and only code_review ever called it, so those
23132
+ # branches could never run.
23133
+ #
23134
+ # Measured: on a real run mock_integrity failed THREE
23135
+ # times (the most of any gate) and
23136
+ # .loki/signals/GATE_ESCALATION.json was never written.
23137
+ # The agent was told the gate failed and never handed the
23138
+ # findings file that says WHY, which is the 56%
23139
+ # "did not attempt to recover" failure shape.
23140
+ if [ "$(gate_failure_disposition "$mk_count")" != "block" ]; then
23141
+ local _mk_thresh="$GATE_CLEAR_LIMIT"
23142
+ [ "$GATE_ESCALATE_LIMIT" -lt "$_mk_thresh" ] && _mk_thresh="$GATE_ESCALATE_LIMIT"
23143
+ write_gate_escalation_guidance "mock_integrity" "$mk_count" "$_mk_thresh" || true
23144
+ fi
22997
23145
  # F0, third gate. Measured on the v8.49.0 FireLater run:
22998
23146
  # mock_integrity failed 3 times -- MORE than any other
22999
23147
  # gate -- and was not wired to the stuck check, so an
@@ -23030,6 +23178,15 @@ if __name__ == "__main__":
23030
23178
  mt_count=$(track_gate_failure "mutation_integrity")
23031
23179
  gate_failures="${gate_failures}mutation_integrity,"
23032
23180
  log_warn "Mutation integrity gate FAILED ($mt_count consecutive) - HIGH test-fitting detected"
23181
+ # Same dead-branch fix as mock_integrity above:
23182
+ # write_gate_escalation_guidance maps mutation_integrity to
23183
+ # mutation-findings.txt and nothing ever called it with that
23184
+ # gate name, so the mapping could never fire.
23185
+ if [ "$(gate_failure_disposition "$mt_count")" != "block" ]; then
23186
+ local _mt_thresh="$GATE_CLEAR_LIMIT"
23187
+ [ "$GATE_ESCALATE_LIMIT" -lt "$_mt_thresh" ] && _mt_thresh="$GATE_ESCALATE_LIMIT"
23188
+ write_gate_escalation_guidance "mutation_integrity" "$mt_count" "$_mt_thresh" || true
23189
+ fi
23033
23190
  # F0: an unchanging cause means the next iteration reaches
23034
23191
  # the same verdict. FireLater burned 3 iterations here on a
23035
23192
  # detector that was never packaged, failing in 0-1s each
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.55.0"
10
+ __version__ = "8.63.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -559,6 +559,40 @@ start_time = datetime.now(timezone.utc)
559
559
  _dashboard_start_time = time.time()
560
560
 
561
561
 
562
+ def _registry_run_alive(loki_dir: _Path) -> bool:
563
+ """True when .loki/pids/ holds a LIVE wrapper/runner process.
564
+
565
+ Third liveness source for a CLI-started background run, which writes
566
+ neither loki.pid nor session.json (run.sh only UPDATES session.json when it
567
+ already exists). Both of those checks therefore fail for `loki start` and
568
+ the run falls through to "stopped" while it is actively building.
569
+
570
+ The kind filter is load-bearing: .loki/pids/ also registers the dashboard
571
+ itself, the status-monitor and the resource-monitor, none of which carry a
572
+ "kind" key. Accepting any live pid here would let the dashboard's own
573
+ process prove the run is alive, turning a false-stopped into a permanent
574
+ false-running. Keep it to wrapper/runner.
575
+
576
+ Liveness is proven with os.kill(pid, 0), never by the file's presence -- a
577
+ stale entry from a crashed run must NOT read as alive.
578
+ """
579
+ try:
580
+ for _entry in (loki_dir / "pids").glob("*.json"):
581
+ _rec = _safe_json_read(_entry, {})
582
+ if not isinstance(_rec, dict):
583
+ continue
584
+ if _rec.get("kind") not in ("wrapper", "runner"):
585
+ continue
586
+ try:
587
+ os.kill(int(_rec.get("pid", 0)), 0)
588
+ except (ValueError, TypeError, OSError, ProcessLookupError):
589
+ continue
590
+ return True
591
+ except OSError:
592
+ pass
593
+ return False
594
+
595
+
562
596
  async def _push_loki_state_loop() -> None:
563
597
  """Background loop: push .loki/ state changes to all WebSocket clients.
564
598
 
@@ -679,35 +713,15 @@ async def _push_loki_state_loop() -> None:
679
713
  pass
680
714
 
681
715
  # 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.
716
+ # CLI-started background run DOES write. Shared with
717
+ # /api/status via _registry_run_alive so both live
718
+ # surfaces agree -- they previously did not: on a real
719
+ # `loki start` build this stream broadcast "running"
720
+ # while /api/status returned "stopped" for the SAME run
721
+ # in the same second, because only this copy had the
722
+ # pids/ source.
696
723
  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
724
+ _pid_alive = _registry_run_alive(loki_dir)
711
725
 
712
726
  status_str = raw.get("mode", "autonomous")
713
727
  # Control files are the AUTHORITY, and they are checked
@@ -1438,6 +1452,13 @@ async def get_status() -> StatusResponse:
1438
1452
  if not mode:
1439
1453
  mode = "autonomous"
1440
1454
 
1455
+ # Third source: .loki/pids/ registry (see _registry_run_alive). A
1456
+ # CLI-started background run writes neither loki.pid nor session.json, so
1457
+ # both checks above miss it and a healthy build reported "stopped" here
1458
+ # while the WS stream -- which already had this source -- said "running".
1459
+ if not running:
1460
+ running = _registry_run_alive(loki_dir)
1461
+
1441
1462
  # Determine status string
1442
1463
  if not running:
1443
1464
  status = "stopped"
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var h_=Object.create;var{getPrototypeOf:v_,defineProperty:rK,getOwnPropertyNames:g_}=Object;var m_=Object.prototype.hasOwnProperty;function u_(Z){return this[Z]}var p_,d_,c_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?p_??=new WeakMap:d_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?h_(v_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of g_(Z))if(!m_.call(K,$))rK(K,$,{get:u_.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 l_=(Z)=>Z;function i_(Z,X){this[Z]=l_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:i_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>P4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>r0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as a_}from"url";import{existsSync as GQ}from"fs";import{homedir as s_}from"os";function n_(){let Z=RO;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(RO,"..","..","..")}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 P4(){return n7(s_(),".loki")}var RO,r0;var G8=p(()=>{RO=tK(a_(import.meta.url));r0=n_()});import{readFileSync as o_}from"fs";import{resolve as r_,dirname as t_}from"path";import{fileURLToPath as e_}from"url";function f3(){if(h5!==null)return h5;let Z="8.55.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=t_(e_(import.meta.url)),Q=eK(X);h5=o_(r_(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var HQ=p(()=>{G8()});var bO={};l0(bO,{runOrThrow:()=>Of,run:()=>E0,readStreamCapped:()=>UQ,commandVersion:()=>Af,commandExists:()=>X9,ShellError:()=>Z$,MAX_STDOUT_BYTES:()=>yO});async function UQ(Z,X=yO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([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 Of(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=Lf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Lf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Af(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,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 jf?"":Z}var jf,L0,k8,p0,ZV0,i0,H8,Q9,v;var S6=p(()=>{jf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),k8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),ZV0=o7("\x1B[0;34m"),i0=o7("\x1B[0;36m"),H8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as xf}from"fs";async function E7(){if(k4!==void 0)return k4;let Z="/opt/homebrew/bin/python3.12";if(xf(Z))return k4=Z,Z;let X=await X9("python3.12");if(X)return k4=X,X;let Q=await X9("python3");return k4=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 k4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Yh});import{existsSync as Y9,readFileSync as v3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as sf}from"path";import{homedir as nf}from"os";function sO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*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=sO(Z),V=sO(X);return` ${H8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function rf(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
2
+ var h_=Object.create;var{getPrototypeOf:v_,defineProperty:rK,getOwnPropertyNames:g_}=Object;var m_=Object.prototype.hasOwnProperty;function u_(Z){return this[Z]}var p_,d_,c_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?p_??=new WeakMap:d_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?h_(v_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of g_(Z))if(!m_.call(K,$))rK(K,$,{get:u_.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 l_=(Z)=>Z;function i_(Z,X){this[Z]=l_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:i_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>P4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>r0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as a_}from"url";import{existsSync as GQ}from"fs";import{homedir as s_}from"os";function n_(){let Z=RO;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(RO,"..","..","..")}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 P4(){return n7(s_(),".loki")}var RO,r0;var G8=p(()=>{RO=tK(a_(import.meta.url));r0=n_()});import{readFileSync as o_}from"fs";import{resolve as r_,dirname as t_}from"path";import{fileURLToPath as e_}from"url";function f3(){if(h5!==null)return h5;let Z="8.63.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=t_(e_(import.meta.url)),Q=eK(X);h5=o_(r_(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var HQ=p(()=>{G8()});var bO={};l0(bO,{runOrThrow:()=>Of,run:()=>E0,readStreamCapped:()=>UQ,commandVersion:()=>Af,commandExists:()=>X9,ShellError:()=>Z$,MAX_STDOUT_BYTES:()=>yO});async function UQ(Z,X=yO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([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 Of(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=Lf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Lf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Af(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,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 jf?"":Z}var jf,L0,k8,p0,ZV0,i0,H8,Q9,v;var S6=p(()=>{jf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),k8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),ZV0=o7("\x1B[0;34m"),i0=o7("\x1B[0;36m"),H8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as xf}from"fs";async function E7(){if(k4!==void 0)return k4;let Z="/opt/homebrew/bin/python3.12";if(xf(Z))return k4=Z,Z;let X=await X9("python3.12");if(X)return k4=X,X;let Q=await X9("python3");return k4=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 k4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Yh});import{existsSync as Y9,readFileSync as v3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as sf}from"path";import{homedir as nf}from"os";function sO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*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=sO(Z),V=sO(X);return` ${H8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function rf(){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)
@@ -1227,4 +1227,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1227
1227
  `),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (b_(),y_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
1228
1228
  `),process.stderr.write(__),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var fW0=await _W0(Bun.argv.slice(2));process.exit(fW0);
1229
1229
 
1230
- //# debugId=16FDE245B508551364756E2164756E21
1230
+ //# debugId=389F590913B3AC8264756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.55.0'
78
+ __version__ = '8.63.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": "8.55.0",
4
+ "version": "8.63.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "8.55.0",
5
+ "version": "8.63.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",
@@ -320,6 +320,19 @@ def chunk_markdown_file(filepath: Path) -> list[dict]:
320
320
  return chunks
321
321
 
322
322
 
323
+ def _is_skipped(p: Path) -> bool:
324
+ """True when a path lies inside a skipped directory.
325
+
326
+ Compares components of the path RELATIVE to PROJECT_ROOT. An absolute-path
327
+ substring test makes indexing depend on where the repo happens to live.
328
+ """
329
+ try:
330
+ rel = p.resolve().relative_to(PROJECT_ROOT)
331
+ except ValueError:
332
+ return False
333
+ return any(part in SKIP_DIRS for part in rel.parts)
334
+
335
+
323
336
  def collect_files() -> list[tuple[Path, str]]:
324
337
  """Collect all files to index with their type."""
325
338
  files = []
@@ -335,7 +348,12 @@ def collect_files() -> list[tuple[Path, str]]:
335
348
  for p in sorted(PROJECT_ROOT.glob(glob_pattern)):
336
349
  if p.name.startswith("__"):
337
350
  continue
338
- if any(skip in str(p) for skip in SKIP_DIRS):
351
+ # Match path COMPONENTS relative to the project root, never a
352
+ # substring of the absolute path. `.claude` in SKIP_DIRS plus a
353
+ # checkout living under .../.claude/worktrees/<name>/ meant EVERY
354
+ # python file matched and the index came back empty -- silently, as
355
+ # a search that returns nothing rather than an error.
356
+ if _is_skipped(p):
339
357
  continue
340
358
  files.append((p, "python"))
341
359