loki-mode 7.7.29 → 7.7.31

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/README.md CHANGED
@@ -38,6 +38,26 @@
38
38
 
39
39
  ## Get Started in 30 Seconds
40
40
 
41
+ **Prerequisites**
42
+
43
+ Loki drives a coding agent CLI and orchestrates real builds, so it needs a few tools on your PATH. `loki doctor` checks all of these and tells you what is missing.
44
+
45
+ Required:
46
+
47
+ - An agent provider CLI (at least one): [Claude Code](https://docs.claude.com/en/docs/claude-code) (`claude`, Tier 1, recommended), or OpenAI Codex CLI (`codex`), Cline, or Aider.
48
+ - Python 3.10+ (`python3`) for the dashboard, memory system, and orchestration helpers.
49
+ - Git 2.x (`git`) for checkpoints and worktrees.
50
+ - `curl` for installation and network calls.
51
+
52
+ Recommended:
53
+
54
+ - Bun 1.3.0+ (`bun`) for the fast runtime (the recommended install path below installs it).
55
+ - Node.js 18+ and npm if you install via npm instead of Bun.
56
+ - `jq` for nicer JSON handling in shell flows.
57
+ - Docker if you want Loki's App Runner to run containerized projects, or to run Loki itself from the published image.
58
+
59
+ You also need credentials for whichever provider you use (for Claude Code, an authenticated `claude` login or `ANTHROPIC_API_KEY`).
60
+
41
61
  **Recommended (Bun, fastest):**
42
62
 
43
63
  ```bash
@@ -66,7 +86,7 @@ loki quick "build a landing page with a signup form"
66
86
  |--------|---------|-------|
67
87
  | **Bun (recommended)** | `bun install -g loki-mode` | Fastest. v8 will be Bun-only. |
68
88
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` | Auto-installs Bun as a dep |
69
- | **Docker** | `docker pull asklokesh/loki-mode:7.5.11 && docker run --rm asklokesh/loki-mode:7.5.11 start prd.md` | Bun pre-installed in image |
89
+ | **Docker** | `docker pull asklokesh/loki-mode:7.7.31 && docker run --rm asklokesh/loki-mode:7.7.31 start prd.md` | Bun pre-installed in image |
70
90
  | **npm (compat)** | `npm install -g loki-mode` | Works without Bun (bash fallback). Migrate any time with `loki self-update --to bun`. |
71
91
 
72
92
  **Upgrading:**
@@ -85,12 +105,12 @@ See the [Installation Guide](docs/INSTALLATION.md) for the long form.
85
105
 
86
106
  ## Runtime Architecture
87
107
 
88
- Loki Mode is in the middle of a phased migration from a Bash-based runtime to a TypeScript/Bun runtime. The work is happening on the `feat/bun-migration` branch and is being shipped incrementally.
108
+ Loki Mode is in a phased migration from a Bash-based runtime to a TypeScript/Bun runtime. The migration has merged to `main` and ships incrementally with each release.
89
109
 
90
110
  **What ships today:**
91
111
 
92
- - A small set of read-only commands is routed to the Bun runtime when `bun` is on `PATH`. The router lives in `bin/loki` and currently routes: `version`, `--version`, `-v`, `status`, `stats`, `doctor`, `provider` (covers `provider show` and `provider list`), `memory` (covers `memory list` and `memory index`).
93
- - Every other command continues to execute on the existing Bash CLI (`autonomy/loki`).
112
+ - Commands routed to the Bun runtime when `bun` is on `PATH` (the router lives in `bin/loki`): `version`, `--version`, `-v`, `status`, `stats`, `doctor`, `provider` (covers `provider show` and `provider list`), `memory` (covers `memory list` and `memory index`), `rollback`, `kpis`, and `internal`.
113
+ - Every other command continues to execute on the existing Bash CLI (`autonomy/loki`), including the autonomous `loki start` / `loki run` loop which remains the Bash orchestrator (`autonomy/run.sh`).
94
114
  - If `bun` is not on `PATH`, the shim falls through to Bash silently. Existing users without Bun installed see no behavior change.
95
115
 
96
116
  **Rollback flag:**
@@ -126,7 +146,7 @@ The next major release sunsets the Bash runtime entirely. There is no firm calen
126
146
  | Method | Command |
127
147
  |--------|---------|
128
148
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` |
129
- | **Docker** | `docker pull asklokesh/loki-mode:7.5.11` |
149
+ | **Docker** | `docker pull asklokesh/loki-mode:7.7.31` |
130
150
  | **Inside Claude Code** | `claude --dangerously-skip-permissions` then type "Loki Mode" |
131
151
  | **Git clone** | `git clone https://github.com/asklokesh/loki-mode.git` |
132
152
 
@@ -137,7 +157,7 @@ See the full [Installation Guide](docs/INSTALLATION.md).
137
157
  <details>
138
158
  <summary><strong>Supported spec formats</strong></summary>
139
159
 
140
- A "spec" is whatever you hand `loki start`. Loki auto-detects the format and normalises it before the RARV loop. A Markdown PRD is one form of spec; the table below lists every input the v7.5.11 CLI accepts.
160
+ A "spec" is whatever you hand `loki start`. Loki auto-detects the format and normalises it before the RARV loop. A Markdown PRD is one form of spec; the table below lists every input the CLI accepts.
141
161
 
142
162
  | Format | Example | Notes |
143
163
  |--------|---------|-------|
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.31
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.31 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.7.29
1
+ 7.7.31
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
@@ -11134,13 +11204,31 @@ except Exception as exc:
11134
11204
  local exit_code=0
11135
11205
  # v7.5.12: Mark provider pipeline as active so SIGINT trap can kill it.
11136
11206
  LOKI_PROVIDER_ACTIVE=1
11207
+ # v7.7.31: authorize autonomous operation at the system-prompt tier so
11208
+ # the spawned agent does not read the user's global ~/.claude/CLAUDE.md,
11209
+ # judge it to conflict with the loki_system prompt, call AskUserQuestion,
11210
+ # and exit having done nothing. An appended system prompt outranks
11211
+ # CLAUDE.md memory (verified empirically). Default-on; opt out with
11212
+ # LOKI_AUTONOMY_OVERRIDE=off. Only added when the installed CLI supports
11213
+ # the flag and the override helper is in scope (sourced via the provider).
11214
+ # Build the claude flag list as an array. The base flags are always
11215
+ # present so the array is never empty (empty "${arr[@]}" under `set -u`
11216
+ # is an error on bash 3.2, the stock macOS shell). The autonomy override
11217
+ # is appended conditionally.
11218
+ local _loki_claude_argv=("--dangerously-skip-permissions" "--model" "$tier_param")
11219
+ if [ "${LOKI_AUTONOMY_OVERRIDE:-on}" != "off" ] \
11220
+ && type _loki_autonomy_override_text >/dev/null 2>&1 \
11221
+ && type loki_claude_flag_supported >/dev/null 2>&1 \
11222
+ && loki_claude_flag_supported "--append-system-prompt"; then
11223
+ _loki_claude_argv+=("--append-system-prompt" "$(_loki_autonomy_override_text)")
11224
+ fi
11137
11225
  case "${PROVIDER_NAME:-claude}" in
11138
11226
  claude)
