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 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.28
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.28 | [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.28
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 in background before the build
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 2>/dev/null &
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 (v6.4.0)"
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 all running sessions"
1787
- echo " loki stop 52 Stop only session #52"
1788
- 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)."
1789
1800
  echo ""
1790
- 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."
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
- if [ -f "$LOKI_DIR/dashboard/dashboard.pid" ]; then
1901
- local dash_pid
1902
- dash_pid=$(cat "$LOKI_DIR/dashboard/dashboard.pid" 2>/dev/null)
1903
- if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
1904
- kill "$dash_pid" 2>/dev/null || true
1905
- sleep 0.5
1906
- kill -9 "$dash_pid" 2>/dev/null || true
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
- rm -f "$LOKI_DIR/dashboard/dashboard.pid"
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
- dashboard_pid_file = os.path.join(loki_dir, 'dashboard', 'dashboard.pid')
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
- dashboard_url = 'http://127.0.0.1:' + dashboard_port + '/'
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
- # Dashboard server management - project-local, unified with run.sh
3300
- DASHBOARD_PID_DIR="${LOKI_DIR}/dashboard"
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
- DASHBOARD_DEFAULT_HOST="127.0.0.1"
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 HTTP endpoint to become ready (up to 10 seconds)
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 -sf "http://${host}:${port}/api/status" >/dev/null 2>&1; then
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
- local port="${LOKI_DASHBOARD_PORT:-57374}"
7809
- local pid_file="$LOKI_DIR/dashboard/dashboard.pid"
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: http://localhost:$port"
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" "$LOKI_DIR/dashboard"
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 [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
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" PYTHONPATH="$SKILL_DIR" nohup "$api_python" -m uvicorn dashboard.server:app "${uvicorn_args[@]}" > "$LOKI_DIR/logs/api.log" 2>&1 &
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: http://localhost:$port"
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: http://localhost:$port"
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
- curl -s "http://localhost:$port/api/status" 2>/dev/null | jq . 2>/dev/null || true
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.
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.7.28"
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.