loki-mode 8.6.1 → 8.8.1
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 +199 -29
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +250 -19
- package/dashboard/static/index.html +10 -9
- package/docs/environment-variables.md +36 -4
- package/loki-ts/data/model-pricing.json +35 -10
- 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/codex.sh +21 -3
- package/providers/model_catalog.json +33 -10
- 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.1
|
|
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.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.8.1
|
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
|