loki-mode 7.7.29 → 7.7.30

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-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.7.29
6
+ # Loki Mode v7.7.30
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.29 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.7.30 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.7.29
1
+ 7.7.30
package/autonomy/loki CHANGED
@@ -1781,22 +1781,33 @@ _stop_session_by_id() {
1781
1781
  # With session_id: stops only that specific session
1782
1782
  cmd_stop() {
1783
1783
  local target_session=""
1784
+ local stop_all=false
1784
1785
 
1785
1786
  # Parse arguments
1786
1787
  while [[ $# -gt 0 ]]; do
1787
1788
  case "$1" in
1788
1789
  --help|-h)
1789
- echo -e "${BOLD}loki stop${NC} - Stop running sessions (v6.4.0)"
1790
+ echo -e "${BOLD}loki stop${NC} - Stop running sessions (v7.7.30)"
1790
1791
  echo ""
1791
- echo "Usage: loki stop [session-id]"
1792
+ echo "Usage: loki stop [session-id] [--all]"
1792
1793
  echo ""
1793
- echo " loki stop Stop all running sessions"
1794
- echo " loki stop 52 Stop only session #52"
1795
- echo " loki stop 54 Stop only session #54"
1794
+ echo " loki stop Stop ONLY the current folder's session."
1795
+ echo " Other folders keep running. Marks this"
1796
+ echo " project stopped in the dashboard registry."
1797
+ echo " loki stop 52 Stop only session #52 in this folder."
1798
+ echo " loki stop --all Stop EVERY loki runner on this machine"
1799
+ echo " (legacy machine-wide behavior)."
1796
1800
  echo ""
1797
- echo "Use 'loki status' to see all running sessions."
1801
+ echo "The shared dashboard stays up if other projects are still"
1802
+ echo "running; it is stopped only when no project remains (or --all)."
1803
+ echo ""
1804
+ echo "Use 'loki status' to see this folder's sessions."
1798
1805
  exit 0
1799
1806
  ;;
1807
+ --all)
1808
+ stop_all=true
1809
+ shift
1810
+ ;;
1800
1811
  -*)
1801
1812
  echo -e "${RED}Unknown option: $1${NC}"
1802
1813
  echo "Run 'loki stop --help' for usage."
@@ -1876,11 +1887,6 @@ cmd_stop() {
1876
1887
  fi
1877
1888
  done
1878
1889
 
1879
- # Also kill any orphaned loki-run temp scripts (SIGTERM then SIGKILL)
1880
- pkill -f "loki-run-" 2>/dev/null || true
1881
- sleep 0.5
1882
- pkill -9 -f "loki-run-" 2>/dev/null || true
1883
-
1884
1890
  # Mark session.json as stopped (skill-invoked sessions)
1885
1891
  # BUG-ST-008: Atomic session.json update via temp file + mv (matches run.sh)
1886
1892
  if [ -f "$LOKI_DIR/session.json" ]; then
@@ -1900,15 +1906,69 @@ except (json.JSONDecodeError, OSError): pass
1900
1906
  " 2>/dev/null || true
1901
1907
  fi
1902
1908
 
1909
+ # v7.7.30: mark THIS project stopped in the shared dashboard registry
1910
+ # so the multi-project switcher reflects it. Best-effort, never fatal.
1911
+ # Honors LOKI_SKIP_PROJECT_REGISTRY (matches run.sh registration).
1912
+ if [ -z "${LOKI_SKIP_PROJECT_REGISTRY:-}" ] && command -v python3 >/dev/null 2>&1; then
1913
+ LOKI_REG_TARGET="$(pwd)" LOKI_REG_SKILL="${SKILL_DIR:-$PROJECT_DIR}" \
1914
+ python3 - <<'PYSTOP' >/dev/null 2>&1 || true
1915
+ import os, sys
1916
+ sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
1917
+ try:
1918
+ from dashboard import registry
1919
+ registry.mark_project_stopped(os.path.abspath(os.environ["LOKI_REG_TARGET"]))
1920
+ except Exception:
1921
+ pass
1922
+ PYSTOP
1923
+ fi
1924
+
1903
1925
  # Clean up control files
1904
1926
  rm -f "$LOKI_DIR/STOP" "$LOKI_DIR/PAUSE" "$LOKI_DIR/PAUSED.md" 2>/dev/null
1905
1927
 
1906
- # Kill dashboard if running. Check BOTH the project-local in-build
1907
- # dashboard (.loki/dashboard) and the standalone one
1908
- # (~/.loki/dashboard) so cleanup never leaves an orphan regardless of
1909
- # which path started it.
1928
+ # Kill dashboard if running.
1929
+ # v7.7.30: the project-local dashboard (.loki/dashboard) belongs solely
1930
+ # to this folder and is ALWAYS killed. The shared standalone dashboard
1931
+ # (~/.loki/dashboard) is killed ONLY when no OTHER registered project is
1932
+ # still running, so we never tear down a dashboard other folders need.
1933
+ # For --all, the shared dashboard is always killed (tear everything down).
1934
+ local _kill_shared_dash=false
1935
+ if [ "$stop_all" = true ]; then
1936
+ _kill_shared_dash=true
1937
+ elif command -v python3 >/dev/null 2>&1; then
1938
+ # Query the registry (after mark_project_stopped excluded this
1939
+ # project) for any OTHER project whose pid is still alive.
1940
+ local _dash_decision
1941
+ _dash_decision=$(LOKI_REG_SKILL="${SKILL_DIR:-$PROJECT_DIR}" \
1942
+ python3 - <<'PYDASH' 2>/dev/null || true
1943
+ import os, sys
1944
+ sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
1945
+ try:
1946
+ from dashboard import registry
1947
+ alive = 0
1948
+ for p in registry.list_projects(include_inactive=True):
1949
+ pid = p.get("pid")
1950
+ if isinstance(pid, int) and pid > 0:
1951
+ try:
1952
+ os.kill(pid, 0)
1953
+ alive += 1
1954
+ except OSError:
1955
+ pass
1956
+ print("CLEAR" if alive == 0 else "KEEP")
1957
+ except Exception:
1958
+ print("CLEAR")
1959
+ PYDASH
1960
+ )
1961
+ [ "$_dash_decision" = "CLEAR" ] && _kill_shared_dash=true
1962
+ else
1963
+ # python3 unavailable: fall back to legacy behavior (kill shared)
1964
+ # to avoid leaking the shared dashboard on minimal systems.
1965
+ _kill_shared_dash=true
1966
+ fi
1967
+
1910
1968
  local _dash_pidf
1911
- for _dash_pidf in "$LOKI_DIR/dashboard/dashboard.pid" "${HOME}/.loki/dashboard/dashboard.pid"; do
1969
+ local _dash_targets=("$LOKI_DIR/dashboard/dashboard.pid")
1970
+ [ "$_kill_shared_dash" = true ] && _dash_targets+=("${HOME}/.loki/dashboard/dashboard.pid")
1971
+ for _dash_pidf in "${_dash_targets[@]}"; do
1912
1972
  if [ -f "$_dash_pidf" ]; then
1913
1973
  local dash_pid
1914
1974
  dash_pid=$(cat "$_dash_pidf" 2>/dev/null)
@@ -1967,6 +2027,16 @@ except (json.JSONDecodeError, OSError): pass
1967
2027
  echo "Start a session with: loki start"
1968
2028
  fi
1969
2029
  fi
2030
+
2031
+ # v7.7.30: --all preserves the legacy machine-wide kill. It runs even when
2032
+ # the current folder has no live session (the "clean everything" use case),
2033
+ # reaping every folder's orphaned loki-run-* temp script (SIGTERM, SIGKILL).
2034
+ if [ "$stop_all" = true ]; then
2035
+ pkill -f "loki-run-" 2>/dev/null || true
2036
+ sleep 0.5
2037
+ pkill -9 -f "loki-run-" 2>/dev/null || true
2038
+ echo -e "${RED}--all: signalled all loki-run-* processes on this machine.${NC}"
2039
+ fi
1970
2040
  }
1971
2041
 
1972
2042
  # Kill orphaned processes from crashed sessions
package/autonomy/run.sh CHANGED
@@ -785,6 +785,76 @@ except Exception:
785
785
  pass
786
786
  PYREG
787
787
  }
