loki-mode 8.55.0 → 8.64.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.64.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.64.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.55.0
1
+ 8.64.0
package/autonomy/run.sh CHANGED
@@ -1610,8 +1610,18 @@ COMPLEXITY_TIER=${LOKI_COMPLEXITY:-auto}
1610
1610
  DETECTED_COMPLEXITY=""
1611
1611
 
1612
1612
  # Multi-Provider Support (v5.0.0)
1613
- # Provider: claude (default), codex, cline, aider
1614
- LOKI_PROVIDER=${LOKI_PROVIDER:-claude}
1613
+ # Provider: auto-detected when unset; claude > cline > codex > aider > opencode.
1614
+ #
1615
+ # WHY NOT `:-claude`. That default was a hard failure for anyone who has Codex
1616
+ # but not Claude: run.sh would select a provider that is not installed and die,
1617
+ # even though auto_detect_provider() has existed in providers/loader.sh -- with
1618
+ # the right priority order and its own passing test -- since v5.0.0. Nothing in
1619
+ # production ever called it. Detection was built, tested, and never wired.
1620
+ #
1621
+ # An EXPLICIT choice still wins: this only fills an unset value, so
1622
+ # LOKI_PROVIDER=codex and --provider codex are untouched.
1623
+ _LOKI_PROVIDER_WAS_EXPLICIT=1
1624
+ [ -z "${LOKI_PROVIDER:-}" ] && _LOKI_PROVIDER_WAS_EXPLICIT=0
1615
1625
 
1616
1626
  # Source provider configuration
1617
1627
  PROVIDERS_DIR="$PROJECT_DIR/providers"
@@ -1619,6 +1629,21 @@ if [ -f "$PROVIDERS_DIR/loader.sh" ]; then
1619
1629
  # shellcheck source=/dev/null
1620
1630
  source "$PROVIDERS_DIR/loader.sh"
1621
1631
 
1632
+ # Detect only when the operator expressed no preference. Sourcing the
1633
+ # loader first is required -- auto_detect_provider() is defined there.
1634
+ if [ "$_LOKI_PROVIDER_WAS_EXPLICIT" -eq 0 ]; then
1635
+ _detected="$(auto_detect_provider 2>/dev/null || true)"
1636
+ if [ -n "$_detected" ]; then
1637
+ LOKI_PROVIDER="$_detected"
1638
+ echo "[loki] provider: $LOKI_PROVIDER (auto-detected)" >&2
1639
+ else
1640
+ # Nothing installed. Keep the historical default so the existing
1641
+ # "not installed" error path reports claude, which is the actionable
1642
+ # message -- rather than an empty provider name.
1643
+ LOKI_PROVIDER=claude
1644
+ fi
1645
+ fi
1646
+
1622
1647
  # Validate provider
1623
1648
  if ! validate_provider "$LOKI_PROVIDER"; then
1624
1649
  echo "ERROR: Unknown provider: $LOKI_PROVIDER" >&2
@@ -21379,7 +21404,23 @@ except Exception:
21379
21404
  # shellcheck disable=SC1090
21380
21405
  . "${SCRIPT_DIR}/spec-interrogation.sh" 2>/dev/null || true
21381
21406
  if type spec_interrogation_run &>/dev/null; then
21407
+ # TIMED. Startup was completely unmeasured: on a real run, 128
21408
+ # SECONDS elapsed between session_start and iteration_start -- over
21409
+ # two minutes in which the user sees nothing and no agent work has
21410
+ # begun. Nothing in .loki/events.jsonl accounted for any of it, so
21411
+ # the interval could not be attributed, let alone optimised.
21412
+ #
21413
+ # This step calls the provider, so it is the prime suspect for the
21414
+ # bulk of that window. Naming it turns "startup is slow" into a
21415
+ # number, the same way stage timings turned "the run is slow" into
21416
+ # "the agent call is 93% of wall clock".
21417
+ #
21418
+ # Uses the existing emit_stage_complete channel, so measure-run.sh
21419
+ # and every other consumer pick it up with no new plumbing.
21420
+ local _si_t0
21421
+ _si_t0=$(date +%s 2>/dev/null || echo 0)
21382
21422
  spec_interrogation_run "$prd_path" || true
