loki-mode 7.7.11 → 7.7.13
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/autonomy/loki +22 -4
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/plans/UT2-6-LSP-DIAGNOSTIC-BROADCAST.md +382 -0
- package/loki-ts/dist/loki.js +37 -3
- package/mcp/__init__.py +1 -1
- 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.13
|
|
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.13 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.13
|
package/autonomy/loki
CHANGED
|
@@ -1373,8 +1373,20 @@ cmd_start() {
|
|
|
1373
1373
|
# No PRD file specified -- warn and confirm before starting
|
|
1374
1374
|
# Auto-confirm in CI environments or when LOKI_AUTO_CONFIRM is set
|
|
1375
1375
|
# LOKI_AUTO_CONFIRM takes precedence when explicitly set;
|
|
1376
|
-
# fall back to CI env var only when LOKI_AUTO_CONFIRM is unset
|
|
1376
|
+
# fall back to CI env var only when LOKI_AUTO_CONFIRM is unset.
|
|
1377
|
+
# v7.7.13 fix (user-reported Docker bug): also auto-confirm when stdin
|
|
1378
|
+
# is not a TTY. Without this, `docker run --rm asklokesh/loki-mode
|
|
1379
|
+
# start` showed the prompt then exited because the user didn't pass
|
|
1380
|
+
# `-it` to docker, so stdin was closed and `read` returned EOF with
|
|
1381
|
+
# an empty string; the script kept going but the user only saw the
|
|
1382
|
+
# prompt before the next bug fired. Now non-TTY callers get a clean
|
|
1383
|
+
# auto-confirm path identical to CI.
|
|
1377
1384
|
local _auto_confirm="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
|
|
1385
|
+
if [[ "$_auto_confirm" != "true" ]] && [ ! -t 0 ]; then
|
|
1386
|
+
_auto_confirm="true"
|
|
1387
|
+
echo -e "${YELLOW}Warning: No PRD file specified, stdin not a TTY (e.g. docker run without -it). Auto-confirming.${NC}"
|
|
1388
|
+
echo -e "${YELLOW}Tip: pass docker run -it ... or set LOKI_AUTO_CONFIRM=true to suppress this warning.${NC}"
|
|
1389
|
+
fi
|
|
1378
1390
|
if [[ "$_auto_confirm" == "true" ]]; then
|
|
1379
1391
|
echo -e "${YELLOW}Warning: No PRD file specified. Auto-confirming (CI mode).${NC}"
|
|
1380
1392
|
else
|
|
@@ -1448,7 +1460,8 @@ cmd_start() {
|
|
|
1448
1460
|
# Load memory context before sandbox start (errors don't block startup)
|
|
1449
1461
|
load_memory_context "$prd_file" || true
|
|
1450
1462
|
emit_event session cli start "provider=$effective_provider" "sandbox=true" "prd_path=${prd_file:-}"
|
|
1451
|
-
|
|
1463
|
+
# v7.7.13 fix: safe empty-array expansion (see line 1551 comment).
|
|
1464
|
+
exec "$SANDBOX_SH" start ${args[@]+"${args[@]}"}
|
|
1452
1465
|
fi
|
|
1453
1466
|
|
|
1454
1467
|
# Load memory context before starting (errors don't block startup)
|
|
@@ -1548,7 +1561,11 @@ cmd_start() {
|
|
|
1548
1561
|
loki_project_graph_discover "$_pg_target" || true
|
|
1549
1562
|
fi
|
|
1550
1563
|
|
|
1551
|
-
|
|
1564
|
+
# v7.7.13 fix (user-reported bug): `"${args[@]}"` triggers "unbound
|
|
1565
|
+
# variable" under bash 3.2 (macOS default) + `set -u` when args is empty.
|
|
1566
|
+
# User report: `loki start` with no PRD on /Users/lokesh/git/anonima
|
|
1567
|
+
# crashed at this line. Safe expansion guards against empty arrays.
|
|
1568
|
+
exec "$RUN_SH" ${args[@]+"${args[@]}"}
|
|
1552
1569
|
}
|
|
1553
1570
|
|
|
1554
1571
|
# Check if session is running
|
|
@@ -11294,7 +11311,8 @@ cmd_trigger() {
|
|
|
11294
11311
|
[[ -n "$secret" ]] && args+=("--secret" "$secret")
|
|
11295
11312
|
[[ -n "$dry_run_flag" ]] && args+=("--dry-run")
|
|
11296
11313
|
mkdir -p .loki/triggers
|
|
11297
|
-
|
|
11314
|
+
# v7.7.13 fix: safe empty-array expansion (bash 3.2 + set -u).
|
|
11315
|
+
nohup python3 "$trigger_server_script" ${args[@]+"${args[@]}"} \
|
|
11298
11316
|
> .loki/triggers/server.log 2>&1 &
|
|
11299
11317
|
local server_pid=$!
|
|
11300
11318
|
echo "$server_pid" > "$pid_file"
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# UT2-6: Multi-agent LSP Diagnostic Broadcast
|
|
2
|
+
|
|
3
|
+
Status: draft plan, not scheduled for v7.7.x
|
|
4
|
+
Author: planning session, 2026-05-24
|
|
5
|
+
Supersedes acceptance criterion #3 of the v7.7.0 LSP grounding work
|
|
6
|
+
Owner: TBD
|
|
7
|
+
|
|
8
|
+
## 1. Problem statement
|
|
9
|
+
|
|
10
|
+
Loki v7.7.0 shipped LSP grounding via `mcp/lsp_proxy.py`. Each MCP server instance lazily spawns per-language LSP processes (`pyright-langserver`, `typescript-language-server`, `gopls`, `rust-analyzer`, `jdtls`) on first tool call. When Loki runs in parallel mode (`skills/parallel-workflows.md`), each worktree gets its own Claude session, which spawns its own MCP server instance, which spawns its own LSP pool. Two failure modes follow:
|
|
11
|
+
|
|
12
|
+
1. **Cold-start tax**: N worktrees x M languages = N*M LSP spawns. Pyright on a real TS project is 3-6 s cold; multiplied by 5 worktrees that is 15-30 s of wall clock and ~250 MB RSS per process duplicated.
|
|
13
|
+
2. **Diagnostic blindness**: when worktree-A discovers via `lsp_get_diagnostics` that `foo.ts` now has a type error after its edit, worktrees B/C/D never see that signal. If B is planning an overlapping edit on `foo.ts`, B will compute against a stale model and may introduce conflicting changes that surface only at merge time.
|
|
14
|
+
|
|
15
|
+
The v7.7.0 CHANGELOG explicitly defers this:
|
|
16
|
+
|
|
17
|
+
> Multi-agent diagnostic broadcast (acceptance #3): we share one LSP client per language per process; two agents in PARALLEL WORKTREES get their own process pool. Cross-worktree broadcast deferred to v7.7.1.
|
|
18
|
+
|
|
19
|
+
This document is the planning input for that deferred work. **It does not need to ship in v7.7.x.**
|
|
20
|
+
|
|
21
|
+
## 2. Design pivot: broadcast first, pool later
|
|
22
|
+
|
|
23
|
+
The task framing puts "shared LSP pool" (1 process for N worktrees) on equal footing with "diagnostic broadcast." This plan splits them and downgrades pooling to a follow-on. Reasoning:
|
|
24
|
+
|
|
25
|
+
- Worktrees are different working copies. Two worktrees of a TypeScript project diverge in:
|
|
26
|
+
- On-disk source (the whole point of worktrees).
|
|
27
|
+
- `node_modules/` if `npm install` was run per worktree (common; see `spawn-parallel.sh` in `parallel-workflows.md` line 360).
|
|
28
|
+
- Active `tsconfig.json` references (a feature branch can edit tsconfig).
|
|
29
|
+
- Per-worktree generated files (`.next/`, `dist/`, etc.).
|
|
30
|
+
- LSP servers maintain per-workspace in-memory module graphs. Multi-root workspaces (`workspaceFolders` in the LSP `initialize` params) exist, but pyright and typescript-language-server treat each root as semi-independent; switching the active root mid-flight or running concurrent edits across roots is not a tested path and surfaces server-specific quirks.
|
|
31
|
+
- The win the requirement actually asks for is "agent B is notified to re-plan when A's edit produced a new diagnostic." That is achievable without process consolidation. Broadcasting from N processes to a shared bus solves the correctness problem; pool consolidation is a separate cold-start optimization.
|
|
32
|
+
|
|
33
|
+
So v1 keeps the N-process model and adds a publish/subscribe channel. v2 (out of scope here) considers process consolidation behind a feature flag, with measured trade-offs.
|
|
34
|
+
|
|
35
|
+
## 3. Prerequisite: fix the orphan `pending_diagnostics` reference
|
|
36
|
+
|
|
37
|
+
`lsp_proxy.py` line 867 (in `lsp_get_diagnostics`) reads:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
if hasattr(client, 'pending_diagnostics'):
|
|
41
|
+
target_uri = _path_to_uri(abs_file)
|
|
42
|
+
for _ in range(5):
|
|
43
|
+
with getattr(client, '_lock', threading.Lock()):
|
|
44
|
+
buf = getattr(client, 'pending_diagnostics', {})
|
|
45
|
+
...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The class `LSPClient` has no `pending_diagnostics` attribute and no notification-reader thread. `LSPClient.request()` reads messages in a busy loop until it sees the response with the matching `id`, dropping all notifications it sees along the way (including `textDocument/publishDiagnostics`). So `lsp_get_diagnostics` today always returns an empty diagnostics array on every call. **This is a latent bug that any broadcast implementation must fix as a prerequisite**, because the diagnostics the bus will broadcast come from those exact notifications.
|
|
49
|
+
|
|
50
|
+
Required change to `LSPClient`:
|
|
51
|
+
- Spawn a single notification-reader thread per client at end of `start()`.
|
|
52
|
+
- The reader owns `proc.stdout`. `request()` no longer reads from stdout directly; instead it parks on a per-request `threading.Event` (or `Queue`) keyed by request id, and the reader thread routes responses by id and routes notifications by method.
|
|
53
|
+
- `publishDiagnostics` notifications populate `self.pending_diagnostics: Dict[str, List[Diagnostic]]` keyed by URI, and also fire a registered callback (the broadcast hook, see section 5).
|
|
54
|
+
- Locking: notification reads and response routing share a single `threading.Lock`; the request-side `Event.wait(timeout)` handles flow control.
|
|
55
|
+
|
|
56
|
+
This is mostly a refactor of the existing send/receive code; no new dependencies. ~150 LOC delta in `lsp_proxy.py`.
|
|
57
|
+
|
|
58
|
+
## 4. Architecture overview
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
+-----------+ +-----------+ +-----------+
|
|
62
|
+
| worktree-A| | worktree-B| | worktree-C|
|
|
63
|
+
| | | | | |
|
|
64
|
+
| Claude | | Claude | | Claude |
|
|
65
|
+
| MCP svr | | MCP svr | | MCP svr |
|
|
66
|
+
| lsp-pxy | | lsp-pxy | | lsp-pxy |
|
|
67
|
+
| +pyright | | +pyright | | +pyright |
|
|
68
|
+
+----+------+ +----+------+ +----+------+
|
|
69
|
+
| publish | publish | publish
|
|
70
|
+
v v v
|
|
71
|
+
+--------------------+
|
|
72
|
+
| shared .loki/events/lsp/ | (or main worktree's .loki/)
|
|
73
|
+
+----------+---------+
|
|
74
|
+
| subscribe
|
|
75
|
+
+----------------+----------------+
|
|
76
|
+
v v v
|
|
77
|
+
worktree-A worktree-B worktree-C
|
|
78
|
+
(subscriber) (subscriber) (subscriber)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Key points:
|
|
82
|
+
- Each worktree still runs its own LSP processes (no pooling in v1).
|
|
83
|
+
- Publishing target is a shared `.loki/events/lsp/` directory resolved at MCP startup. Resolution rule: walk up from CWD looking for a `.loki/parallel-root` marker file (written by `spawn-parallel.sh`); if found, use that directory's `.loki/events/lsp/`; else use local `.loki/events/lsp/` (single-worktree behavior degrades to a noop bus that still works).
|
|
84
|
+
- Reuses `events/bus.py` semantics (file-based, processed-id dedup, lockfile-based atomic writes, cross-language compatible) but with a dedicated subdirectory so LSP events do not pollute the existing pending/archive flow that the dashboard consumes.
|
|
85
|
+
|
|
86
|
+
## 5. Component design
|
|
87
|
+
|
|
88
|
+
### 5.1 New module: `mcp/lsp_broadcast.py`
|
|
89
|
+
|
|
90
|
+
Responsibilities:
|
|
91
|
+
1. Resolve the shared bus root.
|
|
92
|
+
2. Publish diagnostic events from LSP notifications.
|
|
93
|
+
3. Provide a subscriber MCP tool `lsp_subscribe_diagnostics` for agents.
|
|
94
|
+
4. Provide a one-shot poll tool `lsp_recent_diagnostics(since=...)` for stateless agents.
|
|
95
|
+
5. Maintain a per-process subscriber state file so an agent that disconnects and reconnects resumes from `last_event_id`.
|
|
96
|
+
|
|
97
|
+
Pseudocode sketch:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# mcp/lsp_broadcast.py
|
|
101
|
+
|
|
102
|
+
DIAG_EVENT_DIR = ".loki/events/lsp" # under shared root
|
|
103
|
+
DIAG_JSONL = "diagnostics.jsonl" # append-only log, primary channel
|
|
104
|
+
DIAG_INDEX = "index.json" # {last_event_id, last_offset}
|
|
105
|
+
PARALLEL_MARKER = ".loki/parallel-root"
|
|
106
|
+
|
|
107
|
+
def resolve_shared_root(cwd: str) -> Path:
|
|
108
|
+
# Walk up until we find PARALLEL_MARKER or hit fs root / first .git.
|
|
109
|
+
# Marker file contains the absolute path of the shared root.
|
|
110
|
+
# Fallback: local .loki.
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
class DiagnosticPublisher:
|
|
114
|
+
def __init__(self, workspace_id: str, root: Path):
|
|
115
|
+
self.workspace_id = workspace_id # = worktree basename + git branch hash
|
|
116
|
+
self.path = root / DIAG_EVENT_DIR / DIAG_JSONL
|
|
117
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
def publish(self, uri: str, diagnostics: list[dict]) -> None:
|
|
120
|
+
# Called from LSPClient notification thread. Must be lock-free
|
|
121
|
+
# against subscribers; uses POSIX O_APPEND semantics (atomic for
|
|
122
|
+
# writes < PIPE_BUF, 4096 on linux/macos). Each line < 4 KB.
|
|
123
|
+
for d in diagnostics:
|
|
124
|
+
evt = {
|
|
125
|
+
"event_id": uuid4().hex[:12],
|
|
126
|
+
"ts": utcnow_iso(),
|
|
127
|
+
"workspace_id": self.workspace_id,
|
|
128
|
+
"uri": uri,
|
|
129
|
+
"file_relpath": relpath_from_workspace(uri),
|
|
130
|
+
"severity": d.get("severity"), # LSP: 1=error 2=warn 3=info 4=hint
|
|
131
|
+
"range": d.get("range"),
|
|
132
|
+
"message": d.get("message", "")[:512],
|
|
133
|
+
"source": d.get("source"),
|
|
134
|
+
"msg_hash": sha256(d.get("message",""))[:8],
|
|
135
|
+
}
|
|
136
|
+
line = json.dumps(evt, separators=(",", ":")) + "\n"
|
|
137
|
+
with open(self.path, "a", encoding="utf-8") as f:
|
|
138
|
+
f.write(line)
|
|
139
|
+
# Truncation policy: see section 7.2.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Subscriber MCP tool:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
@mcp.tool()
|
|
146
|
+
async def lsp_subscribe_diagnostics(
|
|
147
|
+
file_globs: list[str] | None = None,
|
|
148
|
+
severity_min: int = 2,
|
|
149
|
+
since_event_id: str | None = None,
|
|
150
|
+
timeout_s: float = 5.0,
|
|
151
|
+
max_events: int = 50,
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Return diagnostic events from peer worktrees since the given id.
|
|
154
|
+
|
|
155
|
+
Blocks up to timeout_s if no new events match. Returns ordered by
|
|
156
|
+
event_id (which is monotonic per-file via the JSONL byte offset).
|
|
157
|
+
"""
|
|
158
|
+
...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Optional follow-on tool `lsp_recent_diagnostics(seconds=60)` for one-shot polling without the cursor pattern.
|
|
162
|
+
|
|
163
|
+
### 5.2 Wiring into `LSPClient`
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# in mcp/lsp_proxy.py, inside LSPClient
|
|
167
|
+
def _on_notification(self, method: str, params: dict) -> None:
|
|
168
|
+
if method == "textDocument/publishDiagnostics":
|
|
169
|
+
uri = params.get("uri")
|
|
170
|
+
diags = params.get("diagnostics") or []
|
|
171
|
+
# Local buffer (fixes the section 3 orphan):
|
|
172
|
+
with self._lock:
|
|
173
|
+
self.pending_diagnostics[uri] = diags
|
|
174
|
+
# Broadcast hook (no-op if disabled):
|
|
175
|
+
if _broadcast_publisher is not None:
|
|
176
|
+
_broadcast_publisher.publish(uri, diags)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`_broadcast_publisher` is initialized lazily on first tool call when the env var `LOKI_LSP_BROADCAST=1` is set (see section 8).
|
|
180
|
+
|
|
181
|
+
### 5.3 Workspace identity
|
|
182
|
+
|
|
183
|
+
`workspace_id` must be stable across the process lifetime and distinguish worktrees on disk:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
workspace_id = sha1(abspath(worktree_root))[:10]
|
|
187
|
+
worktree_root = first ancestor with .git file (worktree pointer) or .git dir
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
This avoids leaking absolute paths into the event log while letting subscribers map back via a small local cache.
|
|
191
|
+
|
|
192
|
+
### 5.4 File-relative path normalization
|
|
193
|
+
|
|
194
|
+
Each event carries `file_relpath` resolved against the **publishing worktree's** root. A subscriber that wants to react to "the same logical file" applies the path against its own worktree root. This is correct for the common case (worktrees of the same repo); it breaks if worktrees rename a file or restructure directories (rare; documented as a known limitation in section 10).
|
|
195
|
+
|
|
196
|
+
## 6. Dedup and re-plan signal
|
|
197
|
+
|
|
198
|
+
The brief's requirement: agent A edits `foo.ts` and produces a new diagnostic; agent B was about to do an overlapping edit; B needs to be notified to re-plan.
|
|
199
|
+
|
|
200
|
+
### 6.1 Dedup key
|
|
201
|
+
|
|
202
|
+
Diagnostic identity for dedup:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
key = (workspace_id, file_relpath, range.start.line, range.start.character, severity, msg_hash)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This is intentionally stable across LSP republishes of the same diagnostic on every keystroke. The publisher applies it: if the last event for the same key (within the same workspace_id) had identical `range.end` and `message`, the publish is suppressed. Implementation: a small per-publisher LRU keyed by `(file_relpath, key)` of size 256.
|
|
209
|
+
|
|
210
|
+
Cross-workspace dedup is intentionally *not* applied. If both A and B independently produce the same diagnostic for the same file, both events ship; the subscriber decides whether to coalesce.
|
|
211
|
+
|
|
212
|
+
### 6.2 Subscriber-side overlap detection
|
|
213
|
+
|
|
214
|
+
This is what gives B the "re-plan" signal. Pseudocode for a planner-tier agent's inner loop:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
before each tool_use that edits file F at range R:
|
|
218
|
+
events = lsp_subscribe_diagnostics(
|
|
219
|
+
file_globs=[F],
|
|
220
|
+
since_event_id=my_cursor,
|
|
221
|
+
timeout_s=0.2, # cheap poll, not block
|
|
222
|
+
)
|
|
223
|
+
for e in events:
|
|
224
|
+
if e.workspace_id == self.workspace_id:
|
|
225
|
+
continue # my own edit echoing back
|
|
226
|
+
if ranges_overlap(R, e.range) and e.severity == 1:
|
|
227
|
+
yield Replan(reason=f"peer worktree {e.workspace_id} produced error at {e.range}")
|
|
228
|
+
my_cursor = events[-1].event_id if events else my_cursor
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The MCP tool returns events; whether the agent's prompt instructs it to actually re-plan is a system-prompt change, scoped out of this design doc (it belongs in the v7.7.0 acceptance #5 follow-up).
|
|
232
|
+
|
|
233
|
+
### 6.3 Cursor persistence
|
|
234
|
+
|
|
235
|
+
Subscribers persist `my_cursor` to `.loki/state/lsp-cursor-{agent_id}.json` so a session restart resumes correctly. The publisher rotates JSONL files at 5 MB and keeps the last 3; an over-stale cursor that points into a rotated file gets a `{"cursor_reset": true, "events": [...]}` response telling the agent it lost continuity and should re-baseline (call `lsp_get_diagnostics` for its files of interest).
|
|
236
|
+
|
|
237
|
+
## 7. Failure modes
|
|
238
|
+
|
|
239
|
+
### 7.1 LSP process crashes
|
|
240
|
+
|
|
241
|
+
Symptom: `LSPClient.proc.poll() is not None` in `_get_or_spawn_client`. Today the code re-spawns on next call. With broadcast: also publish a synthetic event `{"event_type": "lsp_down", "language": ..., "workspace_id": ...}` so peer worktrees know diagnostics from this workspace are stale until the next `lsp_up` event. Cleanup: on re-spawn success, publish `lsp_up`. Subscribers that filter by severity will not see these events unless they explicitly opt in via a separate event type.
|
|
242
|
+
|
|
243
|
+
### 7.2 Broadcast channel backs up
|
|
244
|
+
|
|
245
|
+
The JSONL grows unboundedly if no one consumes. Mitigations:
|
|
246
|
+
- **Size-based rotation**: at 5 MB, rename to `diagnostics.jsonl.1`, shift 1 to 2, drop 3. Implemented in the publisher, guarded by a short-held lockfile (`diagnostics.jsonl.rotlock`) to prevent two publishers rotating simultaneously.
|
|
247
|
+
- **Time-based GC**: on each publish, with probability 1/1000, scan `.loki/events/lsp/diagnostics.jsonl.*` and delete files older than 24 h.
|
|
248
|
+
- **Per-publisher rate cap**: each `DiagnosticPublisher` enforces less than or equal to 50 events/second; excess gets batched with a 100 ms timer and a single coalesced event per (file, severity).
|
|
249
|
+
- **Subscriber backpressure**: subscribers reading via `lsp_subscribe_diagnostics` get at most `max_events` per call; if a subscriber is very slow, the rotation may eat its tail. The `cursor_reset` mechanism in section 6.3 handles this gracefully.
|
|
250
|
+
|
|
251
|
+
### 7.3 Agent times out waiting for events
|
|
252
|
+
|
|
253
|
+
`lsp_subscribe_diagnostics(timeout_s=5.0)` returns an empty list on timeout. No error. The subscriber polls again next loop iteration. This is the same pattern as `events/bus.py` `subscribe()`.
|
|
254
|
+
|
|
255
|
+
### 7.4 Disk full / EIO on publish
|
|
256
|
+
|
|
257
|
+
Publisher catches `OSError` from `open`/`write` and logs to stderr, dropping the event. Diagnostics being best-effort is acceptable; the worst case is "agent B does its overlapping edit and we catch the conflict at merge time," which is the current behavior anyway.
|
|
258
|
+
|
|
259
|
+
### 7.5 Concurrent writers and reader-during-write
|
|
260
|
+
|
|
261
|
+
Append-only writes of less than PIPE_BUF (4 KB on Linux/macOS) are atomic per POSIX. Each event line is capped at ~1 KB by the 512-char message truncation. Readers use line-buffered `for line in f:` with a recovery loop: if `json.loads(line)` raises, log and skip the line, advance the cursor past the partial line.
|
|
262
|
+
|
|
263
|
+
### 7.6 Shared root resolution disagrees across worktrees
|
|
264
|
+
|
|
265
|
+
If worktree-A finds the parallel marker but worktree-B does not (e.g., spawn script didn't copy `.loki/parallel-root` into B), they publish to different directories and never see each other. Mitigation: `loki doctor` validates `.loki/parallel-root` exists in every worktree in the orchestrator's tracked set; warns on mismatch. Documented in `skills/parallel-workflows.md` change.
|
|
266
|
+
|
|
267
|
+
### 7.7 Symlink and case-sensitivity quirks across worktrees
|
|
268
|
+
|
|
269
|
+
macOS case-insensitive default filesystems can produce different `file_relpath` casing for `Foo.ts` vs `foo.ts`. Normalize via `os.path.normcase` at publish time and document the assumption that subscribers run on the same machine (broadcast is single-host in v1).
|
|
270
|
+
|
|
271
|
+
## 8. Backward compatibility
|
|
272
|
+
|
|
273
|
+
Default: **opt-in via env var `LOKI_LSP_BROADCAST=1` for v7.7.x, default-on starting v7.8.x**.
|
|
274
|
+
|
|
275
|
+
Rationale:
|
|
276
|
+
- Today's parallel-mode users do not expect cross-worktree diagnostic flow; turning it on by default could change behavior in ways they cannot diagnose (e.g., agent B's "why did you re-plan?" trace points to an event from a different worktree).
|
|
277
|
+
- The opt-in flag is set by `spawn-parallel.sh` automatically when it provisions worktrees, so users running parallel mode via the documented entry point get it for free.
|
|
278
|
+
- Single-worktree users see no change; the publisher is initialized only when the env var is set, and `lsp_subscribe_diagnostics` returns an empty list with a clean diagnostic message if broadcast is off.
|
|
279
|
+
|
|
280
|
+
The MCP tool surface is additive: `lsp_subscribe_diagnostics` and `lsp_recent_diagnostics` are new; no existing tool semantics change. The orphan `pending_diagnostics` fix (section 3) is a bug fix and ships as a patch regardless of the broadcast flag.
|
|
281
|
+
|
|
282
|
+
Migration note for the changelog: any agent prompt that says "use lsp_get_diagnostics to check for errors" continues to work; the new subscribe tool is additive for parallel-mode agents.
|
|
283
|
+
|
|
284
|
+
## 9. Test plan (no $30 multi-agent runs)
|
|
285
|
+
|
|
286
|
+
Three layers, all runnable locally and in CI.
|
|
287
|
+
|
|
288
|
+
### 9.1 Unit tests: `tests/test_lsp_broadcast.py` (pytest)
|
|
289
|
+
|
|
290
|
+
Coverage:
|
|
291
|
+
- `DiagnosticPublisher.publish` dedup LRU: emit same diagnostic twice, assert one event line written.
|
|
292
|
+
- Rotation: publish events until file exceeds 5 MB, assert `diagnostics.jsonl.1` exists and primary file is reset.
|
|
293
|
+
- Cursor reset: subscriber with a cursor pointing into a rotated file gets `cursor_reset=True`.
|
|
294
|
+
- Workspace id stability: same `worktree_root` produces same id; different roots produce different ids.
|
|
295
|
+
- Race: 5 threads each calling `publish` 100 times; assert all 500 events present and parseable.
|
|
296
|
+
|
|
297
|
+
These tests use a **fake LSPClient** that exposes a `inject_notification(method, params)` method. No real LSP binary is spawned.
|
|
298
|
+
|
|
299
|
+
### 9.2 Subprocess harness: `tests/test-lsp-broadcast.sh` (bash)
|
|
300
|
+
|
|
301
|
+
Coverage:
|
|
302
|
+
- Spawns 5 background `python3 -m mcp.lsp_proxy` processes pointing at the same shared root (a temp dir).
|
|
303
|
+
- Each fake-publishes diagnostics on a 100 ms interval for 5 s using a small helper script that imports `DiagnosticPublisher` directly.
|
|
304
|
+
- A 6th process subscribes and asserts it sees events from all 5 workspace_ids.
|
|
305
|
+
- Kills one publisher mid-stream; asserts a `lsp_down` event arrives.
|
|
306
|
+
- Total runtime: less than 15 s. No Claude API calls. No real LSP.
|
|
307
|
+
|
|
308
|
+
This is the test that would have caught the v7.7.0 acceptance #3 gap, and it costs $0.
|
|
309
|
+
|
|
310
|
+
### 9.3 Integration test: one worktree, real pyright, two subscriber processes
|
|
311
|
+
|
|
312
|
+
Smallest realistic end-to-end: a fixture project with a single .py file that has a type error. One publisher process spawns pyright via `LSPClient`, opens the file, waits for diagnostics. Two subscriber processes call `lsp_subscribe_diagnostics(file_globs=["*.py"])` and assert they each see the pyright diagnostic. Validates the section 3 prerequisite (notification reader) end-to-end.
|
|
313
|
+
|
|
314
|
+
CI cost: ~$0 (no Claude). Wall time: ~10 s (mostly pyright cold start).
|
|
315
|
+
|
|
316
|
+
### 9.4 What we deliberately do not test in CI
|
|
317
|
+
|
|
318
|
+
- Five real Claude sessions in five worktrees. Documented as a "release manual test" with a fixture PRD that has a known overlapping-edit hazard between two features. Run before any release that touches the broadcast code, recorded in `tests/manual/lsp-broadcast-real-agents.md`. Cost gate: only run when the broadcast code changed in the release window.
|
|
319
|
+
|
|
320
|
+
## 10. What won't work / scoped out
|
|
321
|
+
|
|
322
|
+
Honest list of things this plan deliberately does not solve:
|
|
323
|
+
|
|
324
|
+
1. **One LSP process serving N worktrees.** The task framed this as item 1; this plan defers it. To do this correctly requires:
|
|
325
|
+
- Investigating per-server `workspaceFolders` behavior in pyright / tsls / gopls / rust-analyzer / jdtls under concurrent edits across roots.
|
|
326
|
+
- A path-rewriting layer that maps tool args from worktree-relative to a canonical root.
|
|
327
|
+
- Solving the divergent-dependency-tree problem (different `node_modules` per worktree). For TypeScript this likely requires running one tsls per `tsconfig.json` root anyway, so the savings are smaller than they look.
|
|
328
|
+
- Recommended only if profiling shows cold-start is a real bottleneck; in practice the LSP processes are long-lived and the cold-start cost amortizes across a session.
|
|
329
|
+
|
|
330
|
+
2. **Cross-machine broadcast.** Single-host only. Multi-host parallel workflows are not a documented use case; if they become one, replace the file bus with a real pub/sub (NATS, Redis) behind the same `DiagnosticPublisher` interface.
|
|
331
|
+
|
|
332
|
+
3. **Cross-language diagnostic correlation.** A TS edit that should trigger a Python type check does not happen through LSP; out of scope. If we want this, it lives in a higher-level agent prompt, not the LSP layer.
|
|
333
|
+
|
|
334
|
+
4. **Worktree file-rename handling.** A diagnostic published as `src/foo.ts` from worktree-A is delivered as `src/foo.ts` to worktree-B even if B has renamed it to `src/bar.ts`. Documented limitation; the subscriber can detect "this file does not exist in my worktree" and discard.
|
|
335
|
+
|
|
336
|
+
5. **The orphan `pending_diagnostics` code path.** Fixing it is a *prerequisite* (section 3), not a goal of this design. If it ships standalone as a patch before broadcast lands, that is strictly better than today's silent-empty-array behavior.
|
|
337
|
+
|
|
338
|
+
6. **Replacing `events/bus.py` with a "real" event bus.** Tempting but out of scope. The existing bus is good enough for diagnostic events at the rates we expect (less than or equal to 50/s per publisher x less than or equal to 5 publishers = 250/s, well under what a JSONL append handles).
|
|
339
|
+
|
|
340
|
+
7. **Notifying about edits that don't yet appear as diagnostics.** "B is about to edit foo.ts" requires a separate pre-flight intent signal (`.loki/signals/EDIT_INTENT_*` per `parallel-workflows.md` section "Inter-Stream Communication"). Out of scope; the subscriber's `ranges_overlap` check covers only the post-edit-diagnostic case.
|
|
341
|
+
|
|
342
|
+
## 11. File-level work breakdown
|
|
343
|
+
|
|
344
|
+
New files:
|
|
345
|
+
- `mcp/lsp_broadcast.py` -- publisher, subscriber MCP tools, shared-root resolver. ~250 LOC.
|
|
346
|
+
- `tests/test_lsp_broadcast.py` -- unit tests with fake LSPClient. ~200 LOC.
|
|
347
|
+
- `tests/test-lsp-broadcast.sh` -- 5-publisher subprocess harness. ~80 LOC.
|
|
348
|
+
- `tests/manual/lsp-broadcast-real-agents.md` -- manual release test runbook. ~60 LOC.
|
|
349
|
+
|
|
350
|
+
Modified files:
|
|
351
|
+
- `mcp/lsp_proxy.py`:
|
|
352
|
+
- Add notification-reader thread to `LSPClient` (~150 LOC delta).
|
|
353
|
+
- Populate `self.pending_diagnostics` (fixes section 3 orphan).
|
|
354
|
+
- Wire optional `_broadcast_publisher` hook in `_on_notification`.
|
|
355
|
+
- Register the two new MCP tools by importing from `mcp/lsp_broadcast.py`.
|
|
356
|
+
- `events/bus.py`:
|
|
357
|
+
- Document that `.loki/events/lsp/` is reserved for LSP diagnostics and is not consumed by the standard `pending/` flow (or move LSP under its own subtype if we later want dashboard surfacing).
|
|
358
|
+
- `autonomy/lib/mcp-config.sh`:
|
|
359
|
+
- When `LOKI_LSP_BROADCAST=1` is in env at config-generation time, add the env var to the lsp-proxy server entry so it propagates to the spawned MCP process.
|
|
360
|
+
- `skills/parallel-workflows.md`:
|
|
361
|
+
- New section "Cross-worktree LSP diagnostics" describing the contract, the env var, the `.loki/parallel-root` marker, and the subscriber pattern.
|
|
362
|
+
- `CHANGELOG.md`:
|
|
363
|
+
- Closes v7.7.0 acceptance criterion #3.
|
|
364
|
+
|
|
365
|
+
## 12. Open questions for the future planning session
|
|
366
|
+
|
|
367
|
+
1. Should `workspace_id` include the git branch name? Argument for: easier debugging. Argument against: leaks branch names into events; subscribers should filter by relpath, not branch.
|
|
368
|
+
2. Should we surface broadcast events to the dashboard? Probably yes (a small "diagnostics from peer worktrees" panel) but that is a v7.8.x concern.
|
|
369
|
+
3. Should `lsp_subscribe_diagnostics` use long-poll or SSE-style streaming? Long-poll is simpler and matches the existing event bus pattern; streaming requires MCP transport changes. Recommend long-poll for v1.
|
|
370
|
+
4. Should the publisher emit a heartbeat event every N seconds so subscribers can detect a silent publisher death (vs. "no diagnostics to publish")? Probably yes, 30 s interval, severity=info, file_relpath=null.
|
|
371
|
+
5. Are we OK with the silent-skip philosophy if `.loki/parallel-root` is missing? `loki doctor` will warn but the tool itself returns empty. Consistent with the project's "no env var, no flag, no per-language config" philosophy from v7.7.0.
|
|
372
|
+
|
|
373
|
+
## 13. Acceptance criteria for this work
|
|
374
|
+
|
|
375
|
+
When this plan is implemented, the following should be true:
|
|
376
|
+
|
|
377
|
+
1. With `LOKI_LSP_BROADCAST=1` set in two worktrees of the same repo, an edit to `foo.ts` in worktree-A that produces a type error appears as an event in worktree-B's `lsp_subscribe_diagnostics` response within 1 second.
|
|
378
|
+
2. The unit test suite in `tests/test_lsp_broadcast.py` runs in less than 5 s and passes 20+ assertions covering publish, dedup, rotation, cursor reset, and concurrent writers.
|
|
379
|
+
3. The subprocess harness `tests/test-lsp-broadcast.sh` runs 5 publishers + 1 subscriber and asserts cross-workspace event delivery, in less than 15 s.
|
|
380
|
+
4. `lsp_get_diagnostics` returns non-empty diagnostics arrays for a file with real errors (closes the section 3 prerequisite bug).
|
|
381
|
+
5. `loki doctor` reports "LSP broadcast: enabled (shared root: ...)" or "LSP broadcast: disabled (set LOKI_LSP_BROADCAST=1)" in the Integrations block.
|
|
382
|
+
6. With the flag off, behavior is byte-identical to v7.7.0 (verified by running the existing `tests/test-lsp-proxy.sh` unchanged).
|
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.13";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)
|
|
@@ -136,6 +136,37 @@ else:
|
|
|
136
136
|
result['iteration'] = 0
|
|
137
137
|
|
|
138
138
|
# Provider + provider_source (v7.7.2 B-5 clarity, parity with bash)
|
|
139
|
+
# v7.7.12 (UT2-13 parity fix): read cli-provider marker FIRST so the bun
|
|
140
|
+
# route reports provider_source='cli' identically to bash route. Without
|
|
141
|
+
# this, --provider flag works for state but status --json silently
|
|
142
|
+
# downgrades the source field, breaking parity.
|
|
143
|
+
def _read_cli_provider_bun(loki_dir):
|
|
144
|
+
cli_file = os.path.join(loki_dir, 'state', 'cli-provider')
|
|
145
|
+
if not os.path.isfile(cli_file):
|
|
146
|
+
return None
|
|
147
|
+
try:
|
|
148
|
+
content = open(cli_file).read().strip()
|
|
149
|
+
parts = content.split(':')
|
|
150
|
+
if len(parts) < 2:
|
|
151
|
+
return None
|
|
152
|
+
prov = parts[0]
|
|
153
|
+
ts = int(parts[1])
|
|
154
|
+
if prov not in ('claude', 'codex', 'cline', 'aider'):
|
|
155
|
+
return None
|
|
156
|
+
if time.time() - ts > 86400:
|
|
157
|
+
return None
|
|
158
|
+
if len(parts) >= 3:
|
|
159
|
+
try:
|
|
160
|
+
pid = int(parts[2])
|
|
161
|
+
if pid > 0:
|
|
162
|
+
os.kill(pid, 0)
|
|
163
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
164
|
+
return None
|
|
165
|
+
return prov
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
cli_prov = _read_cli_provider_bun(loki_dir)
|
|
139
170
|
provider_file = os.path.join(loki_dir, 'state', 'provider')
|
|
140
171
|
if os.path.isfile(provider_file):
|
|
141
172
|
try:
|
|
@@ -146,7 +177,10 @@ if os.path.isfile(provider_file):
|
|
|
146
177
|
else:
|
|
147
178
|
saved = ''
|
|
148
179
|
env_set = os.environ.get('LOKI_PROVIDER', '') != ''
|
|
149
|
-
if
|
|
180
|
+
if cli_prov:
|
|
181
|
+
result['provider'] = cli_prov
|
|
182
|
+
result['provider_source'] = 'cli'
|
|
183
|
+
elif saved:
|
|
150
184
|
result['provider'] = saved
|
|
151
185
|
result['provider_source'] = 'saved'
|
|
152
186
|
elif env_set:
|
|
@@ -550,4 +584,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
550
584
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
551
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);
|
|
552
586
|
|
|
553
|
-
//# debugId=
|
|
587
|
+
//# debugId=4CE70B20232D2D2D64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.13",
|
|
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",
|