788
+
789
+ # v7.7.30: deliberate-exit teardown for the shared dashboard + registry.
790
+ # Marks THIS project (abspath of TARGET_DIR) stopped in the machine-global
791
+ # registry, then decides whether the shared standalone dashboard at
792
+ # ~/.loki/dashboard/dashboard.pid should be killed. The shared dashboard is
793
+ # killed ONLY when no other registered project still has a live pid (CLEAR);
794
+ # if any other project is still running (KEEP) it is left up. NEVER uses a
795
+ # blanket pkill and NEVER touches another folder's pids. Best-effort and
796
+ # failure-swallowed: teardown bookkeeping must never block a clean exit.
797
+ loki_mark_project_stopped_and_maybe_kill_shared_dashboard() {
798
+ local _skill="${LOKI_SKILL_DIR:-${PROJECT_DIR:-$SCRIPT_DIR/..}}"
799
+ local _shared_pidfile="${HOME}/.loki/dashboard/dashboard.pid"
800
+ local _decision="CLEAR"
801
+
802
+ if [ -z "${LOKI_SKIP_PROJECT_REGISTRY:-}" ] && command -v python3 >/dev/null 2>&1; then
803
+ # (a) Mark this project stopped in the shared registry.
804
+ LOKI_REG_TARGET="$TARGET_DIR" LOKI_REG_SKILL="$_skill" \
805
+ python3 - <<'PYSTOP' >/dev/null 2>&1 || true
806
+ import os, sys
807
+ sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
808
+ try:
809
+ from dashboard import registry
810
+ registry.mark_project_stopped(os.path.abspath(os.environ["LOKI_REG_TARGET"]))
811
+ except Exception:
812
+ pass
813
+ PYSTOP
814
+ # (b) CLEAR/KEEP check: any OTHER project still alive keeps the
815
+ # shared dashboard up (this project is already marked stopped above).
816
+ _decision="$(LOKI_REG_SKILL="$_skill" python3 - <<'PYCHECK' 2>/dev/null || echo CLEAR
817
+ import os, sys
818
+ sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
819
+ try:
820
+ from dashboard import registry
821
+ alive = 0
822
+ for p in registry.list_projects(include_inactive=True):
823
+ pid = p.get("pid")
824
+ if isinstance(pid, int) and pid > 0:
825
+ try:
826
+ os.kill(pid, 0)
827
+ alive += 1
828
+ except OSError:
829
+ pass
830
+ print("CLEAR" if alive == 0 else "KEEP")
831
+ except Exception:
832
+ print("CLEAR")
833
+ PYCHECK
834
+ )"
835
+ fi
836
+
837
+ # (c) Only tear down the SHARED dashboard when no other project remains
838
+ # (CLEAR), or when python3 was unavailable (legacy fallback: avoid leaking
839
+ # the shared dashboard on minimal systems).
840
+ if [ "$_decision" = "CLEAR" ]; then
841
+ if [ -f "$_shared_pidfile" ]; then
842
+ local _shared_pid
843
+ _shared_pid=$(cat "$_shared_pidfile" 2>/dev/null)
844
+ if [ -n "$_shared_pid" ]; then
845
+ kill "$_shared_pid" 2>/dev/null || true
846
+ sleep 0.5
847
+ kill -9 "$_shared_pid" 2>/dev/null || true
848
+ fi
849
+ rm -f "$_shared_pidfile" 2>/dev/null || true
850
+ fi
851
+ # (d) Defense-in-depth: reclaim the dashboard port only in the CLEAR
852
+ # case, so we never kill a shared dashboard another project owns.
853
+ if command -v lsof >/dev/null 2>&1; then
854
+ lsof -ti:"${DASHBOARD_PORT:-57374}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
855
+ fi
856
+ fi
857
+ }
788
858
  # Register as running now. We deliberately do NOT install an EXIT trap to
