loki-mode 7.83.0 → 7.84.0
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 +64 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/registry.py +54 -0
- package/dashboard/server.py +55 -9
- package/dashboard/static/index.html +953 -376
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.84.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.84.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.84.0
|
package/autonomy/loki
CHANGED
|
@@ -3193,6 +3193,10 @@ cmd_next() {
|
|
|
3193
3193
|
cmd_ship() {
|
|
3194
3194
|
local do_preview=false
|
|
3195
3195
|
local review_args=()
|
|
3196
|
+
# Tracks whether the caller passed an explicit scope flag (--staged, --since,
|
|
3197
|
+
# ...). Passthrough flags like --yes/--severity do NOT set this, so they do
|
|
3198
|
+
# not suppress the clean-loki-branch auto-scoping below.
|
|
3199
|
+
local ship_explicit_scope=false
|
|
3196
3200
|
while [ $# -gt 0 ]; do
|
|
3197
3201
|
case "${1:-}" in
|
|
3198
3202
|
--help|-h|help)
|
|
@@ -3222,7 +3226,31 @@ cmd_ship() {
|
|
|
3222
3226
|
return 0
|
|
3223
3227
|
;;
|
|
3224
3228
|
--preview) do_preview=true ;;
|
|
3225
|
-
|
|
3229
|
+
# Only a known set of NON-scope passthrough flags are exempt from
|
|
3230
|
+
# "explicit scope". Everything else the caller passes -- an explicit
|
|
3231
|
+
# scope flag (--staged/--since/...), a positional <file-or-dir>, or any
|
|
3232
|
+
# other review flag that narrows WHAT is reviewed -- counts as explicit
|
|
3233
|
+
# scope, so we do NOT override it with the clean-branch auto-scoping
|
|
3234
|
+
# below. This is the safe default: passthrough-that-changes-scope must
|
|
3235
|
+
# win over our auto-scope. The exempt list is only flags that never
|
|
3236
|
+
# change scope: --yes/-y (skip prompt), --severity (threshold).
|
|
3237
|
+
# (Without this, `loki ship somefile.py` on a clean loki branch would
|
|
3238
|
+
# get --since base injected and review the whole range, not the file.)
|
|
3239
|
+
--yes|-y|--severity=*)
|
|
3240
|
+
review_args+=("$1") ;;
|
|
3241
|
+
# --severity in its DOCUMENTED two-token form (`--severity high`) takes
|
|
3242
|
+
# a VALUE in the next arg. Consume both here so the value does not fall
|
|
3243
|
+
# to the catch-all and wrongly mark explicit scope (which would skip the
|
|
3244
|
+
# clean-branch auto-scoping and re-introduce the empty-diff false-clean
|
|
3245
|
+
# bug). --severity sets a threshold, not a scope, so neither token is
|
|
3246
|
+
# explicit scope.
|
|
3247
|
+
--severity)
|
|
3248
|
+
review_args+=("$1")
|
|
3249
|
+
if [ "$#" -ge 2 ] && [ "${2#-}" = "$2" ]; then
|
|
3250
|
+
review_args+=("$2"); shift
|
|
3251
|
+
fi
|
|
3252
|
+
;;
|
|
3253
|
+
*) review_args+=("$1"); ship_explicit_scope=true ;;
|
|
3226
3254
|
esac
|
|
3227
3255
|
shift
|
|
3228
3256
|
done
|
|
@@ -3243,6 +3271,41 @@ cmd_ship() {
|
|
|
3243
3271
|
|
|
3244
3272
|
# 2. Run the SAME diff quality gates as cmd_review. Capture its documented
|
|
3245
3273
|
# exit code (0 clean, 1 HIGH, 2 CRITICAL). set -e safe: capture via || rc=$?.
|
|
3274
|
+
#
|
|
3275
|
+
# Scope correction: cmd_review defaults to the UNCOMMITTED diff, but run.sh
|
|
3276
|
+
# auto-commits the session's work to the loki/session-* branch, so by the
|
|
3277
|
+
# time `loki ship` runs the tree is usually CLEAN -- which would make review
|
|
3278
|
+
# see an empty diff and falsely report "clean" before advising a PR (the
|
|
3279
|
+
# safety gate would be a no-op exactly when it matters). When the caller
|
|
3280
|
+
# gave no explicit scope AND we are on a loki/* branch with a clean tree,
|
|
3281
|
+
# default the scope to the branch-vs-base RANGE (the work being shipped),
|
|
3282
|
+
# resolved the same way the PR advice below resolves it.
|
|
3283
|
+
local _ship_dir="."
|
|
3284
|
+
if [ "$ship_explicit_scope" = false ]; then
|
|
3285
|
+
local _ship_head="" _ship_base="" _ship_dirty=""
|
|
3286
|
+
_ship_head="$(git -C "$_ship_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
3287
|
+
# Untracked files (e.g. a gitignored .loki/) do not count as "dirty" --
|
|
3288
|
+
# what matters is whether the shippable TRACKED work is already committed.
|
|
3289
|
+
_ship_dirty="$(git -C "$_ship_dir" status --porcelain --untracked-files=no 2>/dev/null)"
|
|
3290
|
+
case "$_ship_head" in
|
|
3291
|
+
loki/*)
|
|
3292
|
+
if [ -z "$_ship_dirty" ]; then
|
|
3293
|
+
if [ -s "$_ship_dir/.loki/state/base-branch.txt" ]; then
|
|
3294
|
+
_ship_base="$(head -n1 "$_ship_dir/.loki/state/base-branch.txt" 2>/dev/null || echo "")"
|
|
3295
|
+
fi
|
|
3296
|
+
if [ -z "$_ship_base" ]; then
|
|
3297
|
+
local _ship_oh=""
|
|
3298
|
+
_ship_oh="$(git -C "$_ship_dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")"
|
|
3299
|
+
[ -n "$_ship_oh" ] && _ship_base="${_ship_oh##*/}"
|
|
3300
|
+
fi
|
|
3301
|
+
if [ -n "$_ship_base" ] && git -C "$_ship_dir" rev-parse --verify --quiet "$_ship_base" >/dev/null 2>&1; then
|
|
3302
|
+
review_args+=("--since" "$_ship_base")
|
|
3303
|
+
echo -e "${DIM}Clean working tree on $_ship_head; reviewing the branch range vs $_ship_base.${NC}"
|
|
3304
|
+
fi
|
|
3305
|
+
fi
|
|
3306
|
+
;;
|
|
3307
|
+
esac
|
|
3308
|
+
fi
|
|
3246
3309
|
echo -e "${BOLD}Running quality gates on the diff (loki review)...${NC}"
|
|
3247
3310
|
echo ""
|
|
3248
3311
|
local rc=0
|
package/dashboard/__init__.py
CHANGED
package/dashboard/registry.py
CHANGED
|
@@ -342,6 +342,60 @@ def mark_project_stopped(identifier: str) -> Optional[dict]:
|
|
|
342
342
|
return None
|
|
343
343
|
|
|
344
344
|
|
|
345
|
+
def prune_missing_projects(running_ids: Optional[set] = None) -> list[dict]:
|
|
346
|
+
"""Remove registry entries whose project path no longer exists on disk.
|
|
347
|
+
|
|
348
|
+
The registry records every cwd that has ever been seen by `loki start` and,
|
|
349
|
+
until this function runs, never garbage-collects them: deleted / renamed /
|
|
350
|
+
temp project directories accumulate forever and bloat the dashboard project
|
|
351
|
+
switcher with paths that no longer exist. This prunes those dead entries.
|
|
352
|
+
|
|
353
|
+
Safety:
|
|
354
|
+
- A path is considered dead only when os.path.isdir(path) is False.
|
|
355
|
+
- A currently-running project is NEVER pruned even if its path check is
|
|
356
|
+
racy (e.g. a transient unmount): pass its project id in running_ids and
|
|
357
|
+
the entry is kept regardless of the disk check. An entry with a live pid
|
|
358
|
+
recorded in the registry is also kept defensively (a running build whose
|
|
359
|
+
dir momentarily fails to stat must not be dropped, which would orphan
|
|
360
|
+
the switcher's Stop target).
|
|
361
|
+
- The whole load->mutate->save runs under _registry_lock() with the atomic
|
|
362
|
+
_save_registry, so it is safe under concurrency with the leaf mutators.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
running_ids: Optional set of project ids that are known to be running
|
|
366
|
+
and must be retained unconditionally. None means "trust the disk
|
|
367
|
+
check and the per-entry recorded pid only".
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
The list of pruned (removed) project entries. Empty when nothing was
|
|
371
|
+
removed.
|
|
372
|
+
"""
|
|
373
|
+
keep_running = running_ids or set()
|
|
374
|
+
pruned: list[dict] = []
|
|
375
|
+
with _registry_lock():
|
|
376
|
+
registry = _load_registry()
|
|
377
|
+
projects = registry.get("projects", {})
|
|
378
|
+
|
|
379
|
+
survivors = {}
|
|
380
|
+
for project_id, project in projects.items():
|
|
381
|
+
path = project.get("path", "")
|
|
382
|
+
# Keep unconditionally if the caller marked it running, or if the
|
|
383
|
+
# registry has a live pid recorded for it.
|
|
384
|
+
if project_id in keep_running or _pid_alive(project.get("pid")):
|
|
385
|
+
survivors[project_id] = project
|
|
386
|
+
continue
|
|
387
|
+
# Otherwise keep only if the path still exists on disk.
|
|
388
|
+
if path and os.path.isdir(path):
|
|
389
|
+
survivors[project_id] = project
|
|
390
|
+
else:
|
|
391
|
+
pruned.append(project)
|
|
392
|
+
|
|
393
|
+
if pruned:
|
|
394
|
+
registry["projects"] = survivors
|
|
395
|
+
_save_registry(registry)
|
|
396
|
+
return pruned
|
|
397
|
+
|
|
398
|
+
|
|
345
399
|
def check_project_health(identifier: str) -> dict:
|
|
346
400
|
"""
|
|
347
401
|
Check the health status of a project.
|
package/dashboard/server.py
CHANGED
|
@@ -2900,6 +2900,17 @@ async def list_running_projects():
|
|
|
2900
2900
|
Live-vs-stale is derived from pid liveness, which is robust even when a
|
|
2901
2901
|
session is hard-killed (no exit hook fires). Never raises: registry
|
|
2902
2902
|
problems degrade to an empty list.
|
|
2903
|
+
|
|
2904
|
+
Registry hygiene (project switcher): the registry records every cwd ever
|
|
2905
|
+
seen by `loki start` and never garbage-collects, so the switcher list grows
|
|
2906
|
+
without bound and shows paths that no longer exist on disk. This endpoint:
|
|
2907
|
+
- opportunistically prunes registry entries whose path is gone AND which
|
|
2908
|
+
are not running (registry.prune_missing_projects), keeping the on-disk
|
|
2909
|
+
store small. Running projects are never pruned.
|
|
2910
|
+
- returns a CLEAN list: a project is included only if its path still
|
|
2911
|
+
exists on disk OR it is currently running (or is the active project).
|
|
2912
|
+
- sorts by last_accessed desc (most relevant first) and caps the list
|
|
2913
|
+
defensively, but never drops a running or the active project.
|
|
2903
2914
|
"""
|
|
2904
2915
|
out = []
|
|
2905
2916
|
try:
|
|
@@ -2907,6 +2918,19 @@ async def list_running_projects():
|
|
|
2907
2918
|
except Exception:
|
|
2908
2919
|
projects = []
|
|
2909
2920
|
active = _active_project_dir
|
|
2921
|
+
|
|
2922
|
+
def _is_active(path: str) -> bool:
|
|
2923
|
+
# Compare via realpath: /api/focus resolves symlinks (e.g. macOS
|
|
2924
|
+
# /tmp -> /private/tmp) while the registry stores abspath, so a plain
|
|
2925
|
+
# abspath compare would never match a focused symlinked project.
|
|
2926
|
+
if not (active and path):
|
|
2927
|
+
return False
|
|
2928
|
+
try:
|
|
2929
|
+
return os.path.realpath(active) == os.path.realpath(path)
|
|
2930
|
+
except OSError:
|
|
2931
|
+
return os.path.abspath(active) == os.path.abspath(path)
|
|
2932
|
+
|
|
2933
|
+
running_ids = set()
|
|
2910
2934
|
for p in projects:
|
|
2911
2935
|
path = p.get("path", "")
|
|
2912
2936
|
pid = p.get("pid")
|
|
@@ -2936,15 +2960,15 @@ async def list_running_projects():
|
|
|
2936
2960
|
running = s.get("status") == "running"
|
|
2937
2961
|
except Exception:
|
|
2938
2962
|
pass
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2963
|
+
path_exists = bool(path) and os.path.isdir(path)
|
|
2964
|
+
is_active = _is_active(path)
|
|
2965
|
+
if running:
|
|
2966
|
+
running_ids.add(p.get("id"))
|
|
2967
|
+
# CLEAN list: include only projects whose path still exists, or which
|
|
2968
|
+
# are running, or which are the active project. A dead path that is not
|
|
2969
|
+
# running is excluded (and pruned from the store below).
|
|
2970
|
+
if not (path_exists or running or is_active):
|
|
2971
|
+
continue
|
|
2948
2972
|
out.append({
|
|
2949
2973
|
"id": p.get("id"),
|
|
2950
2974
|
"name": p.get("name") or (os.path.basename(path) if path else "project"),
|
|
@@ -2953,7 +2977,29 @@ async def list_running_projects():
|
|
|
2953
2977
|
"status": p.get("status"),
|
|
2954
2978
|
"running": running,
|
|
2955
2979
|
"is_active": is_active,
|
|
2980
|
+
"_last_accessed": p.get("last_accessed") or p.get("registered_at") or "",
|
|
2956
2981
|
})
|
|
2982
|
+
|
|
2983
|
+
# Opportunistically garbage-collect dead entries from the on-disk registry
|
|
2984
|
+
# so it never grows without bound. Running projects (by id, plus any with a
|
|
2985
|
+
# live recorded pid inside the helper) are retained even if the disk check
|
|
2986
|
+
# is racy. Best-effort: a prune failure must not break the listing.
|
|
2987
|
+
try:
|
|
2988
|
+
registry.prune_missing_projects(running_ids=running_ids)
|
|
2989
|
+
except Exception:
|
|
2990
|
+
pass
|
|
2991
|
+
|
|
2992
|
+
# Most relevant first: last_accessed desc. Cap defensively, but never hide a
|
|
2993
|
+
# running or active project (partition them out before the cap).
|
|
2994
|
+
_SWITCHER_CAP = 50
|
|
2995
|
+
out.sort(key=lambda r: r.get("_last_accessed", ""), reverse=True)
|
|
2996
|
+
if len(out) > _SWITCHER_CAP:
|
|
2997
|
+
pinned = [r for r in out if r["running"] or r["is_active"]]
|
|
2998
|
+
rest = [r for r in out if not (r["running"] or r["is_active"])]
|
|
2999
|
+
out = pinned + rest[: max(0, _SWITCHER_CAP - len(pinned))]
|
|
3000
|
+
# Drop the internal sort key from the response shape.
|
|
3001
|
+
for r in out:
|
|
3002
|
+
r.pop("_last_accessed", None)
|
|
2957
3003
|
return {"projects": out, "active_project_dir": active}
|
|
2958
3004
|
|
|
2959
3005
|
|