loki-mode 7.7.13 → 7.7.14
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 +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +167 -33
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.7.
|
|
6
|
+
# Loki Mode v7.7.14
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
-
**v7.7.
|
|
384
|
+
**v7.7.14 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.14
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var b=(K,$)=>{for(var z in $)_7(K,z,{get:$[z],enumerable:!0,configurable:!0,set:P7.bind($,z)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var V1=import.meta.require;var e1={};b(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let z=S1(K);if(z===K)break;K=z}return u(i1,"..","..","..")}function N1(K){let $=K;for(let z=0;z<6;z++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let Q=S1($);if(Q===$)break;$=Q}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var y=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as x7}from"fs";import{resolve as F7,dirname as w7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(n!==null)return n;let K="7.7.
|
|
2
|
+
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var b=(K,$)=>{for(var z in $)_7(K,z,{get:$[z],enumerable:!0,configurable:!0,set:P7.bind($,z)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var V1=import.meta.require;var e1={};b(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let z=S1(K);if(z===K)break;K=z}return u(i1,"..","..","..")}function N1(K){let $=K;for(let z=0;z<6;z++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let Q=S1($);if(Q===$)break;$=Q}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var y=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as x7}from"fs";import{resolve as F7,dirname as w7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(n!==null)return n;let K="7.7.14";if(typeof K==="string"&&K.length>0)return n=K,n;try{let $=w7(S7(import.meta.url)),z=N1($);n=x7(F7(z,"VERSION"),"utf-8").trim()}catch{n="unknown"}return n}var n=null;var D1=R(()=>{y()});var $0={};b($0,{runOrThrow:()=>N7,run:()=>S,commandVersion:()=>D7,commandExists:()=>D,ShellError:()=>C1});async function S(K,$={}){let z=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),Q,X;if($.timeoutMs&&$.timeoutMs>0)Q=setTimeout(()=>{try{z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{z.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[H,Z,q]=await Promise.all([new Response(z.stdout).text(),new Response(z.stderr).text(),z.exited]);return{stdout:H,stderr:Z,exitCode:q}}finally{if(Q)clearTimeout(Q);if(X)clearTimeout(X)}}async function N7(K,$={}){let z=await S(K,$);if(z.exitCode!==0)throw new C1(`command failed (${z.exitCode}): ${K.join(" ")}`,z.exitCode,z.stdout,z.stderr);return z}async function D(K){let $=k7(K),z=await S(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(z.exitCode===0)return z.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await D(K))return null;let Q=await S([K,$],{timeoutMs:5000});if(Q.exitCode!==0)return null;return((Q.stdout||Q.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var c=R(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,z,Q){super(K);this.message=K;this.exitCode=$;this.stdout=z;this.stderr=Q;this.name="ShellError"}}});function l(K){return C7?"":K}var C7,E,C,x,O6,O,k,F,W;var a=R(()=>{C7=(process.env.NO_COLOR??"").length>0;E=l("\x1B[0;31m"),C=l("\x1B[0;32m"),x=l("\x1B[1;33m"),O6=l("\x1B[0;34m"),O=l("\x1B[0;36m"),k=l("\x1B[1m"),F=l("\x1B[2m"),W=l("\x1B[0m")});import{existsSync as c7}from"fs";async function t(){if(z1!==void 0)return z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return z1=K,K;let $=await D("python3.12");if($)return z1=$,$;let z=await D("python3");return z1=z,z}async function s(K,$={}){let z=await t();if(!z)return{stdout:"",stderr:"python3 not found",exitCode:127};return S([z,"-c",K],$)}var z1;var Q1=R(()=>{c()});var G0={};b(G0,{runStatus:()=>z5});import{existsSync as N,readFileSync as Z1,readdirSync as H0,statSync as W0}from"fs";import{resolve as w,basename as a7}from"path";async function r7(){if(await D("jq"))return!0;return process.stdout.write(`${E}Error: jq is required but not installed.${W}
|
|
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)
|
|
@@ -584,4 +584,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
584
584
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
585
585
|
`),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var z6=await $6(Bun.argv.slice(2));process.exit(z6);
|
|
586
586
|
|
|
587
|
-
//# debugId=
|
|
587
|
+
//# debugId=B8BA461C484BD8E464756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/lsp_proxy.py
CHANGED
|
@@ -43,6 +43,7 @@ import importlib.util
|
|
|
43
43
|
import json
|
|
44
44
|
import logging
|
|
45
45
|
import os
|
|
46
|
+
import queue
|
|
46
47
|
import shutil
|
|
47
48
|
import signal
|
|
48
49
|
import site
|
|
@@ -249,13 +250,54 @@ class LSPClient:
|
|
|
249
250
|
self._opened_uris: set = set()
|
|
250
251
|
self._lock = threading.Lock()
|
|
251
252
|
self._initialized = False
|
|
253
|
+
# v7.7.14 LSP regression fix (was broken since v7.7.0):
|
|
254
|
+
# publishDiagnostics notifications were dropped by request()'s
|
|
255
|
+
# busy-read loop. Now a dedicated reader thread (spawned at end
|
|
256
|
+
# of start()) owns proc.stdout, routes responses to per-request
|
|
257
|
+
# Queues, and routes `textDocument/publishDiagnostics` into
|
|
258
|
+
# `pending_diagnostics`. See docs/plans/UT2-6-LSP-DIAGNOSTIC-
|
|
259
|
+
# BROADCAST.md section 3 for the prior root-cause analysis.
|
|
260
|
+
self.pending_diagnostics: Dict[str, List[Dict[str, Any]]] = {}
|
|
261
|
+
self._response_queues: Dict[int, "queue.Queue[Dict[str, Any]]"] = {}
|
|
262
|
+
self._response_lock = threading.Lock()
|
|
263
|
+
self._reader_thread: Optional[threading.Thread] = None
|
|
264
|
+
self._reader_stop = threading.Event()
|
|
252
265
|
|
|
253
266
|
def start(self) -> None:
|
|
254
267
|
"""Spawn the subprocess and perform the LSP `initialize` +
|
|
255
268
|
`initialized` handshake. Idempotent: re-calling start() on an
|
|
256
|
-
already-initialized client is a no-op.
|
|
269
|
+
already-initialized client is a no-op. If the subprocess died,
|
|
270
|
+
re-spawn cleanly: stop and join the previous reader thread first
|
|
271
|
+
to avoid leaking threads against a dead pipe.
|
|
272
|
+
"""
|
|
257
273
|
if self._initialized and self.proc and self.proc.poll() is None:
|
|
258
274
|
return
|
|
275
|
+
# v7.7.14 (Opus 2 council fix): re-spawn after crash must not leak
|
|
276
|
+
# the previous reader thread. Signal stop + join with timeout, then
|
|
277
|
+
# reset routing state so the new reader starts clean.
|
|
278
|
+
if self._reader_thread and self._reader_thread.is_alive():
|
|
279
|
+
self._reader_stop.set()
|
|
280
|
+
# Close stale stdout to unblock the reader's _read_lsp() if any
|
|
281
|
+
try:
|
|
282
|
+
if self.proc and self.proc.stdout:
|
|
283
|
+
self.proc.stdout.close()
|
|
284
|
+
except OSError:
|
|
285
|
+
pass
|
|
286
|
+
self._reader_thread.join(timeout=1.0)
|
|
287
|
+
self._reader_thread = None
|
|
288
|
+
# Drain any pending response waiters from the previous incarnation;
|
|
289
|
+
# they would otherwise hang for the full request() timeout.
|
|
290
|
+
with self._response_lock:
|
|
291
|
+
for waiter in self._response_queues.values():
|
|
292
|
+
try:
|
|
293
|
+
waiter.put_nowait({'error': {'message': 'LSP restarted; request abandoned'}})
|
|
294
|
+
except queue.Full:
|
|
295
|
+
pass
|
|
296
|
+
self._response_queues.clear()
|
|
297
|
+
with self._lock:
|
|
298
|
+
self.pending_diagnostics.clear()
|
|
299
|
+
self._opened_uris.clear()
|
|
300
|
+
self._initialized = False
|
|
259
301
|
cmd = [self.binary_path] + self.extra_args
|
|
260
302
|
self.proc = subprocess.Popen(
|
|
261
303
|
cmd,
|
|
@@ -299,8 +341,89 @@ class LSPClient:
|
|
|
299
341
|
'params': {},
|
|
300
342
|
})
|
|
301
343
|
self._initialized = True
|
|
344
|
+
# v7.7.14 fix: spawn the notification-reader thread AFTER the
|
|
345
|
+
# synchronous initialize handshake completes. From this point on,
|
|
346
|
+
# all reads from proc.stdout go through `_reader_loop`; request()
|
|
347
|
+
# parks on a per-request Queue keyed by request id.
|
|
348
|
+
self._reader_stop.clear()
|
|
349
|
+
self._reader_thread = threading.Thread(
|
|
350
|
+
target=self._reader_loop,
|
|
351
|
+
name=f"lsp-reader-{self.language}",
|
|
352
|
+
daemon=True,
|
|
353
|
+
)
|
|
354
|
+
self._reader_thread.start()
|
|
302
355
|
_record_pid_to_disk(self.language, self.proc.pid)
|
|
303
356
|
|
|
357
|
+
def _reader_loop(self) -> None:
|
|
358
|
+
"""v7.7.14 fix: dedicated reader thread that owns proc.stdout.
|
|
359
|
+
Routes JSON-RPC responses to per-request Queues keyed by id;
|
|
360
|
+
routes `textDocument/publishDiagnostics` notifications into
|
|
361
|
+
`self.pending_diagnostics`. Exits cleanly on EOF or stop signal.
|
|
362
|
+
|
|
363
|
+
v7.7.14 council fix (Opus 2): on exit, drain pending request
|
|
364
|
+
waiters with an error sentinel so they fail fast instead of
|
|
365
|
+
hanging for the full request() timeout. Log the exit reason so
|
|
366
|
+
a silently-dead reader does not silently break the whole proxy.
|
|
367
|
+
"""
|
|
368
|
+
exit_reason = "stop signal"
|
|
369
|
+
try:
|
|
370
|
+
while not self._reader_stop.is_set():
|
|
371
|
+
try:
|
|
372
|
+
if not self.proc or not self.proc.stdout:
|
|
373
|
+
exit_reason = "proc/stdout missing"
|
|
374
|
+
break
|
|
375
|
+
msg = _read_lsp(self.proc.stdout)
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
exit_reason = f"read exception: {type(exc).__name__}: {exc}"
|
|
378
|
+
break
|
|
379
|
+
if msg is None:
|
|
380
|
+
exit_reason = "EOF (subprocess closed stdout)"
|
|
381
|
+
break
|
|
382
|
+
msg_id = msg.get('id')
|
|
383
|
+
method = msg.get('method')
|
|
384
|
+
if msg_id is not None and method is None:
|
|
385
|
+
# Response to a prior request: hand off to waiter
|
|
386
|
+
with self._response_lock:
|
|
387
|
+
waiter = self._response_queues.get(msg_id)
|
|
388
|
+
if waiter is not None:
|
|
389
|
+
try:
|
|
390
|
+
waiter.put_nowait(msg)
|
|
391
|
+
except queue.Full:
|
|
392
|
+
pass
|
|
393
|
+
elif method == 'textDocument/publishDiagnostics':
|
|
394
|
+
params = msg.get('params') or {}
|
|
395
|
+
uri = params.get('uri')
|
|
396
|
+
diags = params.get('diagnostics') or []
|
|
397
|
+
if uri:
|
|
398
|
+
with self._lock:
|
|
399
|
+
self.pending_diagnostics[uri] = diags
|
|
400
|
+
# Other notifications (window/logMessage, $/progress, etc.)
|
|
401
|
+
# silently ignored. Server-to-client requests are not handled
|
|
402
|
+
# (we declared no capabilities, so servers should not send any).
|
|
403
|
+
finally:
|
|
404
|
+
# Drain any outstanding request waiters with an error sentinel.
|
|
405
|
+
# Without this, request() callers hang the full timeout (5s+) on
|
|
406
|
+
# reader death; downstream tools surface as silent slow paths.
|
|
407
|
+
with self._response_lock:
|
|
408
|
+
waiters = list(self._response_queues.items())
|
|
409
|
+
self._response_queues.clear()
|
|
410
|
+
for rid, waiter in waiters:
|
|
411
|
+
try:
|
|
412
|
+
waiter.put_nowait({'error': {
|
|
413
|
+
'message': f'LSP reader thread exited: {exit_reason}',
|
|
414
|
+
'request_id': rid,
|
|
415
|
+
}})
|
|
416
|
+
except queue.Full:
|
|
417
|
+
pass
|
|
418
|
+
try:
|
|
419
|
+
logger.warning(
|
|
420
|
+
"LSP reader thread for language=%s exited: %s "
|
|
421
|
+
"(drained %d pending waiters)",
|
|
422
|
+
self.language, exit_reason, len(waiters),
|
|
423
|
+
)
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
304
427
|
def _next_request_id(self) -> int:
|
|
305
428
|
rid = self._next_id
|
|
306
429
|
self._next_id += 1
|
|
@@ -335,29 +458,41 @@ class LSPClient:
|
|
|
335
458
|
self._opened_uris.add(uri)
|
|
336
459
|
|
|
337
460
|
def request(self, method: str, params: Dict[str, Any], timeout: float = 5.0) -> Dict[str, Any]:
|
|
338
|
-
"""Send a JSON-RPC request and block until its response
|
|
339
|
-
|
|
340
|
-
|
|
461
|
+
"""Send a JSON-RPC request and block until its response arrives.
|
|
462
|
+
|
|
463
|
+
v7.7.14: parks on a per-request Queue instead of reading stdout
|
|
464
|
+
directly (which would race the reader thread). The reader thread
|
|
465
|
+
spawned in start() routes responses to this Queue by request id.
|
|
466
|
+
"""
|
|
341
467
|
rid = self._next_request_id()
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return
|
|
355
|
-
|
|
356
|
-
|
|
468
|
+
wait_q: "queue.Queue[Dict[str, Any]]" = queue.Queue(maxsize=1)
|
|
469
|
+
with self._response_lock:
|
|
470
|
+
self._response_queues[rid] = wait_q
|
|
471
|
+
try:
|
|
472
|
+
try:
|
|
473
|
+
_write_lsp(self.proc.stdin, {
|
|
474
|
+
'jsonrpc': '2.0',
|
|
475
|
+
'id': rid,
|
|
476
|
+
'method': method,
|
|
477
|
+
'params': params,
|
|
478
|
+
})
|
|
479
|
+
except (BrokenPipeError, OSError) as exc:
|
|
480
|
+
return {'error': {'message': f'LSP I/O failure on write: {exc}'}}
|
|
481
|
+
try:
|
|
482
|
+
return wait_q.get(timeout=timeout)
|
|
483
|
+
except queue.Empty:
|
|
484
|
+
return {'error': {'message': f'LSP timeout after {timeout}s'}}
|
|
485
|
+
finally:
|
|
486
|
+
with self._response_lock:
|
|
487
|
+
self._response_queues.pop(rid, None)
|
|
357
488
|
|
|
358
489
|
def shutdown(self) -> None:
|
|
359
490
|
"""Send `shutdown` + `exit`, then SIGTERM after a 2s grace and
|
|
360
491
|
SIGKILL after another 1s if still alive."""
|
|
492
|
+
# v7.7.14: signal the reader thread to stop. It will also exit on
|
|
493
|
+
# its own once proc.stdout closes (EOF), but the flag is belt-and-
|
|
494
|
+
# suspenders for the kill path below.
|
|
495
|
+
self._reader_stop.set()
|
|
361
496
|
if not self.proc or self.proc.poll() is not None:
|
|
362
497
|
return
|
|
363
498
|
try:
|
|
@@ -859,21 +994,20 @@ async def lsp_get_diagnostics(file: str) -> str:
|
|
|
859
994
|
client.did_open(abs_file)
|
|
860
995
|
except (BrokenPipeError, OSError) as exc:
|
|
861
996
|
return json.dumps({'error': f'LSP I/O failure on didOpen: {exc}', 'language': language})
|
|
862
|
-
#
|
|
863
|
-
#
|
|
864
|
-
#
|
|
865
|
-
#
|
|
997
|
+
# v7.7.14: reader thread populates client.pending_diagnostics on
|
|
998
|
+
# textDocument/publishDiagnostics notifications. Poll for up to ~1s
|
|
999
|
+
# (matches docstring "waits up to 1 second"). Pyright cold-pass on
|
|
1000
|
+
# a small file lands diagnostics in 100-400ms; gopls/rust-analyzer
|
|
1001
|
+
# vary. If the buffer never fills, return empty (no false errors).
|
|
866
1002
|
diagnostics: List[Dict[str, Any]] = []
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
break
|
|
876
|
-
time.sleep(0.05)
|
|
1003
|
+
target_uri = _path_to_uri(abs_file)
|
|
1004
|
+
for _ in range(20):
|
|
1005
|
+
with client._lock:
|
|
1006
|
+
buf = client.pending_diagnostics
|
|
1007
|
+
if target_uri in buf:
|
|
1008
|
+
diagnostics = list(buf.get(target_uri) or [])
|
|
1009
|
+
break
|
|
1010
|
+
time.sleep(0.05)
|
|
877
1011
|
err_count = sum(1 for d in diagnostics if d.get('severity') == 1)
|
|
878
1012
|
warn_count = sum(1 for d in diagnostics if d.get('severity') == 2)
|
|
879
1013
|
return json.dumps({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.14",
|
|
4
4
|
"description": "Loki Mode by Autonomi. Multi-agent autonomous SDLC framework. Spec to deployed app: PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. 4 AI providers (Claude Code, OpenAI Codex, Cline, Aider). 11 quality gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|