789
859
  # flip it to idle: a top-level EXIT trap here would be clobbered by the
790
860
  # lock-release EXIT trap installed later in the main path (and could
@@ -12274,6 +12344,13 @@ cleanup() {
12274
12344
  app_runner_cleanup
12275
12345
  fi
12276
12346
  stop_status_monitor
12347
+ # v7.7.30: tear down this project's dashboard contribution on a
12348
+ # deliberate STOP-file exit. stop_dashboard handles the project-local
12349
+ # dashboard (.loki/dashboard/dashboard.pid); the helper marks this
12350
+ # project stopped in the registry and kills the shared dashboard only
12351
+ # when no other project is still running.
12352
+ stop_dashboard
12353
+ loki_mark_project_stopped_and_maybe_kill_shared_dashboard
12277
12354
  kill_all_registered
12278
12355
  rm -f "$loki_dir/loki.pid" 2>/dev/null
12279
12356
  # Clean up per-session PID file if running with session ID
@@ -12315,6 +12392,13 @@ except (json.JSONDecodeError, OSError): pass
12315
12392
  app_runner_cleanup
12316
12393
  fi
12317
12394
  stop_status_monitor
12395
+ # v7.7.30: tear down this project's dashboard contribution on a
12396
+ # deliberate double-Ctrl+C exit. stop_dashboard handles the
12397
+ # project-local dashboard; the helper marks this project stopped in
12398
+ # the registry and kills the shared dashboard only when no other
12399
+ # project is still running.
12400
+ stop_dashboard
12401
+ loki_mark_project_stopped_and_maybe_kill_shared_dashboard
12318
12402
  kill_all_registered
12319
12403
  rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
12320
12404
  # UT2-13: Clear cli-provider marker on session end.
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.7.29"
10
+ __version__ = "7.7.30"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -189,6 +189,40 @@ def update_last_accessed(identifier: str) -> Optional[dict]:
189
189
  return None
190
190
 
191
191
 
192
+ def mark_project_stopped(identifier: str) -> Optional[dict]:
193
+ """
194
+ Mark a project's runtime status as stopped and clear its live pid.
195
+
196
+ Used when a session ends (loki stop, dashboard per-project stop, or a
197
+ graceful Ctrl+C teardown) so the multi-project switcher reflects the
198
+ project as not-running immediately, without waiting for pid-liveness to
199
+ catch up. The entry is intentionally kept (not unregistered) so the
200
+ project stays selectable and re-registers cleanly on the next loki start.
201
+
202
+ Args:
203
+ identifier: Project ID, path, or alias
204
+
205
+ Returns:
206
+ The updated project entry, or None if no matching project was found.
207
+ Idempotent: marking an already-stopped project is a no-op that still
208
+ returns the entry.
209
+ """
210
+ registry = _load_registry()
211
+
212
+ for pid_key, project in registry["projects"].items():
213
+ if (
214
+ pid_key == identifier
215
+ or project["path"] == identifier
216
+ or project.get("alias") == identifier
217
+ ):
218
+ project["status"] = "stopped"
219
+ project["pid"] = None
220
+ project["updated_at"] = datetime.now(timezone.utc).isoformat()
221
+ _save_registry(registry)
222
+ return project
223
+ return None
224
+
225
+
192
226
  def check_project_health(identifier: str) -> dict:
193
227
  """
194
228
  Check the health status of a project.
@@ -2205,6 +2205,122 @@ async def list_running_projects():
2205
2205
  return {"projects": out, "active_project_dir": active}
2206
2206
 
2207
2207
 
2208
+ class RunningProjectStopRequest(BaseModel):
2209
+ """Schema for stopping a specific registered project from the switcher.
2210
+
2211
+ Exactly one of id or project_dir must be provided. id is preferred;
2212
+ project_dir is accepted for symmetry with /api/focus. The provided value
2213
+ is resolved through the dashboard registry, so an arbitrary filesystem
2214
+ path is never used directly as a write target.
2215
+ """
2216
+ id: Optional[str] = None
2217
+ project_dir: Optional[str] = None
2218
+
2219
+
2220
+ @app.post("/api/running-projects/stop", dependencies=[Depends(auth.require_scope("control"))])
2221
+ async def stop_running_project(request: Request, body: RunningProjectStopRequest):
2222
+ """Stop a specific registered project (v7.7.30 per-project switcher stop).
2223
+
2224
+ Resolves the project via the dashboard registry (by id or path), writes a
2225
+ STOP file into that project's .loki for a clean runner teardown, then runs
2226
+ the graceful SIGTERM -> poll-5s -> SIGKILL dance against the recorded
2227
+ orchestrator pid (not the dashboard's own _get_loki_dir). Marks the
2228
+ project's session.json and registry entry stopped so the switcher reflects
2229
+ it immediately.
2230
+
2231
+ Security: the STOP file is only ever written to the path already stored in
2232
+ the registry for the resolved id, never to a caller-supplied path.
2233
+ """
2234
+ if not _control_limiter.check("control"):
2235
+ raise HTTPException(status_code=429, detail="Rate limit exceeded")
2236
+
2237
+ identifier = (body.id or body.project_dir or "").strip()
2238
+ if not identifier:
2239
+ raise HTTPException(status_code=400, detail="id or project_dir is required")
2240
+
2241
+ project = registry.get_project(identifier)
2242
+ if not project:
2243
+ raise HTTPException(status_code=404, detail="project not found")
2244
+
2245
+ project_id = project.get("id")
2246
+ audit.log_event(
2247
+ action="stop",
2248
+ resource_type="session",
2249
+ details={"source": "api", "project_id": project_id},
2250
+ ip_address=request.client.host if request.client else None,
2251
+ )
2252
+
2253
+ # Validate the registry-stored path is a real dir containing .loki before
2254
+ # writing into it. Mirrors the /api/focus guard. If invalid we still mark
2255
+ # the project stopped but skip the STOP-file write.
2256
+ path = project.get("path", "")
2257
+ loki_dir = None
2258
+ if path:
2259
+ p = _Path(path)
2260
+ if p.is_dir() and (p / ".loki").is_dir():
2261
+ loki_dir = p / ".loki"
2262
+
2263
+ pid = project.get("pid")
2264
+ if not isinstance(pid, int) or pid <= 0:
2265
+ # Already not running: nothing to signal, just reconcile the registry.
2266
+ registry.mark_project_stopped(project_id)
2267
+ return {
2268
+ "success": True,
2269
+ "project_id": project_id,
2270
+ "stopped": False,
2271
+ "already_stopped": True,
2272
+ }
2273
+
2274
+ # Write the STOP file so the runner's own cleanup STOP-branch fires for a
2275
+ # clean teardown. Only into the registry-resolved .loki dir.
2276
+ if loki_dir is not None:
2277
+ try:
2278
+ (loki_dir / "STOP").write_text(datetime.now(timezone.utc).isoformat())
2279
+ except OSError:
2280
+ pass
2281
+
2282
+ # Graceful dance against the recorded orchestrator pid.
2283
+ stopped = False
2284
+ try:
2285
+ os.kill(pid, 15) # SIGTERM
2286
+ for _ in range(10):
2287
+ await asyncio.sleep(0.5)
2288
+ try:
2289
+ os.kill(pid, 0) # Check if still alive
2290
+ except OSError:
2291
+ stopped = True
2292
+ break
2293
+ if not stopped:
2294
+ try:
2295
+ os.kill(pid, 9) # SIGKILL
2296
+ stopped = True
2297
+ except (OSError, ProcessLookupError):
2298
+ stopped = True
2299
+ except (ValueError, OSError, ProcessLookupError):
2300
+ # pid already dead or unsignalable -- treat as stopped.
2301
+ stopped = True
2302
+
2303
+ # Mark session.json stopped in that project's .loki.
2304
+ if loki_dir is not None:
2305
+ session_file = loki_dir / "session.json"
2306
+ if session_file.exists():
2307
+ try:
2308
+ sd = json.loads(session_file.read_text())
2309
+ sd["status"] = "stopped"
2310
+ atomic_write_json(session_file, sd, use_lock=True)
2311
+ except Exception:
2312
+ pass
2313
+
2314
+ registry.mark_project_stopped(project_id)
2315
+
2316
+ return {
2317
+ "success": True,
2318
+ "project_id": project_id,
2319
+ "stopped": stopped,
2320
+ "already_stopped": False,
2321
+ }
2322
+
2323
+
2208
2324
  # =============================================================================
2209
2325
  # Enterprise Features (Optional - enabled via environment variables)
2210
2326
  # =============================================================================
@@ -303,6 +303,50 @@
303
303
  border-color: var(--loki-accent);
304
304
  box-shadow: 0 0 0 3px var(--loki-accent-glow);
305
305
  }
306
+ /* v7.7.30 per-project stop list */
307
+ .project-stop-list {
308
+ display: flex;
309
+ flex-wrap: wrap;
310
+ align-items: center;
311
+ gap: 6px;
312
+ margin-left: 10px;
313
+ }
314
+ .project-stop-row {
315
+ display: inline-flex;
316
+ align-items: center;
317
+ gap: 6px;
318
+ padding: 3px 6px 3px 10px;
319
+ background: var(--loki-bg-primary);
320
+ border: 1px solid var(--loki-border);
321
+ border-radius: 14px;
322
+ font-size: 11px;
323
+ font-family: 'Inter', system-ui, sans-serif;
324
+ color: var(--loki-text-primary);
325
+ }
326
+ .project-stop-row .project-stop-name {
327
+ max-width: 160px;
328
+ overflow: hidden;
329
+ text-overflow: ellipsis;
330
+ white-space: nowrap;
331
+ }
332
+ .project-stop-row button {
333
+ padding: 2px 8px;
334
+ background: transparent;
335
+ border: 1px solid var(--loki-border);
336
+ border-radius: 10px;
337
+ font-size: 11px;
338
+ font-family: 'Inter', system-ui, sans-serif;
339
+ color: var(--loki-text-secondary);
340
+ cursor: pointer;
341
+ }
342
+ .project-stop-row button:hover:not(:disabled) {
343
+ border-color: #d64545;
344
+ color: #d64545;
345
+ }
346
+ .project-stop-row button:disabled {
347
+ opacity: 0.6;
348
+ cursor: default;
349
+ }
306
350
 
307
351
  /* Main Content */
308
352
  .main-content {
@@ -556,6 +600,11 @@
556
600
  <select class="project-switcher" id="project-switcher" title="Switch project" aria-label="Switch project">
557
601
  <option value="">All projects (current dir)</option>
558
602
  </select>
603
+ <!-- v7.7.30 per-project stop: a compact list of running projects, each
604
+ with a Stop button that gracefully halts that project's runner
605
+ without affecting any other folder. Built at runtime; empty when
606
+ no project is running. -->
607
+ <div class="project-stop-list" id="project-stop-list" aria-label="Running projects"></div>
559
608
  </div>
560
609
 
561
610
  <nav class="nav-links">
@@ -13403,6 +13452,41 @@ document.addEventListener('DOMContentLoaded', function() {
13403
13452
  (function initProjectSwitcher() {
13404
13453
  var sel = document.getElementById('project-switcher');
13405
13454
  if (!sel) return;
13455
+ var stopList = document.getElementById('project-stop-list');
13456
+ // v7.7.30: build a per-row Stop control for each running project using
13457
+ // createElement + textContent only (never innerHTML for project-supplied
13458
+ // strings), so a project name can never inject markup.
13459
+ function buildStopList(projects) {
13460
+ if (!stopList) return;
13461
+ while (stopList.firstChild) stopList.removeChild(stopList.firstChild);
13462
+ projects.forEach(function(p){
13463
+ if (p.running !== true) return;
13464
+ var row = document.createElement('div');
13465
+ row.className = 'project-stop-row';
13466
+ var name = document.createElement('span');
13467
+ name.className = 'project-stop-name';
13468
+ name.textContent = p.name || p.path || 'project';
13469
+ var btn = document.createElement('button');
13470
+ btn.type = 'button';
13471
+ btn.textContent = 'Stop';
13472
+ if (p.id) btn.setAttribute('data-id', p.id);
13473
+ btn.addEventListener('click', function(){
13474
+ if (!p.id) return;
13475
+ btn.disabled = true;
13476
+ btn.textContent = 'Stopping...';
13477
+ fetch('/api/running-projects/stop', {
13478
+ method: 'POST',
13479
+ headers: { 'Content-Type': 'application/json' },
13480
+ body: JSON.stringify({ id: p.id })
13481
+ })
13482
+ .then(function(){ refresh(); })
13483
+ .catch(function(){ btn.disabled = false; btn.textContent = 'Stop'; });
13484
+ });
13485
+ row.appendChild(name);
13486
+ row.appendChild(btn);
13487
+ stopList.appendChild(row);
13488
+ });
13489
+ }
13406
13490
  function refresh() {
13407
13491
  fetch('/api/running-projects')
13408
13492
  .then(function(r){ return r.ok ? r.json() : null; })
@@ -13423,6 +13507,7 @@ document.addEventListener('DOMContentLoaded', function() {
13423
13507
  sel.appendChild(o);
13424
13508
  });
13425
13509
  if (!data.active_project_dir && current === '') sel.value = '';
13510
+ buildStopList(data.projects);
13426
13511
  })
13427
13512
  .catch(function(){ /* offline / no endpoint: leave as-is */ });
13428
13513
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.7.29
5
+ **Version:** v7.7.30
6
6
 
7
7
  ---
8
8
 
@@ -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 v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var w=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>L,homeLokiDir:()=>k1,findRepoRootForVersion:()=>S1,REPO_ROOT:()=>p});import{resolve as u,dirname as N1}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 Q=N1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function S1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=N1($);if(X===$)break;$=X}return u(K,"..","..","..")}function L(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=w(()=>{i1=N1(L7(import.meta.url));p=E7()});import{readFileSync as w7}from"fs";import{resolve as x7,dirname as F7}from"path";import{fileURLToPath as N7}from"url";function G1(){if(o!==null)return o;let K="7.7.29";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=F7(N7(import.meta.url)),Q=S1($);o=w7(x7(Q,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var C1=w(()=>{g()});var $0={};v($0,{runOrThrow:()=>S7,run:()=>C,commandVersion:()=>C7,commandExists:()=>b,ShellError:()=>D1});async function C(K,$={}){let Q=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),X,Z;if($.timeoutMs&&$.timeoutMs>0)X=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[W,z,q]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text(),Q.exited]);return{stdout:W,stderr:z,exitCode:q}}finally{if(X)clearTimeout(X);if(Z)clearTimeout(Z)}}async function S7(K,$={}){let Q=await C(K,$);if(Q.exitCode!==0)throw new D1(`command failed (${Q.exitCode}): ${K.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function b(K){let $=k7(K),Q=await C(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.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 C7(K,$="--version"){if(!await b(K))return null;let X=await C([K,$],{timeoutMs:5000});if(X.exitCode!==0)return null;return((X.stdout||X.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var D1;var n=w(()=>{D1=class D1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,Q,X){super(K);this.message=K;this.exitCode=$;this.stdout=Q;this.stderr=X;this.name="ShellError"}}});function c(K){return D7?"":K}var D7,F,y,N,A6,A,D,S,H;var a=w(()=>{D7=(process.env.NO_COLOR??"").length>0;F=c("\x1B[0;31m"),y=c("\x1B[0;32m"),N=c("\x1B[1;33m"),A6=c("\x1B[0;34m"),A=c("\x1B[0;36m"),D=c("\x1B[1m"),S=c("\x1B[2m"),H=c("\x1B[0m")});import{existsSync as c7}from"fs";async function i(){if(Z1!==void 0)return Z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return Z1=K,K;let $=await b("python3.12");if($)return Z1=$,$;let Q=await b("python3");return Z1=Q,Q}async function s(K,$={}){let Q=await i();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return C([Q,"-c",K],$)}var Z1;var z1=w(()=>{n()});var G0={};v(G0,{runStatus:()=>X5});import{existsSync as k,readFileSync as K1,readdirSync as W0,statSync as H0}from"fs";import{resolve as R,basename as a7}from"path";import{homedir as s7}from"os";async function t7(){if(await b("jq"))return!0;return process.stdout.write(`${F}Error: jq is required but not installed.${H}
2
+ var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var w=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>L,homeLokiDir:()=>k1,findRepoRootForVersion:()=>S1,REPO_ROOT:()=>p});import{resolve as u,dirname as N1}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 Q=N1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function S1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=N1($);if(X===$)break;$=X}return u(K,"..","..","..")}function L(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=w(()=>{i1=N1(L7(import.meta.url));p=E7()});import{readFileSync as w7}from"fs";import{resolve as x7,dirname as F7}from"path";import{fileURLToPath as N7}from"url";function G1(){if(o!==null)return o;let K="7.7.30";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=F7(N7(import.meta.url)),Q=S1($);o=w7(x7(Q,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var C1=w(()=>{g()});var $0={};v($0,{runOrThrow:()=>S7,run:()=>C,commandVersion:()=>C7,commandExists:()=>b,ShellError:()=>D1});async function C(K,$={}){let Q=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),X,Z;if($.timeoutMs&&$.timeoutMs>0)X=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[W,z,q]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text(),Q.exited]);return{stdout:W,stderr:z,exitCode:q}}finally{if(X)clearTimeout(X);if(Z)clearTimeout(Z)}}async function S7(K,$={}){let Q=await C(K,$);if(Q.exitCode!==0)throw new D1(`command failed (${Q.exitCode}): ${K.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function b(K){let $=k7(K),Q=await C(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.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 C7(K,$="--version"){if(!await b(K))return null;let X=await C([K,$],{timeoutMs:5000});if(X.exitCode!==0)return null;return((X.stdout||X.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var D1;var n=w(()=>{D1=class D1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,Q,X){super(K);this.message=K;this.exitCode=$;this.stdout=Q;this.stderr=X;this.name="ShellError"}}});function c(K){return D7?"":K}var D7,F,y,N,A6,A,D,S,H;var a=w(()=>{D7=(process.env.NO_COLOR??"").length>0;F=c("\x1B[0;31m"),y=c("\x1B[0;32m"),N=c("\x1B[1;33m"),A6=c("\x1B[0;34m"),A=c("\x1B[0;36m"),D=c("\x1B[1m"),S=c("\x1B[2m"),H=c("\x1B[0m")});import{existsSync as c7}from"fs";async function i(){if(Z1!==void 0)return Z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return Z1=K,K;let $=await b("python3.12");if($)return Z1=$,$;let Q=await b("python3");return Z1=Q,Q}async function s(K,$={}){let Q=await i();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return C([Q,"-c",K],$)}var Z1;var z1=w(()=>{n()});var G0={};v(G0,{runStatus:()=>X5});import{existsSync as k,readFileSync as K1,readdirSync as W0,statSync as H0}from"fs";import{resolve as R,basename as a7}from"path";import{homedir as s7}from"os";async function t7(){if(await b("jq"))return!0;return process.stdout.write(`${F}Error: jq is required but not installed.${H}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -604,4 +604,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
604
604
  `),2}default:return process.stderr.write(`Unknown command: ${$}
605
605
  `),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var Z6=await X6(Bun.argv.slice(2));process.exit(Z6);
606
606
 
607
- //# debugId=304835BA74DB765264756E2164756E21
607
+ //# debugId=1D57DCC7E6AE757E64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.7.29'
60
+ __version__ = '7.7.30'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.7.29",
3
+ "version": "7.7.30",
4
4
  "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 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
5
5
  "keywords": [
6
6
  "agent",