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 +26 -6
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +86 -16
- package/autonomy/run.sh +129 -14
- package/dashboard/__init__.py +1 -1
- package/dashboard/registry.py +34 -0
- package/dashboard/server.py +126 -3
- package/dashboard/static/index.html +85 -0
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/providers/claude.sh +32 -0
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.
|
|
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
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
384
|
+
**v7.7.31 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
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 (
|
|
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
|
|
1794
|
-
echo "
|
|
1795
|
-
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)."
|
|
1796
1800
|
echo ""
|
|
1797
|
-
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."
|
|
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.
|
|
1907
|
-
# dashboard (.loki/dashboard)
|
|
1908
|
-
#
|
|
1909
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
11884
|
-
|
|
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
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
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.
|
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.
|
package/dashboard/server.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
2178
|
-
|
|
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
|
}
|
package/docs/INSTALLATION.md
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var 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.
|
|
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=
|
|
607
|
+
//# debugId=4167329ADA92E7A964756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.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",
|
package/providers/claude.sh
CHANGED
|
@@ -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).
|