loki-mode 7.7.29 → 7.7.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +86 -16
- package/autonomy/run.sh +84 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/registry.py +34 -0
- package/dashboard/server.py +116 -0
- 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/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.7.
|
|
6
|
+
# Loki Mode v7.7.30
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
-
**v7.7.
|
|
384
|
+
**v7.7.30 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.30
|
package/autonomy/loki
CHANGED
|
@@ -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
|
|
@@ -12274,6 +12344,13 @@ cleanup() {
|
|
|
12274
12344
|
app_runner_cleanup
|
|
12275
12345
|
fi
|
|
12276
12346
|
stop_status_monitor
|
|
12347
|
+
# v7.7.30: tear down this project's dashboard contribution on a
|
|
12348
|
+
# deliberate STOP-file exit. stop_dashboard handles the project-local
|
|
12349
|
+
# dashboard (.loki/dashboard/dashboard.pid); the helper marks this
|
|
12350
|
+
# project stopped in the registry and kills the shared dashboard only
|
|
12351
|
+
# when no other project is still running.
|
|
12352
|
+
stop_dashboard
|
|
12353
|
+
loki_mark_project_stopped_and_maybe_kill_shared_dashboard
|
|
12277
12354
|
kill_all_registered
|
|
12278
12355
|
rm -f "$loki_dir/loki.pid" 2>/dev/null
|
|
12279
12356
|
# Clean up per-session PID file if running with session ID
|
|
@@ -12315,6 +12392,13 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
12315
12392
|
app_runner_cleanup
|
|
12316
12393
|
fi
|
|
12317
12394
|
stop_status_monitor
|
|
12395
|
+
# v7.7.30: tear down this project's dashboard contribution on a
|
|
12396
|
+
# deliberate double-Ctrl+C exit. stop_dashboard handles the
|
|
12397
|
+
# project-local dashboard; the helper marks this project stopped in
|
|
12398
|
+
# the registry and kills the shared dashboard only when no other
|
|
12399
|
+
# project is still running.
|
|
12400
|
+
stop_dashboard
|
|
12401
|
+
loki_mark_project_stopped_and_maybe_kill_shared_dashboard
|
|
12318
12402
|
kill_all_registered
|
|
12319
12403
|
rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
|
|
12320
12404
|
# UT2-13: Clear cli-provider marker on session end.
|
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
|
@@ -2205,6 +2205,122 @@ async def list_running_projects():
|
|
|
2205
2205
|
return {"projects": out, "active_project_dir": active}
|
|
2206
2206
|
|
|
2207
2207
|
|
|
2208
|
+
class RunningProjectStopRequest(BaseModel):
|
|
2209
|
+
"""Schema for stopping a specific registered project from the switcher.
|
|
2210
|
+
|
|
2211
|
+
Exactly one of id or project_dir must be provided. id is preferred;
|
|
2212
|
+
project_dir is accepted for symmetry with /api/focus. The provided value
|
|
2213
|
+
is resolved through the dashboard registry, so an arbitrary filesystem
|
|
2214
|
+
path is never used directly as a write target.
|
|
2215
|
+
"""
|
|
2216
|
+
id: Optional[str] = None
|
|
2217
|
+
project_dir: Optional[str] = None
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
@app.post("/api/running-projects/stop", dependencies=[Depends(auth.require_scope("control"))])
|
|
2221
|
+
async def stop_running_project(request: Request, body: RunningProjectStopRequest):
|
|
2222
|
+
"""Stop a specific registered project (v7.7.30 per-project switcher stop).
|
|
2223
|
+
|
|
2224
|
+
Resolves the project via the dashboard registry (by id or path), writes a
|
|
2225
|
+
STOP file into that project's .loki for a clean runner teardown, then runs
|
|
2226
|
+
the graceful SIGTERM -> poll-5s -> SIGKILL dance against the recorded
|
|
2227
|
+
orchestrator pid (not the dashboard's own _get_loki_dir). Marks the
|
|
2228
|
+
project's session.json and registry entry stopped so the switcher reflects
|
|
2229
|
+
it immediately.
|
|
2230
|
+
|
|
2231
|
+
Security: the STOP file is only ever written to the path already stored in
|
|
2232
|
+
the registry for the resolved id, never to a caller-supplied path.
|
|
2233
|
+
"""
|
|
2234
|
+
if not _control_limiter.check("control"):
|
|
2235
|
+
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
2236
|
+
|
|
2237
|
+
identifier = (body.id or body.project_dir or "").strip()
|
|
2238
|
+
if not identifier:
|
|
2239
|
+
raise HTTPException(status_code=400, detail="id or project_dir is required")
|
|
2240
|
+
|
|
2241
|
+
project = registry.get_project(identifier)
|
|
2242
|
+
if not project:
|
|
2243
|
+
raise HTTPException(status_code=404, detail="project not found")
|
|
2244
|
+
|
|
2245
|
+
project_id = project.get("id")
|
|
2246
|
+
audit.log_event(
|
|
2247
|
+
action="stop",
|
|
2248
|
+
resource_type="session",
|
|
2249
|
+
details={"source": "api", "project_id": project_id},
|
|
2250
|
+
ip_address=request.client.host if request.client else None,
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
# Validate the registry-stored path is a real dir containing .loki before
|
|
2254
|
+
# writing into it. Mirrors the /api/focus guard. If invalid we still mark
|
|
2255
|
+
# the project stopped but skip the STOP-file write.
|
|
2256
|
+
path = project.get("path", "")
|
|
2257
|
+
loki_dir = None
|
|
2258
|
+
if path:
|
|
2259
|
+
p = _Path(path)
|
|
2260
|
+
if p.is_dir() and (p / ".loki").is_dir():
|
|
2261
|
+
loki_dir = p / ".loki"
|
|
2262
|
+
|
|
2263
|
+
pid = project.get("pid")
|
|
2264
|
+
if not isinstance(pid, int) or pid <= 0:
|
|
2265
|
+
# Already not running: nothing to signal, just reconcile the registry.
|
|
2266
|
+
registry.mark_project_stopped(project_id)
|
|
2267
|
+
return {
|
|
2268
|
+
"success": True,
|
|
2269
|
+
"project_id": project_id,
|
|
2270
|
+
"stopped": False,
|
|
2271
|
+
"already_stopped": True,
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
# Write the STOP file so the runner's own cleanup STOP-branch fires for a
|
|
2275
|
+
# clean teardown. Only into the registry-resolved .loki dir.
|
|
2276
|
+
if loki_dir is not None:
|
|
2277
|
+
try:
|
|
2278
|
+
(loki_dir / "STOP").write_text(datetime.now(timezone.utc).isoformat())
|
|
2279
|
+
except OSError:
|
|
2280
|
+
pass
|
|
2281
|
+
|
|
2282
|
+
# Graceful dance against the recorded orchestrator pid.
|
|
2283
|
+
stopped = False
|
|
2284
|
+
try:
|
|
2285
|
+
os.kill(pid, 15) # SIGTERM
|
|
2286
|
+
for _ in range(10):
|
|
2287
|
+
await asyncio.sleep(0.5)
|
|
2288
|
+
try:
|
|
2289
|
+
os.kill(pid, 0) # Check if still alive
|
|
2290
|
+
except OSError:
|
|
2291
|
+
stopped = True
|
|
2292
|
+
break
|
|
2293
|
+
if not stopped:
|
|
2294
|
+
try:
|
|
2295
|
+
os.kill(pid, 9) # SIGKILL
|
|
2296
|
+
stopped = True
|
|
2297
|
+
except (OSError, ProcessLookupError):
|
|
2298
|
+
stopped = True
|
|
2299
|
+
except (ValueError, OSError, ProcessLookupError):
|
|
2300
|
+
# pid already dead or unsignalable -- treat as stopped.
|
|
2301
|
+
stopped = True
|
|
2302
|
+
|
|
2303
|
+
# Mark session.json stopped in that project's .loki.
|
|
2304
|
+
if loki_dir is not None:
|
|
2305
|
+
session_file = loki_dir / "session.json"
|
|
2306
|
+
if session_file.exists():
|
|
2307
|
+
try:
|
|
2308
|
+
sd = json.loads(session_file.read_text())
|
|
2309
|
+
sd["status"] = "stopped"
|
|
2310
|
+
atomic_write_json(session_file, sd, use_lock=True)
|
|
2311
|
+
except Exception:
|
|
2312
|
+
pass
|
|
2313
|
+
|
|
2314
|
+
registry.mark_project_stopped(project_id)
|
|
2315
|
+
|
|
2316
|
+
return {
|
|
2317
|
+
"success": True,
|
|
2318
|
+
"project_id": project_id,
|
|
2319
|
+
"stopped": stopped,
|
|
2320
|
+
"already_stopped": False,
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
|
|
2208
2324
|
# =============================================================================
|
|
2209
2325
|
# Enterprise Features (Optional - enabled via environment variables)
|
|
2210
2326
|
# =============================================================================
|
|
@@ -303,6 +303,50 @@
|
|
|
303
303
|
border-color: var(--loki-accent);
|
|
304
304
|
box-shadow: 0 0 0 3px var(--loki-accent-glow);
|
|
305
305
|
}
|
|
306
|
+
/* v7.7.30 per-project stop list */
|
|
307
|
+
.project-stop-list {
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-wrap: wrap;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 6px;
|
|
312
|
+
margin-left: 10px;
|
|
313
|
+
}
|
|
314
|
+
.project-stop-row {
|
|
315
|
+
display: inline-flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
gap: 6px;
|
|
318
|
+
padding: 3px 6px 3px 10px;
|
|
319
|
+
background: var(--loki-bg-primary);
|
|
320
|
+
border: 1px solid var(--loki-border);
|
|
321
|
+
border-radius: 14px;
|
|
322
|
+
font-size: 11px;
|
|
323
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
324
|
+
color: var(--loki-text-primary);
|
|
325
|
+
}
|
|
326
|
+
.project-stop-row .project-stop-name {
|
|
327
|
+
max-width: 160px;
|
|
328
|
+
overflow: hidden;
|
|
329
|
+
text-overflow: ellipsis;
|
|
330
|
+
white-space: nowrap;
|
|
331
|
+
}
|
|
332
|
+
.project-stop-row button {
|
|
333
|
+
padding: 2px 8px;
|
|
334
|
+
background: transparent;
|
|
335
|
+
border: 1px solid var(--loki-border);
|
|
336
|
+
border-radius: 10px;
|
|
337
|
+
font-size: 11px;
|
|
338
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
339
|
+
color: var(--loki-text-secondary);
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
}
|
|
342
|
+
.project-stop-row button:hover:not(:disabled) {
|
|
343
|
+
border-color: #d64545;
|
|
344
|
+
color: #d64545;
|
|
345
|
+
}
|
|
346
|
+
.project-stop-row button:disabled {
|
|
347
|
+
opacity: 0.6;
|
|
348
|
+
cursor: default;
|
|
349
|
+
}
|
|
306
350
|
|
|
307
351
|
/* Main Content */
|
|
308
352
|
.main-content {
|
|
@@ -556,6 +600,11 @@
|
|
|
556
600
|
<select class="project-switcher" id="project-switcher" title="Switch project" aria-label="Switch project">
|
|
557
601
|
<option value="">All projects (current dir)</option>
|
|
558
602
|
</select>
|
|
603
|
+
<!-- v7.7.30 per-project stop: a compact list of running projects, each
|
|
604
|
+
with a Stop button that gracefully halts that project's runner
|
|
605
|
+
without affecting any other folder. Built at runtime; empty when
|
|
606
|
+
no project is running. -->
|
|
607
|
+
<div class="project-stop-list" id="project-stop-list" aria-label="Running projects"></div>
|
|
559
608
|
</div>
|
|
560
609
|
|
|
561
610
|
<nav class="nav-links">
|
|
@@ -13403,6 +13452,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
13403
13452
|
(function initProjectSwitcher() {
|
|
13404
13453
|
var sel = document.getElementById('project-switcher');
|
|
13405
13454
|
if (!sel) return;
|
|
13455
|
+
var stopList = document.getElementById('project-stop-list');
|
|
13456
|
+
// v7.7.30: build a per-row Stop control for each running project using
|
|
13457
|
+
// createElement + textContent only (never innerHTML for project-supplied
|
|
13458
|
+
// strings), so a project name can never inject markup.
|
|
13459
|
+
function buildStopList(projects) {
|
|
13460
|
+
if (!stopList) return;
|
|
13461
|
+
while (stopList.firstChild) stopList.removeChild(stopList.firstChild);
|
|
13462
|
+
projects.forEach(function(p){
|
|
13463
|
+
if (p.running !== true) return;
|
|
13464
|
+
var row = document.createElement('div');
|
|
13465
|
+
row.className = 'project-stop-row';
|
|
13466
|
+
var name = document.createElement('span');
|
|
13467
|
+
name.className = 'project-stop-name';
|
|
13468
|
+
name.textContent = p.name || p.path || 'project';
|
|
13469
|
+
var btn = document.createElement('button');
|
|
13470
|
+
btn.type = 'button';
|
|
13471
|
+
btn.textContent = 'Stop';
|
|
13472
|
+
if (p.id) btn.setAttribute('data-id', p.id);
|
|
13473
|
+
btn.addEventListener('click', function(){
|
|
13474
|
+
if (!p.id) return;
|
|
13475
|
+
btn.disabled = true;
|
|
13476
|
+
btn.textContent = 'Stopping...';
|
|
13477
|
+
fetch('/api/running-projects/stop', {
|
|
13478
|
+
method: 'POST',
|
|
13479
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13480
|
+
body: JSON.stringify({ id: p.id })
|
|
13481
|
+
})
|
|
13482
|
+
.then(function(){ refresh(); })
|
|
13483
|
+
.catch(function(){ btn.disabled = false; btn.textContent = 'Stop'; });
|
|
13484
|
+
});
|
|
13485
|
+
row.appendChild(name);
|
|
13486
|
+
row.appendChild(btn);
|
|
13487
|
+
stopList.appendChild(row);
|
|
13488
|
+
});
|
|
13489
|
+
}
|
|
13406
13490
|
function refresh() {
|
|
13407
13491
|
fetch('/api/running-projects')
|
|
13408
13492
|
.then(function(r){ return r.ok ? r.json() : null; })
|
|
@@ -13423,6 +13507,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
13423
13507
|
sel.appendChild(o);
|
|
13424
13508
|
});
|
|
13425
13509
|
if (!data.active_project_dir && current === '') sel.value = '';
|
|
13510
|
+
buildStopList(data.projects);
|
|
13426
13511
|
})
|
|
13427
13512
|
.catch(function(){ /* offline / no endpoint: leave as-is */ });
|
|
13428
13513
|
}
|
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.30";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=F7(N7(import.meta.url)),Q=S1($);o=w7(x7(Q,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var C1=w(()=>{g()});var $0={};v($0,{runOrThrow:()=>S7,run:()=>C,commandVersion:()=>C7,commandExists:()=>b,ShellError:()=>D1});async function C(K,$={}){let Q=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),X,Z;if($.timeoutMs&&$.timeoutMs>0)X=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[W,z,q]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text(),Q.exited]);return{stdout:W,stderr:z,exitCode:q}}finally{if(X)clearTimeout(X);if(Z)clearTimeout(Z)}}async function S7(K,$={}){let Q=await C(K,$);if(Q.exitCode!==0)throw new D1(`command failed (${Q.exitCode}): ${K.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function b(K){let $=k7(K),Q=await C(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function C7(K,$="--version"){if(!await b(K))return null;let X=await C([K,$],{timeoutMs:5000});if(X.exitCode!==0)return null;return((X.stdout||X.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var D1;var n=w(()=>{D1=class D1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,Q,X){super(K);this.message=K;this.exitCode=$;this.stdout=Q;this.stderr=X;this.name="ShellError"}}});function c(K){return D7?"":K}var D7,F,y,N,A6,A,D,S,H;var a=w(()=>{D7=(process.env.NO_COLOR??"").length>0;F=c("\x1B[0;31m"),y=c("\x1B[0;32m"),N=c("\x1B[1;33m"),A6=c("\x1B[0;34m"),A=c("\x1B[0;36m"),D=c("\x1B[1m"),S=c("\x1B[2m"),H=c("\x1B[0m")});import{existsSync as c7}from"fs";async function i(){if(Z1!==void 0)return Z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return Z1=K,K;let $=await b("python3.12");if($)return Z1=$,$;let Q=await b("python3");return Z1=Q,Q}async function s(K,$={}){let Q=await i();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return C([Q,"-c",K],$)}var Z1;var z1=w(()=>{n()});var G0={};v(G0,{runStatus:()=>X5});import{existsSync as k,readFileSync as K1,readdirSync as W0,statSync as H0}from"fs";import{resolve as R,basename as a7}from"path";import{homedir as s7}from"os";async function t7(){if(await b("jq"))return!0;return process.stdout.write(`${F}Error: jq is required but not installed.${H}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -604,4 +604,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
604
604
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
605
605
|
`),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var Z6=await X6(Bun.argv.slice(2));process.exit(Z6);
|
|
606
606
|
|
|
607
|
-
//# debugId=
|
|
607
|
+
//# debugId=1D57DCC7E6AE757E64756E2164756E21
|
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.30",
|
|
4
4
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|