loki-mode 8.6.1 → 8.8.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/lib/deadline.py +38 -4
- package/autonomy/loki +194 -3
- package/autonomy/run.sh +170 -29
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +207 -19
- package/dashboard/static/index.html +10 -9
- package/docs/environment-variables.md +36 -4
- package/loki-ts/dist/loki.js +387 -382
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/aider.sh +35 -3
- package/providers/claude.sh +12 -1
- package/providers/cline.sh +26 -3
- package/providers/model_catalog.json +17 -6
- package/providers/models.sh +58 -3
- package/providers/opencode.sh +8 -2
- package/references/multi-provider.md +9 -0
- package/skills/providers.md +35 -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 v8.
|
|
6
|
+
# Loki Mode v8.8.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
-
**v8.
|
|
472
|
+
**v8.8.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.8.0
|
package/autonomy/lib/deadline.py
CHANGED
|
@@ -254,6 +254,14 @@ def _process_snapshot() -> dict[int, _ProcessRecord]:
|
|
|
254
254
|
return snapshot
|
|
255
255
|
|
|
256
256
|
|
|
257
|
+
class _LineageUnknown(Exception):
|
|
258
|
+
"""The lineage marker could not be read because the process is gone.
|
|
259
|
+
|
|
260
|
+
Distinct from "the marker is absent". A vanished process is not evidence
|
|
261
|
+
of a lineage violation -- it is the absence of evidence either way.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
|
|
257
265
|
def _process_has_lineage_token(pid: int, token: str) -> bool:
|
|
258
266
|
needle = f"{_LINEAGE_ENV_NAME}={token}".encode("ascii")
|
|
259
267
|
if sys.platform == "darwin":
|
|
@@ -274,6 +282,19 @@ def _process_has_lineage_token(pid: int, token: str) -> bool:
|
|
|
274
282
|
try:
|
|
275
283
|
with open(f"/proc/{pid}/environ", "rb") as handle:
|
|
276
284
|
return needle in handle.read().split(b"\0")
|
|
285
|
+
except FileNotFoundError:
|
|
286
|
+
# The process is already gone. /proc/<pid>/environ disappears the
|
|
287
|
+
# moment a child is reaped, so a short-lived runner that exits
|
|
288
|
+
# before this check runs is indistinguishable here from one that
|
|
289
|
+
# never carried the marker -- and answering "no marker" for it
|
|
290
|
+
# fails the lineage guard and returns 127 as a LAUNCH failure, for
|
|
291
|
+
# a process that in fact launched and completed.
|
|
292
|
+
#
|
|
293
|
+
# Signal "unknown", not "absent". The caller decides, because only
|
|
294
|
+
# the caller knows whether the process it spawned is still alive.
|
|
295
|
+
# macOS never hit this: it has the pipe-handle fallback, so the
|
|
296
|
+
# bug was Linux-only and invisible on a developer Mac.
|
|
297
|
+
raise _LineageUnknown(pid) from None
|
|
277
298
|
except OSError:
|
|
278
299
|
return False
|
|
279
300
|
return False
|
|
@@ -354,10 +375,23 @@ class _LineageTracker:
|
|
|
354
375
|
root = _process_record(process.pid)
|
|
355
376
|
if root is None:
|
|
356
377
|
raise RuntimeError("provider attempt identity is unavailable")
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
378
|
+
try:
|
|
379
|
+
has_marker = (
|
|
380
|
+
_process_has_lineage_token(root.pid, token)
|
|
381
|
+
or _process_has_lineage_pipe(root.pid, lineage_pipe_handles)
|
|
382
|
+
)
|
|
383
|
+
except _LineageUnknown:
|
|
384
|
+
# The child exited before we could read its marker. That is a
|
|
385
|
+
# completed run, not a lineage violation -- and it is the common
|
|
386
|
+
# case for any runner that finishes fast. Confirm it really is our
|
|
387
|
+
# child (Popen.poll() is authoritative: it reaps only the process
|
|
388
|
+
# this object spawned) before accepting.
|
|
389
|
+
#
|
|
390
|
+
# This stays fail-closed for the case the guard exists to catch: a
|
|
391
|
+
# LIVE process whose marker is genuinely missing still raises,
|
|
392
|
+
# because poll() returns None for it and we fall through.
|
|
393
|
+
has_marker = process.poll() is not None
|
|
394
|
+
if not has_marker:
|
|
361
395
|
raise RuntimeError("provider attempt lineage marker is unavailable")
|
|
362
396
|
self.root_pid = root.pid
|
|
363
397
|
self.session_id = root.session_id
|
package/autonomy/loki
CHANGED
|
@@ -61,6 +61,16 @@ if [ -f "$_LOKI_SCRIPT_DIR/tui.sh" ]; then
|
|
|
61
61
|
source "$_LOKI_SCRIPT_DIR/tui.sh"
|
|
62
62
|
fi
|
|
63
63
|
|
|
64
|
+
# Portable lock helper -- provides safe_acquire_lock / safe_release_lock.
|
|
65
|
+
# Needed here (not just in run.sh) because ensure_dashboard_venv tears down and
|
|
66
|
+
# rebuilds the HOST-GLOBAL ~/.loki/dashboard-venv, so two concurrent CLI
|
|
67
|
+
# invocations can otherwise delete the venv the other is importing from.
|
|
68
|
+
# Self-guarded against double-source.
|
|
69
|
+
if [ -f "$_LOKI_SCRIPT_DIR/lib/lock.sh" ]; then
|
|
70
|
+
# shellcheck source=lib/lock.sh
|
|
71
|
+
source "$_LOKI_SCRIPT_DIR/lib/lock.sh"
|
|
72
|
+
fi
|
|
73
|
+
|
|
64
74
|
# Crash-reporting helpers (provides loki_collection_enabled, used by
|
|
65
75
|
# cmd_telemetry status and cmd_crash). Self-guarded against double-source.
|
|
66
76
|
if [ -f "$_LOKI_SCRIPT_DIR/crash.sh" ]; then
|
|
@@ -446,6 +456,33 @@ ensure_dashboard_venv() {
|
|
|
446
456
|
return 0
|
|
447
457
|
fi
|
|
448
458
|
|
|
459
|
+
# The venv is HOST-GLOBAL, so concurrent runs must not rebuild it at once:
|
|
460
|
+
# one run's `rm -rf` would delete the tree another is importing from. Lock
|
|
461
|
+
# the venv path itself -- safe_acquire_lock appends ".lockdir", giving a
|
|
462
|
+
# SIBLING mutex that the teardown below cannot destroy. Timeout must outlast
|
|
463
|
+
# a real cold `python3 -m venv` + `pip install` (not the 5s used by the
|
|
464
|
+
# JSON read-modify-write call sites).
|
|
465
|
+
local _venv_locked=false
|
|
466
|
+
if type safe_acquire_lock >/dev/null 2>&1; then
|
|
467
|
+
if safe_acquire_lock "$dashboard_venv" "${LOKI_VENV_LOCK_TIMEOUT:-300}"; then
|
|
468
|
+
_venv_locked=true
|
|
469
|
+
# The winner may have finished the build while we waited -- re-probe
|
|
470
|
+
# before tearing anything down.
|
|
471
|
+
DASHBOARD_PYTHON="python3"
|
|
472
|
+
[ -x "${dashboard_venv}/bin/python3" ] && DASHBOARD_PYTHON="${dashboard_venv}/bin/python3"
|
|
473
|
+
if "$DASHBOARD_PYTHON" -c "import fastapi; import sqlalchemy; import aiosqlite" 2>/dev/null; then
|
|
474
|
+
safe_release_lock "$dashboard_venv"
|
|
475
|
+
return 0
|
|
476
|
+
fi
|
|
477
|
+
else
|
|
478
|
+
# Do NOT fall through into the rm -rf unlocked -- that is the exact
|
|
479
|
+
# race this lock exists to prevent.
|
|
480
|
+
echo -e "${RED}Timed out waiting for another run to finish building the dashboard venv${NC}"
|
|
481
|
+
echo " Retry, or remove a stale lock: rm -rf ${dashboard_venv}.lockdir"
|
|
482
|
+
return 1
|
|
483
|
+
fi
|
|
484
|
+
fi
|
|
485
|
+
|
|
449
486
|
echo -e "${YELLOW}Setting up dashboard virtualenv...${NC}"
|
|
450
487
|
|
|
451
488
|
# Create venv if missing or broken
|
|
@@ -461,6 +498,7 @@ ensure_dashboard_venv() {
|
|
|
461
498
|
echo "You may need to install python3-venv:"
|
|
462
499
|
echo " sudo apt install python3-venv (Debian/Ubuntu)"
|
|
463
500
|
echo " brew install python3 (macOS)"
|
|
501
|
+
[ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
|
|
464
502
|
return 1
|
|
465
503
|
}
|
|
466
504
|
fi
|
|
@@ -485,6 +523,7 @@ ensure_dashboard_venv() {
|
|
|
485
523
|
if ! "${dashboard_venv}/bin/pip" install fastapi uvicorn pydantic websockets sqlalchemy aiosqlite httpx pexpect watchdog 2>&1 | tail -1; then
|
|
486
524
|
echo -e "${RED}Failed to install dashboard dependencies${NC}"
|
|
487
525
|
echo "Try manually: ${dashboard_venv}/bin/pip install fastapi uvicorn sqlalchemy aiosqlite"
|
|
526
|
+
[ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
|
|
488
527
|
return 1
|
|
489
528
|
fi
|
|
490
529
|
# Try greenlet separately (optional, needs C compiler on some platforms)
|
|
@@ -497,9 +536,11 @@ ensure_dashboard_venv() {
|
|
|
497
536
|
echo "Try removing the venv and retrying:"
|
|
498
537
|
echo " rm -rf ${dashboard_venv}"
|
|
499
538
|
echo " loki dashboard start"
|
|
539
|
+
[ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
|
|
500
540
|
return 1
|
|
501
541
|
fi
|
|
502
542
|
|
|
543
|
+
[ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
|
|
503
544
|
return 0
|
|
504
545
|
}
|
|
505
546
|
|
|
@@ -5337,7 +5378,14 @@ cmd_provider_models() {
|
|
|
5337
5378
|
echo -e "${BOLD}Model Configuration (resolved):${NC}"
|
|
5338
5379
|
echo ""
|
|
5339
5380
|
|
|
5340
|
-
|
|
5381
|
+
echo -e " ${CYAN}Ask for a tier by capability -- small, medium or high -- and each"
|
|
5382
|
+
echo -e " provider supplies its own latest model in that class. medium is the"
|
|
5383
|
+
echo -e " default. Set one with LOKI_SESSION_MODEL=small or --session-model small.${NC}"
|
|
5384
|
+
echo ""
|
|
5385
|
+
|
|
5386
|
+
# opencode ships a provider file and a catalog entry, so omitting it here
|
|
5387
|
+
# under-reported what this command claims to show ("all providers").
|
|
5388
|
+
local providers=("claude" "codex" "cline" "aider" "opencode")
|
|
5341
5389
|
for provider in "${providers[@]}"; do
|
|
5342
5390
|
local provider_file="$script_dir/providers/${provider}.sh"
|
|
5343
5391
|
[ -f "$provider_file" ] || continue
|
|
@@ -5407,7 +5455,34 @@ cmd_provider_models() {
|
|
|
5407
5455
|
fast) value="$fast" ;;
|
|
5408
5456
|
esac
|
|
5409
5457
|
|
|
5410
|
-
|
|
5458
|
+
# Label each tier with the GENERIC vocabulary users actually select
|
|
5459
|
+
# (small|medium|high), so this command answers "what do I really get
|
|
5460
|
+
# if I ask for medium on this provider" without anyone having to know
|
|
5461
|
+
# a vendor model name. The canonical tier name stays alongside it
|
|
5462
|
+
# because every env var, log line and state file still uses it -- and
|
|
5463
|
+
# a user reading LOKI_CLAUDE_MODEL_DEVELOPMENT needs to see the two
|
|
5464
|
+
# spellings connected. Mapping mirrors loki_tier_alias() in
|
|
5465
|
+
# providers/models.sh.
|
|
5466
|
+
local generic
|
|
5467
|
+
case "$tier" in
|
|
5468
|
+
fast) generic="small" ;;
|
|
5469
|
+
development) generic="medium" ;;
|
|
5470
|
+
planning) generic="high" ;;
|
|
5471
|
+
*) generic="$tier" ;;
|
|
5472
|
+
esac
|
|
5473
|
+
|
|
5474
|
+
# An EMPTY resolved model is a real, supported state, not a lookup
|
|
5475
|
+
# failure: providers/codex.sh sets CODEX_DEFAULT_MODEL="" on purpose
|
|
5476
|
+
# so no --model flag is passed and Codex resolves an
|
|
5477
|
+
# account-appropriate default (a hardcoded name broke ChatGPT-account
|
|
5478
|
+
# users outright). Say that plainly instead of printing a blank
|
|
5479
|
+
# column. Deliberately NOT filled in from the catalog: this table
|
|
5480
|
+
# reports what will be DISPATCHED, and the catalog id would be a
|
|
5481
|
+
# guess this file is documented as unable to make.
|
|
5482
|
+
[ -n "$value" ] || value="(provider default -- no --model sent)"
|
|
5483
|
+
|
|
5484
|
+
printf " %-8s %-14s %-38s (source: %s)\n" \
|
|
5485
|
+
"$generic" "(${tier})" "$value" "$source"
|
|
5411
5486
|
done
|
|
5412
5487
|
|
|
5413
5488
|
# Show extra info per provider
|
|
@@ -11341,6 +11416,49 @@ except Exception:
|
|
|
11341
11416
|
fi
|
|
11342
11417
|
echo ""
|
|
11343
11418
|
|
|
11419
|
+
# Model catalog freshness. Providers ship models constantly and this catalog
|
|
11420
|
+
# is hand-maintained, so it rots silently -- nothing else in the system ever
|
|
11421
|
+
# reports its age. Reads ONLY the local file's "updated" field: zero network
|
|
11422
|
+
# I/O, so `loki doctor` stays air-gapped-safe (docs/air-gapped.md). We do not
|
|
11423
|
+
# and will not auto-fetch or guess model IDs; inventing a model that does not
|
|
11424
|
+
# exist is worse than being stale, so refresh stays a human-verified step.
|
|
11425
|
+
#
|
|
11426
|
+
# Informational only -- like Runtime route and Cockpit above, it does NOT
|
|
11427
|
+
# touch pass/warn/fail counts and never calls _doctor_block, so a stale
|
|
11428
|
+
# catalog can never flip doctor's exit code or block a build. Advisory by
|
|
11429
|
+
# construction, not by convention.
|
|
11430
|
+
#
|
|
11431
|
+
# Mirrored byte-for-byte in loki-ts/src/commands/doctor.ts -- the bun-parity
|
|
11432
|
+
# matrix diffs this section (it is NOT in the normalizer's strip list), so
|
|
11433
|
+
# edit BOTH routes or parity fails.
|
|
11434
|
+
echo -e "${CYAN}Model catalog:${NC}"
|
|
11435
|
+
_catalog_path="${LOKI_MODEL_CATALOG:-${_LOKI_SCRIPT_DIR}/../providers/model_catalog.json}"
|
|
11436
|
+
_catalog_line=$(LOKI_CATALOG_PATH="$_catalog_path" python3 -c "
|
|
11437
|
+
import json, os, datetime
|
|
11438
|
+
# Threshold: 90 days. The upstream probe workflow runs weekly, so a catalog
|
|
11439
|
+
# untouched for a quarter means the probe PRs are going unread.
|
|
11440
|
+
STALE_DAYS = 90
|
|
11441
|
+
p = os.environ['LOKI_CATALOG_PATH']
|
|
11442
|
+
try:
|
|
11443
|
+
updated = json.load(open(p))['updated']
|
|
11444
|
+
age = (datetime.date.today() - datetime.date.fromisoformat(updated)).days
|
|
11445
|
+
except Exception:
|
|
11446
|
+
print('warn|Catalog unreadable or missing an ISO \"updated\" date -- cannot determine age')
|
|
11447
|
+
else:
|
|
11448
|
+
if age > STALE_DAYS:
|
|
11449
|
+
print(f'warn|Last updated {updated} ({age} days ago) -- may be missing newer models. Refresh: python3 tools/probe-model-catalog.py (reports new model IDs from provider docs; you verify and edit the catalog by hand -- never auto-applied)')
|
|
11450
|
+
else:
|
|
11451
|
+
print(f'pass|Last updated {updated} ({age} days ago)')
|
|
11452
|
+
" 2>/dev/null)
|
|
11453
|
+
# Fail-open: if python3 is missing or the probe dies, say so and move on.
|
|
11454
|
+
[ -n "$_catalog_line" ] || _catalog_line="warn|Could not probe catalog age"
|
|
11455
|
+
if [ "${_catalog_line%%|*}" = "pass" ]; then
|
|
11456
|
+
echo -e " ${GREEN}PASS${NC} ${_catalog_line#*|}"
|
|
11457
|
+
else
|
|
11458
|
+
echo -e " ${YELLOW}WARN${NC} ${_catalog_line#*|}"
|
|
11459
|
+
fi
|
|
11460
|
+
echo ""
|
|
11461
|
+
|
|
11344
11462
|
# Cockpit capability (E5): report what `loki cockpit` will actually do in
|
|
11345
11463
|
# this terminal. Informational only (does NOT touch pass/warn/fail counts,
|
|
11346
11464
|
# like Runtime route above), so the bun-parity matrix stays reconcilable.
|
|
@@ -11448,7 +11566,9 @@ except Exception:
|
|
|
11448
11566
|
cmd_doctor_json() {
|
|
11449
11567
|
local _loki_version
|
|
11450
11568
|
_loki_version=$(get_version)
|
|
11451
|
-
LOKI_VERSION="$_loki_version"
|
|
11569
|
+
LOKI_VERSION="$_loki_version" \
|
|
11570
|
+
LOKI_CATALOG_PATH="${LOKI_MODEL_CATALOG:-${_LOKI_SCRIPT_DIR}/../providers/model_catalog.json}" \
|
|
11571
|
+
python3 -c "
|
|
11452
11572
|
import json, os, subprocess, sys, shutil
|
|
11453
11573
|
|
|
11454
11574
|
def get_version(cmd, args=None):
|
|
@@ -11623,6 +11743,32 @@ if _any_provider:
|
|
|
11623
11743
|
else:
|
|
11624
11744
|
fail_count += 1
|
|
11625
11745
|
|
|
11746
|
+
# Model catalog freshness. Local read only -- no network. Advisory: deliberately
|
|
11747
|
+
# excluded from pass/fail/warn counts and from 'ok', so a stale catalog can never
|
|
11748
|
+
# flip doctor's exit code. Mirrors describeCatalogFreshness() in
|
|
11749
|
+
# loki-ts/src/commands/doctor.ts.
|
|
11750
|
+
CATALOG_STALE_DAYS = 90
|
|
11751
|
+
_cat_path = os.environ['LOKI_CATALOG_PATH']
|
|
11752
|
+
try:
|
|
11753
|
+
import datetime as _dt
|
|
11754
|
+
_cat_updated = json.load(open(_cat_path))['updated']
|
|
11755
|
+
_cat_age = (_dt.date.today() - _dt.date.fromisoformat(_cat_updated)).days
|
|
11756
|
+
if _cat_age > CATALOG_STALE_DAYS:
|
|
11757
|
+
# Deliberately NOT counted. The block comment above states catalog age is
|
|
11758
|
+
# excluded from pass/fail/warn and from 'ok' so a stale catalog can never
|
|
11759
|
+
# fail a build -- this increment contradicted that contract, and it also
|
|
11760
|
+
# broke route parity: the Bun route (doctor.ts) never counted it, so the
|
|
11761
|
+
# same host reported warnings: 2 on bash and 1 on Bun. bun-parity
|
|
11762
|
+
# compares these byte for byte, so it would have gone red on day 91.
|
|
11763
|
+
model_catalog = {'status': 'warn', 'updated': _cat_updated, 'age_days': _cat_age,
|
|
11764
|
+
'detail': f'Last updated {_cat_updated} ({_cat_age} days ago) -- may be missing newer models'}
|
|
11765
|
+
else:
|
|
11766
|
+
model_catalog = {'status': 'pass', 'updated': _cat_updated, 'age_days': _cat_age,
|
|
11767
|
+
'detail': f'Last updated {_cat_updated} ({_cat_age} days ago)'}
|
|
11768
|
+
except Exception:
|
|
11769
|
+
model_catalog = {'status': 'warn', 'updated': None, 'age_days': None,
|
|
11770
|
+
'detail': 'Catalog unreadable or missing an ISO \"updated\" date -- cannot determine age'}
|
|
11771
|
+
|
|
11626
11772
|
result = {
|
|
11627
11773
|
'loki_mode_version': os.environ.get('LOKI_VERSION', 'unknown'),
|
|
11628
11774
|
'checks': checks,
|
|
@@ -11634,6 +11780,7 @@ result = {
|
|
|
11634
11780
|
'sentrux': sentrux,
|
|
11635
11781
|
'receipt_signing': receipt_signing,
|
|
11636
11782
|
'memory': memory,
|
|
11783
|
+
'model_catalog': model_catalog,
|
|
11637
11784
|
'summary': {
|
|
11638
11785
|
'passed': pass_count,
|
|
11639
11786
|
'failed': fail_count,
|
|
@@ -17159,8 +17306,19 @@ def _loki_norm_alias(raw):
|
|
|
17159
17306
|
# pin is a tier route, so tier names ARE valid pins. This normalizer mirrors
|
|
17160
17307
|
# run.sh's trim+lowercase (interior whitespace preserved, so 'fab le' stays junk
|
|
17161
17308
|
# and falls through to the default tier exactly like the runner's '*' arm).
|
|
17309
|
+
#
|
|
17310
|
+
# GENERIC TIER VOCABULARY (small|medium|high): translated onto the canonical
|
|
17311
|
+
# tier names before the allowlist check, mirroring run.sh's entry-point case.
|
|
17312
|
+
# The estimator runs in its own process and reads LOKI_SESSION_MODEL directly,
|
|
17313
|
+
# so without this a 'medium' pin would fall through to the '' default and the
|
|
17314
|
+
# quote would silently price a different model than the run dispatches -- the
|
|
17315
|
+
# exact estimator-vs-runner divergence this file keeps paying down. Only the
|
|
17316
|
+
# three new words are translated; every existing value passes through unchanged.
|
|
17317
|
+
_LOKI_GENERIC_TIERS = {'small': 'fast', 'medium': 'development', 'high': 'planning'}
|
|
17318
|
+
|
|
17162
17319
|
def _loki_norm_session_pin(raw):
|
|
17163
17320
|
raw = (raw or '').strip().lower()
|
|
17321
|
+
raw = _LOKI_GENERIC_TIERS.get(raw, raw)
|
|
17164
17322
|
return raw if raw in (
|
|
17165
17323
|
'haiku', 'sonnet', 'opus', 'fable',
|
|
17166
17324
|
'planning', 'development', 'fast',
|
|
@@ -18128,6 +18286,23 @@ main() {
|
|
|
18128
18286
|
local command="$1"
|
|
18129
18287
|
shift
|
|
18130
18288
|
|
|
18289
|
+
# LOKI_HELP_ONLY is set by `loki help <command>`, which delegates by
|
|
18290
|
+
# re-entering dispatch as `loki <command> --help`. Asking for help must
|
|
18291
|
+
# never DO anything, and dispatch reaches commands that start builds, stop
|
|
18292
|
+
# runs and modify the install. Every command honours --help today; this
|
|
18293
|
+
# makes that a guaranteed property of the help path rather than a fact
|
|
18294
|
+
# someone has to re-verify whenever a command is added. If the delegated
|
|
18295
|
+
# invocation somehow lost its --help, refuse rather than execute.
|
|
18296
|
+
if [ "${LOKI_HELP_ONLY:-0}" = "1" ]; then
|
|
18297
|
+
case " $* " in
|
|
18298
|
+
*" --help "*|*" -h "*) : ;;
|
|
18299
|
+
*)
|
|
18300
|
+
echo "loki: refusing to run '$command' from the help path" >&2
|
|
18301
|
+
return 1
|
|
18302
|
+
;;
|
|
18303
|
+
esac
|
|
18304
|
+
fi
|
|
18305
|
+
|
|
18131
18306
|
# v7.4.13: first-run telemetry moved to bin/loki shim so it fires for
|
|
18132
18307
|
# both Bun-routed and bash-routed commands (autonomy/loki main() never
|
|
18133
18308
|
# runs for the 8 ported commands). Marker file: ~/.loki-first-run.
|
|
@@ -18585,6 +18760,22 @@ main() {
|
|
|
18585
18760
|
# deprecated-alias table; bare help prints the grouped front page.
|
|
18586
18761
|
if [ "${1:-}" = "aliases" ]; then
|
|
18587
18762
|
show_help_aliases
|
|
18763
|
+
elif [ -n "${1:-}" ]; then
|
|
18764
|
+
# `loki help <command>` reaches the command's own help. Before
|
|
18765
|
+
# this, $1 was read only for the literal "aliases" and otherwise
|
|
18766
|
+
# discarded, so `loki help proof` printed the same generic front
|
|
18767
|
+
# page as bare `loki help` -- while `loki proof --help` printed
|
|
18768
|
+
# real help. Both spellings now agree.
|
|
18769
|
+
#
|
|
18770
|
+
# LOKI_HELP_ONLY makes this safe by construction rather than by
|
|
18771
|
+
# audit. Delegation re-enters dispatch, and dispatch reaches
|
|
18772
|
+
# commands that start, stop and modify things. Every one of them
|
|
18773
|
+
# honours --help today, but "asking for help must never DO
|
|
18774
|
+
# anything" is a property worth enforcing at the boundary
|
|
18775
|
+
# instead of re-checking every time a command is added.
|
|
18776
|
+
local _help_target="$1"
|
|
18777
|
+
shift
|
|
18778
|
+
LOKI_HELP_ONLY=1 "$0" "$_help_target" --help "$@"
|
|
18588
18779
|
else
|
|
18589
18780
|
show_help
|
|
18590
18781
|
fi
|
package/autonomy/run.sh
CHANGED
|
@@ -535,6 +535,43 @@ if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
|
535
535
|
unset _loki_sid_raw _loki_sid_safe
|
|
536
536
|
fi
|
|
537
537
|
|
|
538
|
+
# GENERIC TIER VOCABULARY (small|medium|high). A user should be able to ask for
|
|
539
|
+
# a capability class without naming a vendor model, and get that provider's
|
|
540
|
+
# latest model in the class. LOKI_SESSION_MODEL is the knob that already does
|
|
541
|
+
# this -- it accepts the raw tier names planning|development|fast alongside the
|
|
542
|
+
# Claude aliases -- so the generic words are normalized ONTO it here rather than
|
|
543
|
+
# becoming a fourth spelling. LOKI_MAX_TIER (a cost CEILING) and LOKI_TIER (the
|
|
544
|
+
# OSS/enterprise licensing seam) mean different things and are left alone.
|
|
545
|
+
#
|
|
546
|
+
# WHY NORMALIZE AT THE ENTRY POINT: the session-pin case block is byte-mirrored
|
|
547
|
+
# in the estimator (autonomy/loki) and the dashboard (dashboard/server.py), and
|
|
548
|
+
# every one of those mirrors is locked by a parity test. Translating here means
|
|
549
|
+
# they keep seeing only the three canonical tier names and none of them change.
|
|
550
|
+
#
|
|
551
|
+
# The mapping is loki_tier_alias() in providers/models.sh -- the single source
|
|
552
|
+
# of truth, not a second copy. Inlined as a case because run.sh must not source
|
|
553
|
+
# a provider file this early in startup. Kept in lockstep by
|
|
554
|
+
# tests/test-generic-tiers.sh.
|
|
555
|
+
#
|
|
556
|
+
# ONLY the three new words are translated. sonnet/haiku/opus/fable and the raw
|
|
557
|
+
# tier names pass through untouched, so an unset LOKI_SESSION_MODEL still
|
|
558
|
+
# defaults to sonnet and "medium" resolves to the same development-tier model
|
|
559
|
+
# today's builds already use. This changes no existing run's model.
|
|
560
|
+
# NORMALIZATION IS TRIM-ONLY + LOWERCASE, matching the estimator and dashboard
|
|
561
|
+
# mirrors exactly. Interior whitespace is deliberately PRESERVED, so " med ium "
|
|
562
|
+
# stays junk here just as it does there. Stripping interior spaces would make
|
|
563
|
+
# this reader accept a value the other two reject, which is the precise kind of
|
|
564
|
+
# divergence the session-pin parity tests exist to catch.
|
|
565
|
+
_loki_generic_tier="${LOKI_SESSION_MODEL:-}"
|
|
566
|
+
_loki_generic_tier="${_loki_generic_tier#"${_loki_generic_tier%%[![:space:]]*}"}"
|
|
567
|
+
_loki_generic_tier="${_loki_generic_tier%"${_loki_generic_tier##*[![:space:]]}"}"
|
|
568
|
+
case "$(printf '%s' "$_loki_generic_tier" | tr '[:upper:]' '[:lower:]')" in
|
|
569
|
+
small) LOKI_SESSION_MODEL="fast" ; export LOKI_SESSION_MODEL ;;
|
|
570
|
+
medium) LOKI_SESSION_MODEL="development" ; export LOKI_SESSION_MODEL ;;
|
|
571
|
+
high) LOKI_SESSION_MODEL="planning" ; export LOKI_SESSION_MODEL ;;
|
|
572
|
+
esac
|
|
573
|
+
unset _loki_generic_tier
|
|
574
|
+
|
|
538
575
|
# Process Supervision (opt-in)
|
|
539
576
|
WATCHDOG_ENABLED=${LOKI_WATCHDOG:-"false"} # Enable process health monitoring
|
|
540
577
|
WATCHDOG_INTERVAL=${LOKI_WATCHDOG_INTERVAL:-30} # Check interval in seconds
|
|
@@ -12951,6 +12988,67 @@ with open(os.environ["LOKI_DA_PROMPT_OUT"], "w", encoding="utf-8") as handle:
|
|
|
12951
12988
|
BUILD_DA_PROMPT
|
|
12952
12989
|
}
|
|
12953
12990
|
|
|
12991
|
+
# Derive a review size cap (in bytes) from the active provider's context window.
|
|
12992
|
+
#
|
|
12993
|
+
# Args: $1 = env override (wins outright when set), $2 = historical default.
|
|
12994
|
+
# Echoes the effective cap.
|
|
12995
|
+
#
|
|
12996
|
+
# Why this exists: the caps were fixed byte counts sized for a ~200k-token model.
|
|
12997
|
+
# A local 12b/14b with an 8k-32k window would be handed a 425000-byte prompt and
|
|
12998
|
+
# fail in a way that reads as "the model is bad" rather than "we mis-sized it".
|
|
12999
|
+
#
|
|
13000
|
+
# Two stated assumptions, kept separate so they stay auditable:
|
|
13001
|
+
# 1. ~3 bytes per token. Real tokenizers land around 3-4 for code-heavy text;
|
|
13002
|
+
# 3 is the conservative end, and under-estimating capacity errs toward a
|
|
13003
|
+
# smaller cap, which is the safe direction for a fail-closed gate.
|
|
13004
|
+
# 2. ~75% of the window is available for review INPUT. The remainder is the
|
|
13005
|
+
# reviewer's own output and reasoning, which share the same window.
|
|
13006
|
+
# Neither is precise, and neither needs to be: the result is only ever used to
|
|
13007
|
+
# LOWER a cap below the shipped default.
|
|
13008
|
+
#
|
|
13009
|
+
# The min() is load-bearing. PROVIDER_CONTEXT_WINDOW is set on every current run
|
|
13010
|
+
# (LOKI_PROVIDER defaults to claude, whose window is 1000000), so deriving
|
|
13011
|
+
# upward would raise the cap 4-8x for every existing user. Taking the smaller of
|
|
13012
|
+
# derived-vs-default means a cap can only ever move DOWN. Concretely, a window
|
|
13013
|
+
# clamps to the shipped default whenever it is >= ~188889 tokens; every provider
|
|
13014
|
+
# that declares a window today (1M/400k/200k/200k) clears that, and a provider
|
|
13015
|
+
# declaring none takes the unset path, so no shipped provider changes behavior.
|
|
13016
|
+
# Only a genuinely small window (a local 12b/14b) shrinks anything.
|
|
13017
|
+
#
|
|
13018
|
+
# The shipped defaults are 425000 (prompt) and 400000 (diff). That 25000-byte gap
|
|
13019
|
+
# is the reviewer scaffolding wrapped around the diff, and the ordering is
|
|
13020
|
+
# load-bearing: if both caps derived to the SAME number, a diff sized just under
|
|
13021
|
+
# the diff gate would build a prompt exceeding the prompt gate, so every review
|
|
13022
|
+
# would block fail-closed with no operator-visible cause, on exactly the
|
|
13023
|
+
# small-window providers this derivation exists to support. Scaling by
|
|
13024
|
+
# _default/425000 preserves that gap at every window size. The 425000 denominator
|
|
13025
|
+
# must track the prompt-cap default passed by the caller below; if that default
|
|
13026
|
+
# changes, change the denominator with it or the diff-cap proportion breaks.
|
|
13027
|
+
review_effective_cap() {
|
|
13028
|
+
local _override="$1" _default="$2"
|
|
13029
|
+
# Operator wins outright, at any value, over both the default and the window.
|
|
13030
|
+
if [ -n "$_override" ]; then
|
|
13031
|
+
printf '%s' "$_override"
|
|
13032
|
+
return 0
|
|
13033
|
+
fi
|
|
13034
|
+
# Fail safe to the historical default unless the window is a clean positive
|
|
13035
|
+
# integer. A non-numeric result here would make the caller's `[ ... -gt ... ]`
|
|
13036
|
+
# exit 2, which reads as false and would dispatch an oversized review.
|
|
13037
|
+
case "${PROVIDER_CONTEXT_WINDOW:-}" in
|
|
13038
|
+
''|*[!0-9]*) printf '%s' "$_default"; return 0 ;;
|
|
13039
|
+
esac
|
|
13040
|
+
[ "$PROVIDER_CONTEXT_WINDOW" -gt 0 ] 2>/dev/null || { printf '%s' "$_default"; return 0; }
|
|
13041
|
+
# Derive the INPUT budget, then scale it to this caller's cap so the
|
|
13042
|
+
# prompt/diff proportion (and thus the scaffolding gap) is preserved.
|
|
13043
|
+
local _budget=$(( PROVIDER_CONTEXT_WINDOW * 3 / 4 * 3 ))
|
|
13044
|
+
local _derived=$(( _budget * _default / 425000 ))
|
|
13045
|
+
if [ "$_derived" -lt "$_default" ]; then
|
|
13046
|
+
printf '%s' "$_derived"
|
|
13047
|
+
else
|
|
13048
|
+
printf '%s' "$_default"
|
|
13049
|
+
fi
|
|
13050
|
+
}
|
|
13051
|
+
|
|
12954
13052
|
run_code_review() {
|
|
12955
13053
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
12956
13054
|
local review_dir="$loki_dir/quality/reviews"
|
|
@@ -13311,11 +13409,12 @@ ${dependency_context}"
|
|
|
13311
13409
|
# produces an opaque block. This remains fail-closed and never truncates.
|
|
13312
13410
|
local _review_diff_bytes=0
|
|
13313
13411
|
_review_diff_bytes=$(printf '%s' "$diff_content" | wc -c | tr -d ' ')
|
|
13314
|
-
local _review_max_bytes
|
|
13412
|
+
local _review_max_bytes
|
|
13413
|
+
_review_max_bytes=$(review_effective_cap "${LOKI_REVIEW_MAX_DIFF_BYTES:-}" 400000)
|
|
13315
13414
|
if [ "${_review_diff_bytes:-0}" -gt "$_review_max_bytes" ] 2>/dev/null; then
|
|
13316
13415
|
local _big_dirs
|
|
13317
13416
|
_big_dirs=$(printf '%s\n' "$changed_files" | sed 's#/.*##' | grep -v '^$' | sort | uniq -c | sort -rn | head -3 | awk '{print $2" ("$1" files)"}' | tr '\n' ' ')
|
|
13318
|
-
log_error "Code review: context is ${_review_diff_bytes} bytes (limit ${_review_max_bytes}); refusing to truncate or dispatch a partial review. Biggest dirs: ${_big_dirs:-unknown}. Split the change or raise LOKI_REVIEW_MAX_DIFF_BYTES."
|
|
13417
|
+
log_error "Code review: context is ${_review_diff_bytes} bytes (limit ${_review_max_bytes}, derived from PROVIDER_CONTEXT_WINDOW=${PROVIDER_CONTEXT_WINDOW:-unset}); refusing to truncate or dispatch a partial review. Biggest dirs: ${_big_dirs:-unknown}. Split the change or raise LOKI_REVIEW_MAX_DIFF_BYTES."
|
|
13319
13418
|
emit_event_json "code_review_diff_oversized" \
|
|
13320
13419
|
"review_id=$review_id" \
|
|
13321
13420
|
"diff_bytes=$_review_diff_bytes" \
|
|
@@ -13847,7 +13946,8 @@ REVIEW_SELECTION_RECORD
|
|
|
13847
13946
|
reviewer_count=$(echo "$selected_specialists" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['reviewers']))")
|
|
13848
13947
|
local dispatch_count
|
|
13849
13948
|
dispatch_count=$(echo "$dispatch_specialists" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['reviewers']))")
|
|
13850
|
-
local _review_max_prompt_bytes
|
|
13949
|
+
local _review_max_prompt_bytes
|
|
13950
|
+
_review_max_prompt_bytes=$(review_effective_cap "${LOKI_REVIEW_MAX_PROMPT_BYTES:-}" 425000)
|
|
13851
13951
|
local _review_max_output_bytes="${LOKI_REVIEW_MAX_OUTPUT_BYTES:-1048576}"
|
|
13852
13952
|
local review_pending_dir="$review_dir/$review_id/.pending"
|
|
13853
13953
|
if ! mkdir -m 700 "$review_pending_dir" 2>/dev/null; then
|
|
@@ -15433,40 +15533,71 @@ start_dashboard() {
|
|
|
15433
15533
|
|
|
15434
15534
|
# Check all required imports
|
|
15435
15535
|
if ! "$python_cmd" -c "import fastapi; import sqlalchemy; import aiosqlite" 2>/dev/null; then
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
15536
|
+
# The venv is HOST-GLOBAL, so concurrent runs must not rebuild it at
|
|
15537
|
+
# once: one run's `rm -rf` would delete the tree another is importing
|
|
15538
|
+
# from. Lock the venv path itself -- safe_acquire_lock appends
|
|
15539
|
+
# ".lockdir", giving a SIBLING mutex the teardown below cannot destroy.
|
|
15540
|
+
# Timeout must outlast a real cold venv create + pip install (not the 5s
|
|
15541
|
+
# used by the JSON read-modify-write call sites). Mirrors the same guard
|
|
15542
|
+
# in ensure_dashboard_venv (autonomy/loki) -- edit BOTH.
|
|
15543
|
+
local _venv_locked=false
|
|
15544
|
+
if type safe_acquire_lock >/dev/null 2>&1 \
|
|
15545
|
+
&& safe_acquire_lock "$dashboard_venv" "${LOKI_VENV_LOCK_TIMEOUT:-300}"; then
|
|
15546
|
+
_venv_locked=true
|
|
15547
|
+
fi
|
|
15548
|
+
# Re-probe: the run we queued behind may have just built it for us.
|
|
15549
|
+
[ -x "${dashboard_venv}/bin/python3" ] && python_cmd="${dashboard_venv}/bin/python3"
|
|
15550
|
+
if "$python_cmd" -c "import fastapi; import sqlalchemy; import aiosqlite" 2>/dev/null; then
|
|
15551
|
+
[ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
|
|
15552
|
+
_venv_locked=false
|
|
15553
|
+
elif [ "$_venv_locked" = false ] && type safe_acquire_lock >/dev/null 2>&1; then
|
|
15554
|
+
# Lock timed out and the venv is still unusable. Do NOT rm -rf
|
|
15555
|
+
# unlocked -- that is the exact race this lock exists to prevent.
|
|
15556
|
+
#
|
|
15557
|
+
# The re-probe above may have pointed python_cmd at the OTHER run's
|
|
15558
|
+
# half-built venv (bin/python3 exists, pip install not finished).
|
|
15559
|
+
# Reset to the system interpreter so the server launch below does not
|
|
15560
|
+
# exec a knowingly-broken one.
|
|
15561
|
+
python_cmd="python3"
|
|
15562
|
+
log_warn "Timed out waiting for another run to build the dashboard venv"
|
|
15563
|
+
log_warn "Dashboard will not be available (stale lock? rm -rf ${dashboard_venv}.lockdir)"
|
|
15564
|
+
else
|
|
15565
|
+
log_step "Setting up dashboard virtualenv..."
|
|
15566
|
+
if ! [ -x "${dashboard_venv}/bin/python3" ]; then
|
|
15567
|
+
# Remove broken venv if exists
|
|
15568
|
+
[ -d "$dashboard_venv" ] && rm -rf "$dashboard_venv"
|
|
15569
|
+
mkdir -p "$HOME/.loki"
|
|
15570
|
+
python3 -m venv "$dashboard_venv" 2>/dev/null || python3.13 -m venv "$dashboard_venv" 2>/dev/null || {
|
|
15571
|
+
log_warn "Failed to create virtualenv"
|
|
15572
|
+
log_warn "You may need: sudo apt install python3-venv"
|
|
15573
|
+
}
|
|
15574
|
+
fi
|
|
15575
|
+
if [ -x "${dashboard_venv}/bin/python3" ]; then
|
|
15576
|
+
python_cmd="${dashboard_venv}/bin/python3"
|
|
15577
|
+
log_step "Installing dashboard dependencies..."
|
|
15578
|
+
if [ -f "$req_file" ]; then
|
|
15579
|
+
"${dashboard_venv}/bin/pip" install -r "$req_file" 2>&1 | tail -1 || {
|
|
15580
|
+
log_warn "Pinned deps failed, trying unpinned..."
|
|
15581
|
+
"${dashboard_venv}/bin/pip" install fastapi uvicorn pydantic websockets sqlalchemy aiosqlite 2>&1 | tail -1 || {
|
|
15582
|
+
log_warn "Failed to install dashboard dependencies"
|
|
15583
|
+
log_warn "Dashboard will not be available"
|
|
15584
|
+
}
|
|
15585
|
+
# greenlet is optional (needs C compiler on some platforms)
|
|
15586
|
+
"${dashboard_venv}/bin/pip" install greenlet 2>/dev/null || true
|
|
15587
|
+
}
|
|
15588
|
+
else
|
|
15452
15589
|
"${dashboard_venv}/bin/pip" install fastapi uvicorn pydantic websockets sqlalchemy aiosqlite 2>&1 | tail -1 || {
|
|
15453
15590
|
log_warn "Failed to install dashboard dependencies"
|
|
15454
15591
|
log_warn "Dashboard will not be available"
|
|
15455
15592
|
}
|
|
15456
|
-
# greenlet is optional (needs C compiler on some platforms)
|
|
15457
15593
|
"${dashboard_venv}/bin/pip" install greenlet 2>/dev/null || true
|
|
15458
|
-
|
|
15594
|
+
fi
|
|
15459
15595
|
else
|
|
15460
|
-
"
|
|
15461
|
-
|
|
15462
|
-
log_warn "Dashboard will not be available"
|
|
15463
|
-
}
|
|
15464
|
-
"${dashboard_venv}/bin/pip" install greenlet 2>/dev/null || true
|
|
15596
|
+
log_warn "Failed to install dashboard dependencies"
|
|
15597
|
+
log_warn "Run manually: python3 -m venv ${dashboard_venv} && ${dashboard_venv}/bin/pip install fastapi uvicorn sqlalchemy aiosqlite"
|
|
15465
15598
|
fi
|
|
15466
|
-
else
|
|
15467
|
-
log_warn "Failed to install dashboard dependencies"
|
|
15468
|
-
log_warn "Run manually: python3 -m venv ${dashboard_venv} && ${dashboard_venv}/bin/pip install fastapi uvicorn sqlalchemy aiosqlite"
|
|
15469
15599
|
fi
|
|
15600
|
+
[ "$_venv_locked" = true ] && safe_release_lock "$dashboard_venv"
|
|
15470
15601
|
fi
|
|
15471
15602
|
|
|
15472
15603
|
# Start the FastAPI dashboard server
|
|
@@ -20750,6 +20881,16 @@ except Exception as exc:
|
|
|
20750
20881
|
# Trim to last 500KB
|
|
20751
20882
|
tail -c 500000 "$agent_log" > "$agent_log.tmp" && mv "$agent_log.tmp" "$agent_log"
|
|
20752
20883
|
fi
|
|
20884
|
+
|
|
20885
|
+
# Same cap on the daily log. agent.log has been trimmed since it was
|
|
20886
|
+
# introduced; its sibling never was, and it receives the full raw
|
|
20887
|
+
# stream-json of every iteration -- measured ~1.5MB per iteration, so a
|
|
20888
|
+
# 500-iteration run leaves ~725MB per day per build, times however many
|
|
20889
|
+
# builds share the machine. Same threshold, same trim, no new rotation
|
|
20890
|
+
# scheme.
|
|
20891
|
+
if [ -f "$log_file" ] && [ "$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null)" -gt 1000000 ]; then
|
|
20892
|
+
tail -c 500000 "$log_file" > "$log_file.tmp" && mv "$log_file.tmp" "$log_file"
|
|
20893
|
+
fi
|
|
20753
20894
|
touch "$agent_log"
|
|
20754
20895
|
echo "" >> "$agent_log"
|
|
20755
20896
|
echo "════════════════════════════════════════════════════════════════" >> "$agent_log"
|