loki-mode 7.7.28 → 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 +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +213 -47
- package/autonomy/run.sh +125 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/registry.py +34 -0
- package/dashboard/server.py +181 -0
- package/dashboard/static/index.html +152 -0
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +178 -159
- 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: 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.
|
|
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.
|
|
384
|
+
**v7.7.30 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.30
|
package/autonomy/loki
CHANGED
|
@@ -1593,12 +1593,19 @@ cmd_start() {
|
|
|
1593
1593
|
exit 1
|
|
1594
1594
|
fi
|
|
1595
1595
|
|
|
1596
|
-
# --api flag: start the dashboard API server
|
|
1596
|
+
# --api flag: start the dashboard API server before the build.
|
|
1597
|
+
# cmd_dashboard_start already daemonizes the server (nohup ... &) and runs
|
|
1598
|
+
# its own readiness probe, so run it in a contained SUBSHELL (not a
|
|
1599
|
+
# double-background) and surface the outcome. The old
|
|
1600
|
+
# `cmd_dashboard_start 2>/dev/null &` swallowed all output AND double-
|
|
1601
|
+
# backgrounded the already-daemonized server, so a port-in-use or startup
|
|
1602
|
+
# failure was invisible and the user got no dashboard and no error. The
|
|
1603
|
+
# subshell also contains cmd_dashboard_start's internal `exit 1` so a
|
|
1604
|
+
# dashboard failure can never abort the build.
|
|
1597
1605
|
if [ "${LOKI_START_API:-false}" = "true" ]; then
|
|
1598
1606
|
local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
1599
1607
|
echo -e "${GREEN}Starting dashboard API on port $dash_port...${NC}"
|
|
1600
|
-
cmd_dashboard_start
|
|
1601
|
-
sleep 2
|
|
1608
|
+
( cmd_dashboard_start ) || echo -e "${YELLOW}Dashboard API did not start (see message above); continuing the build without it.${NC}"
|
|
1602
1609
|
fi
|
|
1603
1610
|
|
|
1604
1611
|
# Phase F (v7.5.23): cross-project context discovery. Walks ONE parent
|
|
@@ -1774,22 +1781,33 @@ _stop_session_by_id() {
|
|
|
1774
1781
|
# With session_id: stops only that specific session
|
|
1775
1782
|
cmd_stop() {
|
|
1776
1783
|
local target_session=""
|
|
1784
|
+
local stop_all=false
|
|
1777
1785
|
|
|
1778
1786
|
# Parse arguments
|
|
1779
1787
|
while [[ $# -gt 0 ]]; do
|
|
1780
1788
|
case "$1" in
|
|
1781
1789
|
--help|-h)
|
|
1782
|
-
echo -e "${BOLD}loki stop${NC} - Stop running sessions (
|
|
1790
|
+
echo -e "${BOLD}loki stop${NC} - Stop running sessions (v7.7.30)"
|
|
1783
1791
|
echo ""
|
|
1784
|
-
echo "Usage: loki stop [session-id]"
|
|
1792
|
+
echo "Usage: loki stop [session-id] [--all]"
|
|
1785
1793
|
echo ""
|
|
1786
|
-
echo " loki stop Stop
|
|
1787
|
-
echo "
|
|
1788
|
-
echo "
|
|
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)."
|
|
1789
1800
|
echo ""
|
|
1790
|
-
echo "
|
|
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."
|
|
1791
1805
|
exit 0
|
|
1792
1806
|
;;
|
|
1807
|
+
--all)
|
|
1808
|
+
stop_all=true
|
|
1809
|
+
shift
|
|
1810
|
+
;;
|
|
1793
1811
|
-*)
|
|
1794
1812
|
echo -e "${RED}Unknown option: $1${NC}"
|
|
1795
1813
|
echo "Run 'loki stop --help' for usage."
|
|
@@ -1869,11 +1887,6 @@ cmd_stop() {
|
|
|
1869
1887
|
fi
|
|
1870
1888
|
done
|
|
1871
1889
|
|
|
1872
|
-
# Also kill any orphaned loki-run temp scripts (SIGTERM then SIGKILL)
|
|
1873
|
-
pkill -f "loki-run-" 2>/dev/null || true
|
|
1874
|
-
sleep 0.5
|
|
1875
|
-
pkill -9 -f "loki-run-" 2>/dev/null || true
|
|
1876
|
-
|
|
1877
1890
|
# Mark session.json as stopped (skill-invoked sessions)
|
|
1878
1891
|
# BUG-ST-008: Atomic session.json update via temp file + mv (matches run.sh)
|
|
1879
1892
|
if [ -f "$LOKI_DIR/session.json" ]; then
|
|
@@ -1893,20 +1906,80 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
1893
1906
|
" 2>/dev/null || true
|
|
1894
1907
|
fi
|
|
1895
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
|
+
|
|
1896
1925
|
# Clean up control files
|
|
1897
1926
|
rm -f "$LOKI_DIR/STOP" "$LOKI_DIR/PAUSE" "$LOKI_DIR/PAUSED.md" 2>/dev/null
|
|
1898
1927
|
|
|
1899
|
-
# Kill dashboard if running
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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
|
+
|
|
1968
|
+
local _dash_pidf
|
|
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
|
|
1972
|
+
if [ -f "$_dash_pidf" ]; then
|
|
1973
|
+
local dash_pid
|
|
1974
|
+
dash_pid=$(cat "$_dash_pidf" 2>/dev/null)
|
|
1975
|
+
if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
|
|
1976
|
+
kill "$dash_pid" 2>/dev/null || true
|
|
1977
|
+
sleep 0.5
|
|
1978
|
+
kill -9 "$dash_pid" 2>/dev/null || true
|
|
1979
|
+
fi
|
|
1980
|
+
rm -f "$_dash_pidf"
|
|
1907
1981
|
fi
|
|
1908
|
-
|
|
1909
|
-
fi
|
|
1982
|
+
done
|
|
1910
1983
|
|
|
1911
1984
|
# Kill any remaining registered processes (2s graceful window matches run.sh)
|
|
1912
1985
|
if [ -d "$LOKI_DIR/pids" ]; then
|
|
@@ -1954,6 +2027,16 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
1954
2027
|
echo "Start a session with: loki start"
|
|
1955
2028
|
fi
|
|
1956
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
|
|
1957
2040
|
}
|
|
1958
2041
|
|
|
1959
2042
|
# Kill orphaned processes from crashed sessions
|
|
@@ -2536,15 +2619,33 @@ if os.path.isfile(session_file):
|
|
|
2536
2619
|
else:
|
|
2537
2620
|
result['elapsed_time'] = 0
|
|
2538
2621
|
|
|
2539
|
-
# Dashboard URL
|
|
2540
|
-
|
|
2622
|
+
# Dashboard URL. Check both the project-local in-build dashboard and the
|
|
2623
|
+
# standalone dashboard (~/.loki/dashboard) and honor saved scheme/host/port
|
|
2624
|
+
# side-files, so --json reports the right URL regardless of which started it.
|
|
2625
|
+
_dash_candidates = [
|
|
2626
|
+
os.path.join(loki_dir, 'dashboard', 'dashboard.pid'),
|
|
2627
|
+
os.path.expanduser(os.path.join('~', '.loki', 'dashboard', 'dashboard.pid')),
|
|
2628
|
+
]
|
|
2629
|
+
dashboard_pid_file = next((p for p in _dash_candidates if os.path.isfile(p)), _dash_candidates[0])
|
|
2541
2630
|
dashboard_url = None
|
|
2542
2631
|
if os.path.isfile(dashboard_pid_file):
|
|
2543
2632
|
try:
|
|
2544
2633
|
with open(dashboard_pid_file) as f:
|
|
2545
2634
|
dpid = int(f.read().strip())
|
|
2546
2635
|
os.kill(dpid, 0)
|
|
2547
|
-
|
|
2636
|
+
_dd = os.path.dirname(dashboard_pid_file)
|
|
2637
|
+
def _side(name, default):
|
|
2638
|
+
p = os.path.join(_dd, name)
|
|
2639
|
+
try:
|
|
2640
|
+
return open(p).read().strip() if os.path.isfile(p) else default
|
|
2641
|
+
except OSError:
|
|
2642
|
+
return default
|
|
2643
|
+
_scheme = _side('scheme', 'http')
|
|
2644
|
+
_host = _side('host', '127.0.0.1')
|
|
2645
|
+
_port = _side('port', str(dashboard_port))
|
|
2646
|
+
if _host == '0.0.0.0':
|
|
2647
|
+
_host = '127.0.0.1'
|
|
2648
|
+
dashboard_url = _scheme + '://' + _host + ':' + _port + '/'
|
|
2548
2649
|
except (ProcessLookupError, PermissionError, ValueError, Exception):
|
|
2549
2650
|
pass
|
|
2550
2651
|
result['dashboard_url'] = dashboard_url
|
|
@@ -3296,11 +3397,24 @@ cmd_provider_models() {
|
|
|
3296
3397
|
done
|
|
3297
3398
|
}
|
|
3298
3399
|
|
|
3299
|
-
#
|
|
3300
|
-
|
|
3400
|
+
# Standalone dashboard server management (`loki dashboard`).
|
|
3401
|
+
# State lives at a FIXED, machine-global ~/.loki/dashboard so that
|
|
3402
|
+
# stop|status|open find the running server from ANY working directory.
|
|
3403
|
+
# (The relative ${LOKI_DIR}/dashboard path made these commands silently
|
|
3404
|
+
# fail from a different cwd than `start`, orphaning the server.) The
|
|
3405
|
+
# separate in-build dashboard started by run.sh during `loki start` stays
|
|
3406
|
+
# project-local and is unaffected by this constant.
|
|
3407
|
+
DASHBOARD_PID_DIR="${HOME}/.loki/dashboard"
|
|
3301
3408
|
DASHBOARD_PID_FILE="${DASHBOARD_PID_DIR}/dashboard.pid"
|
|
3302
3409
|
DASHBOARD_DEFAULT_PORT=57374
|
|
3303
|
-
|
|
3410
|
+
# Default bind host: 0.0.0.0 inside a container (so a -p port map reaches
|
|
3411
|
+
# the server; 127.0.0.1 would be unreachable from the host), else loopback
|
|
3412
|
+
# for host safety. Detect Docker via /.dockerenv or LOKI_SANDBOX_MODE.
|
|
3413
|
+
if [ -f /.dockerenv ] || [ "${LOKI_SANDBOX_MODE:-}" = "true" ] || [ -n "${LOKI_IN_CONTAINER:-}" ]; then
|
|
3414
|
+
DASHBOARD_DEFAULT_HOST="0.0.0.0"
|
|
3415
|
+
else
|
|
3416
|
+
DASHBOARD_DEFAULT_HOST="127.0.0.1"
|
|
3417
|
+
fi
|
|
3304
3418
|
|
|
3305
3419
|
cmd_dashboard() {
|
|
3306
3420
|
local subcommand="${1:-}"
|
|
@@ -3546,12 +3660,19 @@ cmd_dashboard_start() {
|
|
|
3546
3660
|
echo "$host" > "${DASHBOARD_PID_DIR}/host"
|
|
3547
3661
|
echo "$url_scheme" > "${DASHBOARD_PID_DIR}/scheme"
|
|
3548
3662
|
|
|
3549
|
-
# Wait for dashboard
|
|
3663
|
+
# Wait for the dashboard to become ready (up to 10 seconds). Probe the
|
|
3664
|
+
# UNAUTHENTICATED /health endpoint over the ACTUAL scheme: /api/status
|
|
3665
|
+
# 401s under LOKI_ENTERPRISE_AUTH, and the scheme is https when TLS is on,
|
|
3666
|
+
# so the old hardcoded http://.../api/status probe always failed against a
|
|
3667
|
+
# healthy TLS or auth-enabled server. Use a loopback-reachable host when
|
|
3668
|
+
# bound to 0.0.0.0, and -k to tolerate self-signed certs.
|
|
3669
|
+
local health_host="$host"; [ "$health_host" = "0.0.0.0" ] && health_host="127.0.0.1"
|
|
3670
|
+
local health_curl_opts="-sf"; [ "$url_scheme" = "https" ] && health_curl_opts="-sfk"
|
|
3550
3671
|
local health_retries=20
|
|
3551
3672
|
local health_interval=0.5
|
|
3552
3673
|
local health_ok=false
|
|
3553
3674
|
while [[ $health_retries -gt 0 ]]; do
|
|
3554
|
-
if curl
|
|
3675
|
+
if curl $health_curl_opts "${url_scheme}://${health_host}:${port}/health" >/dev/null 2>&1; then
|
|
3555
3676
|
health_ok=true
|
|
3556
3677
|
break
|
|
3557
3678
|
fi
|
|
@@ -7802,11 +7923,37 @@ cmd_logs() {
|
|
|
7802
7923
|
fi
|
|
7803
7924
|
}
|
|
7804
7925
|
|
|
7805
|
-
# API server management (delegates to unified FastAPI dashboard server)
|
|
7926
|
+
# API server management (delegates to unified FastAPI dashboard server).
|
|
7927
|
+
# Shares the SAME machine-global PID/control dir as `loki dashboard`
|
|
7928
|
+
# ($DASHBOARD_PID_DIR = ~/.loki/dashboard) so the two commands never fight
|
|
7929
|
+
# over the port and so stop/status/open are consistent across both. Parses
|
|
7930
|
+
# --host/--port, guards a busy port, computes the TLS scheme, and persists
|
|
7931
|
+
# host/port/scheme side-files like cmd_dashboard_start does.
|
|
7806
7932
|
cmd_api() {
|
|
7807
7933
|
local subcommand="${1:-help}"
|
|
7808
|
-
|
|
7809
|
-
local
|
|
7934
|
+
shift || true
|
|
7935
|
+
local port="${LOKI_DASHBOARD_PORT:-$DASHBOARD_DEFAULT_PORT}"
|
|
7936
|
+
local host="${LOKI_DASHBOARD_HOST:-$DASHBOARD_DEFAULT_HOST}"
|
|
7937
|
+
local pid_file="$DASHBOARD_PID_FILE"
|
|
7938
|
+
|
|
7939
|
+
# Parse --host/--port (the old code ignored them, so `loki serve --port X`
|
|
7940
|
+
# silently bound 57374 and `--host 0.0.0.0` was dropped).
|
|
7941
|
+
while [ $# -gt 0 ]; do
|
|
7942
|
+
case "$1" in
|
|
7943
|
+
--port) port="${2:-$port}"; shift 2 ;;
|
|
7944
|
+
--port=*) port="${1#*=}"; shift ;;
|
|
7945
|
+
--host) host="${2:-$host}"; shift 2 ;;
|
|
7946
|
+
--host=*) host="${1#*=}"; shift ;;
|
|
7947
|
+
*) shift ;;
|
|
7948
|
+
esac
|
|
7949
|
+
done
|
|
7950
|
+
|
|
7951
|
+
local scheme="http"
|
|
7952
|
+
if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
|
|
7953
|
+
scheme="https"
|
|
7954
|
+
fi
|
|
7955
|
+
# Loopback-reachable host for printed URLs when bound to 0.0.0.0.
|
|
7956
|
+
local url_host="$host"; [ "$url_host" = "0.0.0.0" ] && url_host="127.0.0.1"
|
|
7810
7957
|
|
|
7811
7958
|
case "$subcommand" in
|
|
7812
7959
|
start)
|
|
@@ -7818,12 +7965,20 @@ cmd_api() {
|
|
|
7818
7965
|
|
|
7819
7966
|
# Check if already running
|
|
7820
7967
|
if [ -f "$pid_file" ]; then
|
|
7821
|
-
local existing_pid=$(cat "$pid_file")
|
|
7822
|
-
if kill -0 "$existing_pid" 2>/dev/null; then
|
|
7968
|
+
local existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
7969
|
+
if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
7823
7970
|
echo -e "${YELLOW}Dashboard server already running (PID: $existing_pid)${NC}"
|
|
7824
|
-
echo "URL:
|
|
7971
|
+
echo "URL: ${scheme}://${url_host}:$port"
|
|
7825
7972
|
exit 0
|
|
7826
7973
|
fi
|
|
7974
|
+
rm -f "$pid_file"
|
|
7975
|
+
fi
|
|
7976
|
+
|
|
7977
|
+
# Refuse a busy port instead of writing a bogus PID.
|
|
7978
|
+
if command -v lsof &> /dev/null && lsof -i ":$port" &> /dev/null; then
|
|
7979
|
+
echo -e "${RED}Error: Port $port is already in use${NC}"
|
|
7980
|
+
echo "Stop the other server or set LOKI_DASHBOARD_PORT / --port to a free port."
|
|
7981
|
+
exit 1
|
|
7827
7982
|
fi
|
|
7828
7983
|
|
|
7829
7984
|
# Ensure dashboard Python dependencies
|
|
@@ -7833,20 +7988,24 @@ cmd_api() {
|
|
|
7833
7988
|
fi
|
|
7834
7989
|
local api_python="$DASHBOARD_PYTHON"
|
|
7835
7990
|
|
|
7836
|
-
# Start server
|
|
7837
|
-
mkdir -p "$LOKI_DIR/logs" "$
|
|
7838
|
-
local host="${LOKI_DASHBOARD_HOST:-127.0.0.1}"
|
|
7991
|
+
# Start server. Persist host/port/scheme so status/open are accurate.
|
|
7992
|
+
mkdir -p "$LOKI_DIR/logs" "$DASHBOARD_PID_DIR"
|
|
7839
7993
|
local uvicorn_args=("--host" "$host" "--port" "$port")
|
|
7840
|
-
if [
|
|
7994
|
+
if [ "$scheme" = "https" ]; then
|
|
7841
7995
|
uvicorn_args+=("--ssl-certfile" "${LOKI_TLS_CERT}" "--ssl-keyfile" "${LOKI_TLS_KEY}")
|
|
7842
7996
|
fi
|
|
7843
|
-
LOKI_DIR="$LOKI_DIR"
|
|
7997
|
+
LOKI_DIR="$LOKI_DIR" LOKI_SKILL_DIR="$SKILL_DIR" PYTHONPATH="$SKILL_DIR" \
|
|
7998
|
+
LOKI_DASHBOARD_HOST="$host" LOKI_DASHBOARD_PORT="$port" \
|
|
7999
|
+
nohup "$api_python" -m uvicorn dashboard.server:app "${uvicorn_args[@]}" > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
7844
8000
|
local new_pid=$!
|
|
7845
8001
|
echo "$new_pid" > "$pid_file"
|
|
8002
|
+
echo "$port" > "${DASHBOARD_PID_DIR}/port"
|
|
8003
|
+
echo "$host" > "${DASHBOARD_PID_DIR}/host"
|
|
8004
|
+
echo "$scheme" > "${DASHBOARD_PID_DIR}/scheme"
|
|
7846
8005
|
|
|
7847
8006
|
echo -e "${GREEN}Dashboard server started${NC}"
|
|
7848
8007
|
echo " PID: $new_pid"
|
|
7849
|
-
echo " URL:
|
|
8008
|
+
echo " URL: ${scheme}://${url_host}:$port"
|
|
7850
8009
|
echo " Logs: $LOKI_DIR/logs/api.log"
|
|
7851
8010
|
;;
|
|
7852
8011
|
|
|
@@ -7870,15 +8029,22 @@ cmd_api() {
|
|
|
7870
8029
|
if [ -f "$pid_file" ]; then
|
|
7871
8030
|
local pid=$(cat "$pid_file")
|
|
7872
8031
|
if kill -0 "$pid" 2>/dev/null; then
|
|
8032
|
+
# Prefer persisted side-files for the real bind.
|
|
8033
|
+
local s_scheme="$scheme" s_host="$url_host" s_port="$port"
|
|
8034
|
+
[ -f "${DASHBOARD_PID_DIR}/scheme" ] && s_scheme="$(cat "${DASHBOARD_PID_DIR}/scheme" 2>/dev/null)"
|
|
8035
|
+
[ -f "${DASHBOARD_PID_DIR}/host" ] && s_host="$(cat "${DASHBOARD_PID_DIR}/host" 2>/dev/null)"
|
|
8036
|
+
[ -f "${DASHBOARD_PID_DIR}/port" ] && s_port="$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)"
|
|
8037
|
+
[ "$s_host" = "0.0.0.0" ] && s_host="127.0.0.1"
|
|
7873
8038
|
echo -e "${GREEN}Dashboard server running${NC}"
|
|
7874
8039
|
echo " PID: $pid"
|
|
7875
|
-
echo " URL:
|
|
8040
|
+
echo " URL: ${s_scheme}://${s_host}:${s_port}"
|
|
7876
8041
|
|
|
7877
|
-
# Try to fetch status
|
|
8042
|
+
# Try to fetch status (best-effort; may 401 under auth)
|
|
7878
8043
|
if command -v curl &> /dev/null; then
|
|
7879
8044
|
echo ""
|
|
7880
8045
|
echo -e "${CYAN}Status:${NC}"
|
|
7881
|
-
|
|
8046
|
+
local _k=""; [ "$s_scheme" = "https" ] && _k="-k"
|
|
8047
|
+
curl -s $_k "${s_scheme}://${s_host}:${s_port}/api/status" 2>/dev/null | jq . 2>/dev/null || true
|
|
7882
8048
|
fi
|
|
7883
8049
|
else
|
|
7884
8050
|
echo -e "${YELLOW}Dashboard server not running (stale PID file)${NC}"
|
package/autonomy/run.sh
CHANGED
|
@@ -752,6 +752,117 @@ TARGET_DIR="${LOKI_TARGET_DIR:-$(pwd)}"
|
|
|
752
752
|
PARALLEL_BLOG=${LOKI_PARALLEL_BLOG:-false}
|
|
753
753
|
AUTO_MERGE=${LOKI_AUTO_MERGE:-true}
|
|
754
754
|
|
|
755
|
+
# Multi-project registry (v7.7.29): register this running project in the
|
|
756
|
+
# machine-global registry (~/.loki/dashboard/projects.json) so the dashboard
|
|
757
|
+
# can list and switch between projects running in different folders. Records
|
|
758
|
+
# the absolute path, pid, port, and status. Fully non-blocking and
|
|
759
|
+
# failure-swallowed: registry problems must never affect a build. Marked
|
|
760
|
+
# inactive again on exit via the trap below.
|
|
761
|
+
loki_register_running_project() {
|
|
762
|
+
local _status="${1:-running}"
|
|
763
|
+
[ -n "${LOKI_SKIP_PROJECT_REGISTRY:-}" ] && return 0
|
|
764
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
765
|
+
local _skill="${LOKI_SKILL_DIR:-${PROJECT_DIR:-$SCRIPT_DIR/..}}"
|
|
766
|
+
LOKI_REG_TARGET="$TARGET_DIR" LOKI_REG_SKILL="$_skill" \
|
|
767
|
+
LOKI_REG_PID="$$" LOKI_REG_PORT="${LOKI_DASHBOARD_PORT:-57374}" \
|
|
768
|
+
LOKI_REG_STATUS="$_status" \
|
|
769
|
+
python3 - <<'PYREG' >/dev/null 2>&1 || true
|
|
770
|
+
import os, sys
|
|
771
|
+
sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
|
|
772
|
+
try:
|
|
773
|
+
from dashboard import registry
|
|
774
|
+
target = os.path.abspath(os.environ["LOKI_REG_TARGET"])
|
|
775
|
+
entry = registry.register_project(target)
|
|
776
|
+
# Enrich with runtime fields the dashboard switcher uses.
|
|
777
|
+
reg = registry._load_registry()
|
|
778
|
+
pid = entry.get("id") or registry._generate_project_id(target)
|
|
779
|
+
if pid in reg.get("projects", {}):
|
|
780
|
+
reg["projects"][pid]["pid"] = int(os.environ.get("LOKI_REG_PID", "0") or 0)
|
|
781
|
+
reg["projects"][pid]["port"] = int(os.environ.get("LOKI_REG_PORT", "57374") or 57374)
|
|
782
|
+
reg["projects"][pid]["status"] = os.environ.get("LOKI_REG_STATUS", "running")
|
|
783
|
+
registry._save_registry(reg)
|
|
784
|
+
except Exception:
|
|
785
|
+
pass
|
|
786
|
+
PYREG
|
|
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
|
+
}
|
|
858
|
+
# Register as running now. We deliberately do NOT install an EXIT trap to
|
|
859
|
+
# flip it to idle: a top-level EXIT trap here would be clobbered by the
|
|
860
|
+
# lock-release EXIT trap installed later in the main path (and could
|
|
861
|
+
# interfere with it). Instead the dashboard determines live vs stale by
|
|
862
|
+
# checking whether the recorded pid is still alive (registry stores pid),
|
|
863
|
+
# which is robust even on hard kills where a trap would never fire.
|
|
864
|
+
loki_register_running_project running
|
|
865
|
+
|
|
755
866
|
# Complexity Tiers (Auto-Claude pattern)
|
|
756
867
|
# auto = detect from PRD/codebase, simple = 3 phases, standard = 6 phases, complex = 8 phases
|
|
757
868
|
COMPLEXITY_TIER=${LOKI_COMPLEXITY:-auto}
|
|
@@ -12233,6 +12344,13 @@ cleanup() {
|
|
|
12233
12344
|
app_runner_cleanup
|
|
12234
12345
|
fi
|
|
12235
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
|
|
12236
12354
|
kill_all_registered
|
|
12237
12355
|
rm -f "$loki_dir/loki.pid" 2>/dev/null
|
|
12238
12356
|
# Clean up per-session PID file if running with session ID
|
|
@@ -12274,6 +12392,13 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
12274
12392
|
app_runner_cleanup
|
|
12275
12393
|
fi
|
|
12276
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
|
|
12277
12402
|
kill_all_registered
|
|
12278
12403
|
rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
|
|
12279
12404
|
# UT2-13: Clear cli-provider marker on session end.
|
package/dashboard/__init__.py
CHANGED
package/dashboard/registry.py
CHANGED
|
@@ -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.
|