21423
+ emit_stage_complete "spec_interrogation" "pass" "$_si_t0" 2>/dev/null || true
21383
21424
  fi
21384
21425
  # #87: no-HITL fast-fail on an unresolved spec-INTERNAL contradiction.
21385
21426
  # A contradiction (class=contradictory) is NEVER auto-acked (P2-4) and only
@@ -22301,6 +22342,11 @@ def process_stream():
22301
22342
  # message. Stays False when partial messages are off (no stream_event lines).
22302
22343
  streamed_text_blocks = False
22303
22344
 
22345
+ # Per-turn usage samples for the context-growth record (L1). Appended on
22346
+ # every assistant message; written once at the result event. Bounded below
22347
+ # so a pathological run cannot grow this without limit.
22348
+ _turn_usage = []
22349
+
22304
22350
  for line in sys.stdin:
22305
22351
  line = line.strip()
22306
22352
  if not line:
@@ -22336,6 +22382,43 @@ def process_stream():
22336
22382
  # Extract and print assistant text
22337
22383
  message = data.get("message", {})
22338
22384
  content = message.get("content", [])
22385
+
22386
+ # PER-TURN CONTEXT GROWTH (read-only instrumentation, L1).
22387
+ #
22388
+ # WHY. One measured iteration re-sent 10,651,759 cached-read
22389
+ # tokens to produce 34,729 output tokens -- a 307:1 ratio, in a
22390
+ # SINGLE provider call (one iteration_start, one
22391
+ # result-cost-1.json, so cross-iteration reuse is ruled out).
22392
+ # That call was 100% of measured stage time.
22393
+ #
22394
+ # The provider cache already saved us 10x ($31.96 -> $3.20 of a
22395
+ # $4.74 iteration). We are not missing a cache; the ORDER being
22396
+ # discounted is enormous, and cached reads are still 67% of the
22397
+ # bill. Those tokens are prefill the model must process serially
22398
+ # before emitting a character, so this is the only measured lever
22399
+ # that touches BOTH cost and the 744s.
22400
+ #
22401
+ # "the tool loop re-accumulates history" is INFERRED from the
22402
+ # ratio, not observed. Trimming context on an inference is how
22403
+ # you ship an agent that forgets what it already tried and redoes
22404
+ # the work -- raising iterations and costing more than it saves.
22405
+ # So this MEASURES per turn and trims nothing. The cut is a
22406
+ # separate decision, gated on iterations-to-done rather than on
22407
+ # a token count.
22408
+ try:
22409
+ _tu = (message.get("usage") or {})
22410
+ _tcr = _tu.get("cache_read_input_tokens")
22411
+ if isinstance(_tcr, int) and _tcr >= 0:
22412
+ _turn_usage.append({
22413
+ "turn": len(_turn_usage) + 1,
22414
+ "cache_read_tokens": _tcr,
22415
+ "input_tokens": _tu.get("input_tokens", 0) or 0,
22416
+ "output_tokens": _tu.get("output_tokens", 0) or 0,
22417
+ "cache_creation_tokens":
22418
+ _tu.get("cache_creation_input_tokens", 0) or 0,
22419
+ })
22420
+ except Exception:
22421
+ pass
22339
22422
  for item in content:
22340
22423
  if item.get("type") == "text":
22341
22424
  text = item.get("text", "")
@@ -22488,6 +22571,44 @@ def process_stream():
22488
22571
  "cache_read_tokens": _u.get("cache_read_input_tokens", 0),
22489
22572
  "cache_creation_tokens": _u.get("cache_creation_input_tokens", 0),
22490
22573
  }