11139
11227
  # Claude: Full features with stream-json output and agent tracking
11140
11228
  # Uses dynamic tier for model selection based on RARV phase
11141
11229
  # Pass tier to Python via environment for dashboard display
11142
11230
  { LOKI_CURRENT_MODEL="$tier_param" \
11143
- claude --dangerously-skip-permissions --model "$tier_param" -p "$prompt" \
11231
+ claude "${_loki_claude_argv[@]}" -p "$prompt" \
11144
11232
  --output-format stream-json --verbose 2>&1 | \
11145
11233
  tee -a "$log_file" "$agent_log" "$iter_output" | \
11146
11234
  python3 -u -c '
@@ -11878,21 +11966,34 @@ if __name__ == "__main__":
11878
11966
 
11879
11967
  log_info "Press Ctrl+C to cancel"
11880
11968
 
11881
- # Countdown with progress
11969
+ # Countdown with progress.
11970
+ # v7.7.31: the countdown now sleeps in short 1s ticks and checks the
11971
+ # STOP/PAUSE signal on every tick. Previously it slept in 10s (or 60s
11972
+ # for long waits) chunks and never read the STOP file, so a dashboard
11973
+ # Stop button or `loki stop` issued DURING the inter-iteration wait did
11974
+ # nothing for up to 60s, and a SIGTERM was deferred by bash until the
11975
+ # current sleep chunk finished. Short ticks make Stop take effect within
11976
+ # ~1s and let the SIGTERM trap fire promptly.
11882
11977
  local remaining=$wait_time
11883
- local interval=10
11884
- # Use longer interval for long waits
11885
- if [ $wait_time -gt 1800 ]; then
11886
- interval=60
11887
- fi
11888
-
11978
+ local _loki_dir_wait="${TARGET_DIR:-.}/.loki"
11979
+ local _last_shown=-1
11889
11980
  while [ $remaining -gt 0 ]; do
11890
- local human_remaining=$(format_duration $remaining)
11891
- printf "\r${YELLOW}Resuming in ${human_remaining}...${NC} "
11892
- # BUG-RUN-007: Prevent timer from overshooting into negative
11893
- local sleep_time=$((remaining < interval ? remaining : interval))
11894
- sleep $sleep_time
11895
- remaining=$((remaining - sleep_time))
11981
+ # Honor an immediate stop/pause requested during the wait (dashboard
11982
+ # Stop button, `loki stop`, or a STOP file written by any control).
11983
+ if [ -f "$_loki_dir_wait/STOP" ] || [ -f "$_loki_dir_wait/PAUSE" ]; then
11984
+ echo ""
11985
+ log_warn "Stop/pause signal detected during wait - returning to control loop"
11986
+ break
11987
+ fi
11988
+ # Refresh the human-readable countdown at most once per 10s of change
11989
+ # so we do not spam the terminal while still ticking every second.
11990
+ if [ $((remaining % 10)) -eq 0 ] || [ "$_last_shown" -ne "$remaining" ]; then
11991
+ local human_remaining=$(format_duration $remaining)
11992
+ printf "\r${YELLOW}Resuming in ${human_remaining}...${NC} "
11993
+ _last_shown=$remaining
11994
+ fi
11995
+ sleep 1
11996
+ remaining=$((remaining - 1))
11896
11997
  done
11897
11998
  echo ""
11898
11999
 
@@ -12274,6 +12375,13 @@ cleanup() {
12274
12375
  app_runner_cleanup
12275
12376
  fi
12276
12377
  stop_status_monitor
12378
+ # v7.7.30: tear down this project's dashboard contribution on a
12379
+ # deliberate STOP-file exit. stop_dashboard handles the project-local
12380
+ # dashboard (.loki/dashboard/dashboard.pid); the helper marks this
12381
+ # project stopped in the registry and kills the shared dashboard only
12382
+ # when no other project is still running.
12383
+ stop_dashboard
12384
+ loki_mark_project_stopped_and_maybe_kill_shared_dashboard
12277
12385
  kill_all_registered
12278
12386
  rm -f "$loki_dir/loki.pid" 2>/dev/null
12279
12387
  # Clean up per-session PID file if running with session ID
@@ -12315,6 +12423,13 @@ except (json.JSONDecodeError, OSError): pass
12315
12423
  app_runner_cleanup
12316
12424
  fi
12317
12425
  stop_status_monitor
12426
+ # v7.7.30: tear down this project's dashboard contribution on a
12427
+ # deliberate double-Ctrl+C exit. stop_dashboard handles the
12428
+ # project-local dashboard; the helper marks this project stopped in
12429
+ # the registry and kills the shared dashboard only when no other
12430
+ # project is still running.
12431
+ stop_dashboard
12432
+ loki_mark_project_stopped_and_maybe_kill_shared_dashboard
12318
12433
  kill_all_registered
12319
12434
  rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
12320
12435
  # 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.31"
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.
@@ -2166,7 +2166,8 @@ async def list_running_projects():
2166
2166
  path = p.get("path", "")
2167
2167
  pid = p.get("pid")
2168
2168
  running = False
2169
- if isinstance(pid, int) and pid > 0:
2169
+ has_pid = isinstance(pid, int) and pid > 0
2170
+ if has_pid:
2170
2171
  try:
2171
2172
  os.kill(pid, 0)
2172
2173
  running = True # signal 0 delivered -> pid alive
@@ -2174,8 +2175,14 @@ async def list_running_projects():
2174
2175
  running = True # pid exists but owned by another user
2175
2176
  except (ProcessLookupError, OSError):
2176
2177
  running = False # ESRCH -> dead
2177
- # A project is also "live" if its .loki/session.json says running.
2178
- if not running and path:
2178
+ # session.json is only a FALLBACK for legacy sessions with no recorded
2179
+ # pid. v7.7.31: when a pid IS recorded but dead, that pid is
2180
+ # authoritative -- the orchestrator is gone, so do NOT let a stale
2181
+ # session.json (status still "running" after a hard kill / crash) flip
2182
+ # this back to running. Otherwise the switcher shows a dead session as
2183
+ # running and the Stop button targets a dead pid. Only consult
2184
+ # session.json when no pid was ever recorded.
2185
+ if not running and not has_pid and path:
2179
2186
  try:
2180
2187
  sess = _Path(path) / ".loki" / "session.json"
2181
2188
  if sess.is_file():
@@ -2205,6 +2212,122 @@ async def list_running_projects():
2205
2212
  return {"projects": out, "active_project_dir": active}
2206
2213
 
2207
2214
 
2215
+ class RunningProjectStopRequest(BaseModel):
2216
+ """Schema for stopping a specific registered project from the switcher.
2217
+
2218
+ Exactly one of id or project_dir must be provided. id is preferred;
2219
+ project_dir is accepted for symmetry with /api/focus. The provided value
2220
+ is resolved through the dashboard registry, so an arbitrary filesystem
2221
+ path is never used directly as a write target.
2222
+ """
2223
+ id: Optional[str] = None
2224
+ project_dir: Optional[str] = None
2225
+
2226
+
2227
+ @app.post("/api/running-projects/stop", dependencies=[Depends(auth.require_scope("control"))])
2228
+ async def stop_running_project(request: Request, body: RunningProjectStopRequest):
2229
+ """Stop a specific registered project (v7.7.30 per-project switcher stop).
2230
+
2231
+ Resolves the project via the dashboard registry (by id or path), writes a
2232
+ STOP file into that project's .loki for a clean runner teardown, then runs
2233
+ the graceful SIGTERM -> poll-5s -> SIGKILL dance against the recorded
2234
+ orchestrator pid (not the dashboard's own _get_loki_dir). Marks the
2235
+ project's session.json and registry entry stopped so the switcher reflects
2236
+ it immediately.
2237
+
2238
+ Security: the STOP file is only ever written to the path already stored in
2239
+ the registry for the resolved id, never to a caller-supplied path.
2240
+ """
2241
+ if not _control_limiter.check("control"):
2242
+ raise HTTPException(status_code=429, detail="Rate limit exceeded")
2243
+
2244
+ identifier = (body.id or body.project_dir or "").strip()
2245
+ if not identifier:
2246
+ raise HTTPException(status_code=400, detail="id or project_dir is required")
2247
+
2248
+ project = registry.get_project(identifier)
2249
+ if not project:
2250
+ raise HTTPException(status_code=404, detail="project not found")
2251
+
2252
+ project_id = project.get("id")
2253
+ audit.log_event(
2254
+ action="stop",
2255
+ resource_type="session",
2256
+ details={"source": "api", "project_id": project_id},
2257
+ ip_address=request.client.host if request.client else None,
2258
+ )
2259
+
2260
+ # Validate the registry-stored path is a real dir containing .loki before
2261
+ # writing into it. Mirrors the /api/focus guard. If invalid we still mark
2262
+ # the project stopped but skip the STOP-file write.
2263
+ path = project.get("path", "")
2264
+ loki_dir = None
2265
+ if path:
2266
+ p = _Path(path)
2267
+ if p.is_dir() and (p / ".loki").is_dir():
2268
+ loki_dir = p / ".loki"
2269
+
2270
+ pid = project.get("pid")
2271
+ if not isinstance(pid, int) or pid <= 0:
2272
+ # Already not running: nothing to signal, just reconcile the registry.
2273
+ registry.mark_project_stopped(project_id)
2274
+ return {
2275
+ "success": True,
2276
+ "project_id": project_id,
2277
+ "stopped": False,
2278
+ "already_stopped": True,
2279
+ }
2280
+
2281
+ # Write the STOP file so the runner's own cleanup STOP-branch fires for a
2282
+ # clean teardown. Only into the registry-resolved .loki dir.
2283
+ if loki_dir is not None:
2284
+ try:
2285
+ (loki_dir / "STOP").write_text(datetime.now(timezone.utc).isoformat())
2286
+ except OSError:
2287
+ pass
2288
+
2289
+ # Graceful dance against the recorded orchestrator pid.
2290
+ stopped = False
2291
+ try:
2292
+ os.kill(pid, 15) # SIGTERM
2293
+ for _ in range(10):
2294
+ await asyncio.sleep(0.5)
2295
+ try:
2296
+ os.kill(pid, 0) # Check if still alive
2297
+ except OSError:
2298
+ stopped = True
2299
+ break
2300
+ if not stopped:
2301
+ try:
2302
+ os.kill(pid, 9) # SIGKILL
2303
+ stopped = True
2304
+ except (OSError, ProcessLookupError):
2305
+ stopped = True
2306
+ except (ValueError, OSError, ProcessLookupError):
2307
+ # pid already dead or unsignalable -- treat as stopped.
2308
+ stopped = True
2309
+
2310
+ # Mark session.json stopped in that project's .loki.
2311
+ if loki_dir is not None:
2312
+ session_file = loki_dir / "session.json"
2313
+ if session_file.exists():
2314
+ try:
2315
+ sd = json.loads(session_file.read_text())
2316
+ sd["status"] = "stopped"
2317
+ atomic_write_json(session_file, sd, use_lock=True)
2318
+ except Exception:
2319
+ pass
2320
+
2321
+ registry.mark_project_stopped(project_id)
2322
+
2323
+ return {
2324
+ "success": True,
2325
+ "project_id": project_id,
2326
+ "stopped": stopped,
2327
+ "already_stopped": False,
2328
+ }
2329
+
2330
+
2208
2331
  # =============================================================================
2209
2332
  # Enterprise Features (Optional - enabled via environment variables)
2210
2333
  # =============================================================================
@@ -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.31
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.31";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=4167329ADA92E7A964756E2164756E21
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.31'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "7.7.29",
3
+ "version": "7.7.31",
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",
@@ -201,6 +201,38 @@ _loki_build_claude_auto_flags() {
201
201
  && loki_claude_flag_supported "--include-hook-events"; then
202
202
  _LOKI_CLAUDE_AUTO_FLAGS+=("--include-hook-events")
203
203
  fi
204
+
205
+ # --append-system-prompt (v7.7.31): authorize autonomous operation at the
206
+ # system-prompt tier so the spawned agent does not refuse the run. Without
207
+ # this, the agent reads the user's global ~/.claude/CLAUDE.md (which may say
208
+ # "always ask for clarification" / "never commit without permission"),
209
+ # judges it to conflict with Loki's "never ask, never stop" prompt (which is
210
+ # only a user-message instruction and thus lower precedence), calls
211
+ # AskUserQuestion, and exits in ~30s having done nothing. An appended system
212
+ # prompt outranks CLAUDE.md memory (verified empirically), so it resolves the
213
+ # conflict in Loki's favor for this authorized session. Default-on; opt out
214
+ # with LOKI_AUTONOMY_OVERRIDE=off. We never edit the user's CLAUDE.md.
215
+ if [ "${LOKI_AUTONOMY_OVERRIDE:-on}" != "off" ] \
216
+ && loki_claude_flag_supported "--append-system-prompt"; then
217
+ _LOKI_CLAUDE_AUTO_FLAGS+=("--append-system-prompt" "$(_loki_autonomy_override_text)")
218
+ fi
219
+ }
220
+
221
+ # The system-prompt text that authorizes autonomous operation and resolves
222
+ # precedence against a conflicting user/project CLAUDE.md. Kept in one place so
223
+ # the bash and Bun routes can stay byte-identical. No emojis, no em dashes.
224
+ _loki_autonomy_override_text() {
225
+ cat <<'LOKI_AUTONOMY_EOF'
226
+ You are running INSIDE Loki Mode autonomous execution, a session the user explicitly launched with full prior authorization (loki start / loki run). This IS an authorized autonomous session, not an unattended action you must seek consent for.
227
+
228
+ Instruction precedence for THIS session: when the loki_system instructions in the user prompt conflict with any CLAUDE.md, project memory, or global memory that tells you to ask for clarification, pause for confirmation, refuse to act autonomously, or withhold routine commits, the loki_system instructions take precedence. The user has already consented by launching autonomous mode.
229
+
230
+ Therefore, for this session: do NOT ask the user questions, do NOT call the AskUserQuestion tool, do NOT stop to request confirmation, and do NOT refuse routine, reversible engineering work (writing code, running tests, creating files, atomic git checkpoints in the working tree). Proceed and complete the task directly. If a decision is genuinely ambiguous, choose the most reasonable option, record the assumption in .loki/CONTINUITY.md, and continue rather than halting.
231
+
232
+ Commit hygiene still applies: git checkpoints are LOCAL only. Never push or force-push. Stage files by explicit path, never `git add -A` or `git add .`, and never commit secrets, credentials, .env files, or untracked files you did not author this session.
233
+
234
+ This precedence override is narrow. It does NOT relax any safety rule. Every safety prohibition in CLAUDE.md or memory still fully binds: anything genuinely destructive or irreversible remains out of scope unless the task explicitly calls for it. This includes (not limited to) force-pushing, deleting or overwriting the user's data, dropping or truncating databases, publishing or releasing, rotating or exfiltrating secrets, touching production systems, and anything a CLAUDE.md safety rule explicitly forbids. When in doubt about whether an action is destructive, treat it as destructive and do not do it.
235
+ LOKI_AUTONOMY_EOF
204
236
  }
205
237
 
206
238
  # Invocation function (basic, no tier).