22574
+ # CONTEXT-GROWTH RECORD (L1). Written whenever turns were
22575
+ # observed, independently of whether cost was reported --
22576
+ # the growth shape is the finding, and tying it to
22577
+ # total_cost_usd would lose it on every provider that does
22578
+ # not report dollars (codex reports tokens, never cost).
22579
+ if _turn_usage:
22580
+ try:
22581
+ os.makedirs(".loki/metrics", exist_ok=True)
22582
+ _first = _turn_usage[0]["cache_read_tokens"]
22583
+ _last = _turn_usage[-1]["cache_read_tokens"]
22584
+ _growth = {
22585
+ "iteration": _iter,
22586
+ "turns": len(_turn_usage),
22587
+ "first_turn_cache_read": _first,
22588
+ "last_turn_cache_read": _last,
22589
+ # The headline: how much bigger the context got
22590
+ # between the first and last turn of ONE call.
22591
+ "growth_factor": (round(_last / _first, 2)
22592
+ if _first > 0 else None),
22593
+ "total_cache_read": sum(
22594
+ t["cache_read_tokens"] for t in _turn_usage),
22595
+ "total_output": sum(
22596
+ t["output_tokens"] for t in _turn_usage),
22597
+ # Bounded sample: the shape is visible in the
22598
+ # first and last few turns, and an unbounded
22599
+ # array would make this file grow with the run.
22600
+ "sample": (_turn_usage[:5] + _turn_usage[-5:]
22601
+ if len(_turn_usage) > 10
22602
+ else _turn_usage),
22603
+ }
22604
+ _gp = ".loki/metrics/context-growth-" + str(_iter) + ".json"
22605
+ _gt = _gp + ".tmp"
22606
+ with open(_gt, "w") as _gf:
22607
+ json.dump(_growth, _gf)
22608
+ os.replace(_gt, _gp)
22609
+ except Exception:
22610
+ pass
22611
+
22491
22612
  if _rec["total_cost_usd"] is not None:
22492
22613
  os.makedirs(".loki/metrics", exist_ok=True)
22493
22614
  _p = ".loki/metrics/result-cost-" + str(_iter) + ".json"
@@ -22635,6 +22756,33 @@ if __name__ == "__main__":
22635
22756
  # costs zero extra subprocesses -- we pass the existing epoch through.
22636
22757
  emit_stage_complete "agent" "$([ "$exit_code" -eq 0 ] 2>/dev/null && echo pass || echo fail)" "$start_time"
22637
22758
 
22759
+ # AGENT PROMPT SIZE. The call this brackets is 93% of a run's wall clock
22760
+ # (1814s of 1941s measured), and its INPUT was never measured -- every
22761
+ # reviewer logs its prompt bytes, the dominant call logged nothing.
22762
+ #
22763
+ # Prompt size is the input side of that 93% and one of the few levers we
22764
+ # actually control: we cannot make the provider faster, but we can send
22765
+ # it less. Without the number, "the prompt got bigger" is invisible
22766
+ # until it shows up as latency and cost with no attributable cause --
22767
+ # the same gap W1 closed for tokens.
22768
+ #
22769
+ # Costs one `wc -c` on a string already in memory: no subprocess for the
22770
+ # provider, no extra file read. Emitted on the existing event channel so
22771
+ # measure-run.sh and the receipt pick it up with no new plumbing.
22772
+ if [ -n "${prompt:-}" ]; then
22773
+ local _agent_prompt_bytes
22774
+ _agent_prompt_bytes=$(printf '%s' "$prompt" | wc -c 2>/dev/null | tr -d ' ')
22775
+ case "$_agent_prompt_bytes" in
22776
+ ''|*[!0-9]*) ;; # unmeasurable -> emit nothing, never a zero
22777
+ *)
22778
+ emit_event_json "agent_prompt" \
22779
+ "bytes=$_agent_prompt_bytes" \
22780
+ "iteration=${ITERATION_COUNT:-0}" \
22781
+ "duration_s=$duration" 2>/dev/null || true
22782
+ ;;
22783
+ esac
22784
+ fi
22785
+
22638
22786
  # TIME TO FIRST ARTIFACT. The companion to seconds_to_first_preview, for
22639
22787
  # the case that has no preview at all.
22640
22788
  #
@@ -22959,6 +23107,14 @@ if __name__ == "__main__":
22959
23107
  local tc_count
22960
23108
  tc_count=$(track_gate_failure "test_coverage")
22961
23109
  gate_failures="${gate_failures}test_coverage,"
23110
+ # Fourth dead branch, found by deriving the handled-gate set
23111
+ # from the writer instead of hardcoding it: test_coverage
23112
+ # maps to quality/test-results.json and had no caller either.
23113
+ if [ "$(gate_failure_disposition "$tc_count")" != "block" ]; then
23114
+ local _tc_thresh="$GATE_CLEAR_LIMIT"
23115
+ [ "$GATE_ESCALATE_LIMIT" -lt "$_tc_thresh" ] && _tc_thresh="$GATE_ESCALATE_LIMIT"
23116
+ write_gate_escalation_guidance "test_coverage" "$tc_count" "$_tc_thresh" || true
23117
+ fi
22962
23118
  # P0-1 Fix A: distinguish a coverage-only block (tests passed,
22963
23119
  # enforced coverage below threshold) from a genuine tests-red
22964
23120
  # block in the log so the operator is not misled.
@@ -22994,6 +23150,23 @@ if __name__ == "__main__":
22994
23150
  mk_count=$(track_gate_failure "mock_integrity")
22995
23151
  gate_failures="${gate_failures}mock_integrity,"
22996
23152
  log_warn "Mock integrity gate FAILED ($mk_count consecutive) - CRITICAL/HIGH mock problems"
23153
+ # Escalation guidance was DEAD for this gate.
23154
+ # write_gate_escalation_guidance already handles
23155
+ # mock_integrity, mutation_integrity and test_coverage by
23156
+ # name -- and only code_review ever called it, so those
23157
+ # branches could never run.
23158
+ #
23159
+ # Measured: on a real run mock_integrity failed THREE
23160
+ # times (the most of any gate) and
23161
+ # .loki/signals/GATE_ESCALATION.json was never written.
23162
+ # The agent was told the gate failed and never handed the
23163
+ # findings file that says WHY, which is the 56%
23164
+ # "did not attempt to recover" failure shape.
23165
+ if [ "$(gate_failure_disposition "$mk_count")" != "block" ]; then
23166
+ local _mk_thresh="$GATE_CLEAR_LIMIT"
23167
+ [ "$GATE_ESCALATE_LIMIT" -lt "$_mk_thresh" ] && _mk_thresh="$GATE_ESCALATE_LIMIT"
23168
+ write_gate_escalation_guidance "mock_integrity" "$mk_count" "$_mk_thresh" || true
23169
+ fi
22997
23170
  # F0, third gate. Measured on the v8.49.0 FireLater run:
22998
23171
  # mock_integrity failed 3 times -- MORE than any other
22999
23172
  # gate -- and was not wired to the stuck check, so an
@@ -23030,6 +23203,15 @@ if __name__ == "__main__":
23030
23203
  mt_count=$(track_gate_failure "mutation_integrity")
23031
23204
  gate_failures="${gate_failures}mutation_integrity,"
23032
23205
  log_warn "Mutation integrity gate FAILED ($mt_count consecutive) - HIGH test-fitting detected"
23206
+ # Same dead-branch fix as mock_integrity above:
23207
+ # write_gate_escalation_guidance maps mutation_integrity to
23208
+ # mutation-findings.txt and nothing ever called it with that
23209
+ # gate name, so the mapping could never fire.
23210
+ if [ "$(gate_failure_disposition "$mt_count")" != "block" ]; then
23211
+ local _mt_thresh="$GATE_CLEAR_LIMIT"
23212
+ [ "$GATE_ESCALATE_LIMIT" -lt "$_mt_thresh" ] && _mt_thresh="$GATE_ESCALATE_LIMIT"
23213
+ write_gate_escalation_guidance "mutation_integrity" "$mt_count" "$_mt_thresh" || true
23214
+ fi
23033
23215
  # F0: an unchanging cause means the next iteration reaches
23034
23216
  # the same verdict. FireLater burned 3 iterations here on a
23035
23217
  # 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.64.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.64.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=9338478554FE2A2264756E2164756E21
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.64.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.64.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.64.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