loki-mode 7.41.4 → 7.42.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/README.md +18 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +143 -37
- package/autonomy/hooks/migration-hooks.sh +131 -7
- package/autonomy/loki +54 -43
- package/autonomy/run.sh +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +102 -0
- package/docs/INSTALLATION.md +70 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +274 -89
- package/memory/engine.py +15 -3
- package/memory/storage.py +6 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/core-workflow.md +7 -0
- package/references/quality-control.md +6 -0
- package/skills/agents.md +1 -0
package/autonomy/loki
CHANGED
|
@@ -13178,13 +13178,18 @@ FEOF
|
|
|
13178
13178
|
;;
|
|
13179
13179
|
--disable)
|
|
13180
13180
|
if [ -f "$failover_file" ]; then
|
|
13181
|
-
python3 -c "
|
|
13182
|
-
import json
|
|
13183
|
-
|
|
13181
|
+
if _FAILOVER_FILE="$failover_file" python3 -c "
|
|
13182
|
+
import json, os
|
|
13183
|
+
failover_file = os.environ['_FAILOVER_FILE']
|
|
13184
|
+
with open(failover_file) as f: d = json.load(f)
|
|
13184
13185
|
d['enabled'] = False
|
|
13185
|
-
with open(
|
|
13186
|
-
"
|
|
13187
|
-
|
|
13186
|
+
with open(failover_file, 'w') as f: json.dump(d, f, indent=2)
|
|
13187
|
+
"; then
|
|
13188
|
+
echo -e "${YELLOW}Failover disabled${NC}"
|
|
13189
|
+
else
|
|
13190
|
+
echo -e "${RED}Error: failed to disable failover${NC}"
|
|
13191
|
+
return 1
|
|
13192
|
+
fi
|
|
13188
13193
|
else
|
|
13189
13194
|
echo "Failover not initialized."
|
|
13190
13195
|
fi
|
|
@@ -13212,13 +13217,19 @@ with open('$failover_file', 'w') as f: json.dump(d, f, indent=2)
|
|
|
13212
13217
|
return 1
|
|
13213
13218
|
fi
|
|
13214
13219
|
|
|
13215
|
-
python3 -c "
|
|
13216
|
-
import json
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
with open(
|
|
13220
|
-
|
|
13221
|
-
|
|
13220
|
+
if _FAILOVER_FILE="$failover_file" _NEW_CHAIN="$new_chain" python3 -c "
|
|
13221
|
+
import json, os
|
|
13222
|
+
failover_file = os.environ['_FAILOVER_FILE']
|
|
13223
|
+
new_chain = os.environ['_NEW_CHAIN']
|
|
13224
|
+
with open(failover_file) as f: d = json.load(f)
|
|
13225
|
+
d['chain'] = new_chain.split(',')
|
|
13226
|
+
with open(failover_file, 'w') as f: json.dump(d, f, indent=2)
|
|
13227
|
+
"; then
|
|
13228
|
+
echo "Failover chain updated: $new_chain"
|
|
13229
|
+
else
|
|
13230
|
+
echo -e "${RED}Error: failed to update failover chain${NC}"
|
|
13231
|
+
return 1
|
|
13232
|
+
fi
|
|
13222
13233
|
shift
|
|
13223
13234
|
;;
|
|
13224
13235
|
--test)
|
|
@@ -18601,16 +18612,16 @@ else:
|
|
|
18601
18612
|
exit 1
|
|
18602
18613
|
fi
|
|
18603
18614
|
|
|
18604
|
-
python3 -c "
|
|
18615
|
+
_REGISTRY_FILE="$registry_file" _PROJ_PATH="$path" _PROJ_NAME="$name" _PROJ_ALIAS="$alias" python3 -c "
|
|
18605
18616
|
import json
|
|
18606
18617
|
import os
|
|
18607
18618
|
import hashlib
|
|
18608
18619
|
from datetime import datetime, timezone
|
|
18609
18620
|
|
|
18610
|
-
registry_file = '
|
|
18611
|
-
path = '
|
|
18612
|
-
name = '
|
|
18613
|
-
alias = '
|
|
18621
|
+
registry_file = os.environ['_REGISTRY_FILE']
|
|
18622
|
+
path = os.environ['_PROJ_PATH']
|
|
18623
|
+
name = os.environ['_PROJ_NAME'] or os.path.basename(path)
|
|
18624
|
+
alias = os.environ['_PROJ_ALIAS'] or None
|
|
18614
18625
|
|
|
18615
18626
|
# Generate project ID
|
|
18616
18627
|
project_id = hashlib.md5(path.encode()).hexdigest()[:12]
|
|
@@ -18651,7 +18662,7 @@ with open(registry_file, 'w') as f:
|
|
|
18651
18662
|
print(f' Path: {path}')
|
|
18652
18663
|
if alias:
|
|
18653
18664
|
print(f' Alias: {alias}')
|
|
18654
|
-
"
|
|
18665
|
+
"
|
|
18655
18666
|
;;
|
|
18656
18667
|
|
|
18657
18668
|
remove|rm)
|
|
@@ -18662,12 +18673,12 @@ if alias:
|
|
|
18662
18673
|
exit 1
|
|
18663
18674
|
fi
|
|
18664
18675
|
|
|
18665
|
-
python3 -c "
|
|
18676
|
+
_REGISTRY_FILE="$registry_file" _IDENTIFIER="$identifier" python3 -c "
|
|
18666
18677
|
import json
|
|
18667
18678
|
import os
|
|
18668
18679
|
|
|
18669
|
-
registry_file = '
|
|
18670
|
-
identifier = '
|
|
18680
|
+
registry_file = os.environ['_REGISTRY_FILE']
|
|
18681
|
+
identifier = os.environ['_IDENTIFIER']
|
|
18671
18682
|
|
|
18672
18683
|
with open(registry_file, 'r') as f:
|
|
18673
18684
|
data = json.load(f)
|
|
@@ -18690,7 +18701,7 @@ if found_id:
|
|
|
18690
18701
|
else:
|
|
18691
18702
|
print(f'Not found: {identifier}')
|
|
18692
18703
|
exit(1)
|
|
18693
|
-
"
|
|
18704
|
+
"
|
|
18694
18705
|
;;
|
|
18695
18706
|
|
|
18696
18707
|
discover)
|
|
@@ -18842,12 +18853,12 @@ print(f'Added: {added}, Missing: {missing}, Total: {len(projects)}')
|
|
|
18842
18853
|
health)
|
|
18843
18854
|
local identifier="${2:-$(pwd)}"
|
|
18844
18855
|
|
|
18845
|
-
python3 -c "
|
|
18856
|
+
_REGISTRY_FILE="$registry_file" _IDENTIFIER="$identifier" python3 -c "
|
|
18846
18857
|
import json
|
|
18847
18858
|
import os
|
|
18848
18859
|
|
|
18849
|
-
registry_file = '
|
|
18850
|
-
identifier = '
|
|
18860
|
+
registry_file = os.environ['_REGISTRY_FILE']
|
|
18861
|
+
identifier = os.environ['_IDENTIFIER']
|
|
18851
18862
|
|
|
18852
18863
|
# If it's a path, resolve it
|
|
18853
18864
|
if os.path.isdir(identifier):
|
|
@@ -18886,7 +18897,7 @@ print('Health Checks:')
|
|
|
18886
18897
|
for check, passed in checks.items():
|
|
18887
18898
|
icon = '[OK]' if passed else '[FAIL]'
|
|
18888
18899
|
print(f' {icon} {check}')
|
|
18889
|
-
"
|
|
18900
|
+
"
|
|
18890
18901
|
;;
|
|
18891
18902
|
|
|
18892
18903
|
--help|-h|help)
|
|
@@ -19040,17 +19051,17 @@ cmd_enterprise() {
|
|
|
19040
19051
|
esac
|
|
19041
19052
|
done
|
|
19042
19053
|
|
|
19043
|
-
python3 -c "
|
|
19054
|
+
_TOKEN_FILE="$token_file" _TOKEN_NAME="$name" _TOKEN_SCOPES="$scopes" _TOKEN_EXPIRES="$expires" python3 -c "
|
|
19044
19055
|
import json
|
|
19045
19056
|
import secrets
|
|
19046
19057
|
import hashlib
|
|
19047
19058
|
from datetime import datetime, timezone, timedelta
|
|
19048
19059
|
import os
|
|
19049
19060
|
|
|
19050
|
-
token_file = '
|
|
19051
|
-
name = '
|
|
19052
|
-
scopes_str = '
|
|
19053
|
-
expires_str = '
|
|
19061
|
+
token_file = os.environ['_TOKEN_FILE']
|
|
19062
|
+
name = os.environ['_TOKEN_NAME']
|
|
19063
|
+
scopes_str = os.environ['_TOKEN_SCOPES']
|
|
19064
|
+
expires_str = os.environ['_TOKEN_EXPIRES']
|
|
19054
19065
|
|
|
19055
19066
|
# Parse scopes
|
|
19056
19067
|
scopes = scopes_str.split(',') if scopes_str else ['*']
|
|
@@ -19105,7 +19116,7 @@ if expires_at:
|
|
|
19105
19116
|
print('')
|
|
19106
19117
|
print('Token (save this - shown only once):')
|
|
19107
19118
|
print(f' {raw_token}')
|
|
19108
|
-
"
|
|
19119
|
+
"
|
|
19109
19120
|
;;
|
|
19110
19121
|
|
|
19111
19122
|
list|ls)
|
|
@@ -19174,12 +19185,12 @@ else:
|
|
|
19174
19185
|
exit 2
|
|
19175
19186
|
fi
|
|
19176
19187
|
|
|
19177
|
-
python3 -c "
|
|
19178
|
-
import json
|
|
19188
|
+
_TOKEN_FILE="$token_file" _IDENTIFIER="$identifier" python3 -c "
|
|
19189
|
+
import json, os
|
|
19179
19190
|
from datetime import datetime, timezone
|
|
19180
19191
|
|
|
19181
|
-
token_file = '
|
|
19182
|
-
identifier = '
|
|
19192
|
+
token_file = os.environ['_TOKEN_FILE']
|
|
19193
|
+
identifier = os.environ['_IDENTIFIER']
|
|
19183
19194
|
|
|
19184
19195
|
with open(token_file, 'r') as f:
|
|
19185
19196
|
data = json.load(f)
|
|
@@ -19202,7 +19213,7 @@ if found_id:
|
|
|
19202
19213
|
else:
|
|
19203
19214
|
print(f'Token not found: {identifier}')
|
|
19204
19215
|
exit(1)
|
|
19205
|
-
"
|
|
19216
|
+
"
|
|
19206
19217
|
;;
|
|
19207
19218
|
|
|
19208
19219
|
delete)
|
|
@@ -19213,11 +19224,11 @@ else:
|
|
|
19213
19224
|
exit 2
|
|
19214
19225
|
fi
|
|
19215
19226
|
|
|
19216
|
-
python3 -c "
|
|
19217
|
-
import json
|
|
19227
|
+
_TOKEN_FILE="$token_file" _IDENTIFIER="$identifier" python3 -c "
|
|
19228
|
+
import json, os
|
|
19218
19229
|
|
|
19219
|
-
token_file = '
|
|
19220
|
-
identifier = '
|
|
19230
|
+
token_file = os.environ['_TOKEN_FILE']
|
|
19231
|
+
identifier = os.environ['_IDENTIFIER']
|
|
19221
19232
|
|
|
19222
19233
|
with open(token_file, 'r') as f:
|
|
19223
19234
|
data = json.load(f)
|
|
@@ -19241,7 +19252,7 @@ if found_id:
|
|
|
19241
19252
|
else:
|
|
19242
19253
|
print(f'Token not found: {identifier}')
|
|
19243
19254
|
exit(1)
|
|
19244
|
-
"
|
|
19255
|
+
"
|
|
19245
19256
|
;;
|
|
19246
19257
|
|
|
19247
19258
|
*)
|
package/autonomy/run.sh
CHANGED
|
@@ -1003,7 +1003,7 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
|
1003
1003
|
log_warning() { log_warn "$@"; } # Alias for backwards compatibility
|
|
1004
1004
|
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
|
1005
1005
|
log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
|
1006
|
-
log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" || true; }
|
|
1006
|
+
log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2 || true; }
|
|
1007
1007
|
|
|
1008
1008
|
#===============================================================================
|
|
1009
1009
|
# Process Registry (PID Supervisor)
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -7034,6 +7034,96 @@ def _pid_is_alive(pid):
|
|
|
7034
7034
|
return None
|
|
7035
7035
|
|
|
7036
7036
|
|
|
7037
|
+
# Margin (seconds) added to the recorded reference time before a live pid is
|
|
7038
|
+
# judged to be a recycled (different) process. Must comfortably exceed clock
|
|
7039
|
+
# skew plus the launch-to-first-state-write gap so a genuine app is never
|
|
7040
|
+
# downgraded. A PID recycled after a crash typically belongs to a process that
|
|
7041
|
+
# started minutes or hours later, so a generous margin still catches recycles
|
|
7042
|
+
# while strongly biasing against the far worse false-positive of killing a live
|
|
7043
|
+
# app's status. See _reconcile_app_runner_liveness.
|
|
7044
|
+
_APP_RUNNER_PID_RECYCLE_MARGIN_SECONDS = 120
|
|
7045
|
+
|
|
7046
|
+
|
|
7047
|
+
def _pid_start_time(pid):
|
|
7048
|
+
"""Best-effort wall-clock start time of pid, as epoch seconds, or None.
|
|
7049
|
+
|
|
7050
|
+
Reads `ps -o lstart= -p <pid>`, which is available on both macOS and Linux
|
|
7051
|
+
and prints the process start time in local time (e.g. "Sun Jun 14 18:39:15
|
|
7052
|
+
2026"). The string is locale-dependent (%a/%b), so any parse failure, empty
|
|
7053
|
+
output, or missing process returns None and the caller degrades gracefully
|
|
7054
|
+
to its prior behavior. The returned epoch is timezone-correct because the
|
|
7055
|
+
naive local timestamp is interpreted in the system's local zone before
|
|
7056
|
+
conversion (ps reports local time; never mix it with a UTC value directly).
|
|
7057
|
+
"""
|
|
7058
|
+
try:
|
|
7059
|
+
pid = int(pid)
|
|
7060
|
+
except (TypeError, ValueError):
|
|
7061
|
+
return None
|
|
7062
|
+
if pid <= 0:
|
|
7063
|
+
return None
|
|
7064
|
+
try:
|
|
7065
|
+
out = subprocess.run(["ps", "-o", "lstart=", "-p", str(pid)],
|
|
7066
|
+
capture_output=True, text=True, timeout=5)
|
|
7067
|
+
except (OSError, subprocess.SubprocessError):
|
|
7068
|
+
return None
|
|
7069
|
+
raw = (out.stdout or "").strip()
|
|
7070
|
+
if not raw:
|
|
7071
|
+
return None
|
|
7072
|
+
try:
|
|
7073
|
+
# lstart is local time without a zone; parse naive then attach the
|
|
7074
|
+
# local zone so .timestamp() yields a correct epoch regardless of TZ.
|
|
7075
|
+
naive = datetime.strptime(raw, "%a %b %d %H:%M:%S %Y")
|
|
7076
|
+
local = naive.replace(tzinfo=datetime.now().astimezone().tzinfo)
|
|
7077
|
+
return local.timestamp()
|
|
7078
|
+
except (ValueError, OverflowError, OSError):
|
|
7079
|
+
return None
|
|
7080
|
+
|
|
7081
|
+
|
|
7082
|
+
def _state_reference_epoch(state):
|
|
7083
|
+
"""Epoch seconds for state.json's recorded reference time, or None.
|
|
7084
|
+
|
|
7085
|
+
Uses `started_at` (rewritten by the app-runner on every state write; it is
|
|
7086
|
+
the last-state-write time, not pure launch time). For a genuine process the
|
|
7087
|
+
real start time is always <= this value, so it is a safe upper bound to
|
|
7088
|
+
compare a live pid's start time against. The value is UTC (Z-suffixed).
|
|
7089
|
+
"""
|
|
7090
|
+
if not isinstance(state, dict):
|
|
7091
|
+
return None
|
|
7092
|
+
started_at = state.get("started_at")
|
|
7093
|
+
if not started_at:
|
|
7094
|
+
return None
|
|
7095
|
+
try:
|
|
7096
|
+
ts = datetime.fromisoformat(str(started_at).replace("Z", "+00:00"))
|
|
7097
|
+
except (ValueError, TypeError):
|
|
7098
|
+
return None
|
|
7099
|
+
if ts.tzinfo is None:
|
|
7100
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
7101
|
+
return ts.timestamp()
|
|
7102
|
+
|
|
7103
|
+
|
|
7104
|
+
def _pid_is_recycled(state):
|
|
7105
|
+
"""True if the recorded main_pid is alive but is a DIFFERENT process now.
|
|
7106
|
+
|
|
7107
|
+
After the recorded app dies, the OS can recycle its numeric pid for an
|
|
7108
|
+
unrelated process; os.kill(pid, 0) then reports the stale pid "alive"
|
|
7109
|
+
forever and a dead run is never reconciled. We detect this by comparing the
|
|
7110
|
+
live pid's real start time against the recorded reference time: a genuine
|
|
7111
|
+
process started at or before the reference, so a live pid whose start time
|
|
7112
|
+
is comfortably AFTER the reference cannot be the original.
|
|
7113
|
+
|
|
7114
|
+
Returns True only with positive evidence of recycling. Any missing data
|
|
7115
|
+
(no recorded reference, start time unavailable) returns False so the caller
|
|
7116
|
+
keeps its prior behavior -- best-effort, biased against false positives.
|
|
7117
|
+
"""
|
|
7118
|
+
reference = _state_reference_epoch(state)
|
|
7119
|
+
if reference is None:
|
|
7120
|
+
return False
|
|
7121
|
+
pid_start = _pid_start_time(state.get("main_pid"))
|
|
7122
|
+
if pid_start is None:
|
|
7123
|
+
return False
|
|
7124
|
+
return pid_start > reference + _APP_RUNNER_PID_RECYCLE_MARGIN_SECONDS
|
|
7125
|
+
|
|
7126
|
+
|
|
7037
7127
|
def _health_checked_age_seconds(state):
|
|
7038
7128
|
"""Seconds since last_health.checked_at, or None if unparseable/absent."""
|
|
7039
7129
|
health = state.get("last_health")
|
|
@@ -7059,6 +7149,9 @@ def _reconcile_app_runner_liveness(state):
|
|
|
7059
7149
|
Here we cross-check the recorded main_pid against the real OS before
|
|
7060
7150
|
returning, and only ever downgrade -- never upgrade -- the status:
|
|
7061
7151
|
- recorded running/starting + pid genuinely gone -> "stopped"
|
|
7152
|
+
- recorded running/starting + pid "alive" but its real start time is
|
|
7153
|
+
after the recorded reference (the OS recycled a dead run's pid for an
|
|
7154
|
+
unrelated process) -> "stopped"
|
|
7062
7155
|
- recorded running/starting + pid not verifiable +
|
|
7063
7156
|
last_health.checked_at older than the threshold -> "stale"
|
|
7064
7157
|
Any failure falls back to the raw recorded status (fail open to the writer's
|
|
@@ -7076,6 +7169,15 @@ def _reconcile_app_runner_liveness(state):
|
|
|
7076
7169
|
state["status"] = "stopped"
|
|
7077
7170
|
state["liveness"] = "pid_gone"
|
|
7078
7171
|
return state
|
|
7172
|
+
if alive is True:
|
|
7173
|
+
# The numeric pid exists, but os.kill(pid, 0) cannot tell whether it
|
|
7174
|
+
# is still the SAME process. After a dead run the OS can recycle the
|
|
7175
|
+
# pid; detect that via the process start time so a recycled pid is
|
|
7176
|
+
# treated as gone rather than reported "running" forever.
|
|
7177
|
+
if _pid_is_recycled(state):
|
|
7178
|
+
state["status"] = "stopped"
|
|
7179
|
+
state["liveness"] = "pid_recycled"
|
|
7180
|
+
return state
|
|
7079
7181
|
if alive is None:
|
|
7080
7182
|
# Cannot verify via pid (e.g. compose subshell pid). Fall back to
|
|
7081
7183
|
# the health-beat freshness with a generous threshold.
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.42.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -63,6 +63,7 @@ review verdict, evidence-related parses) so determinism is never affected.
|
|
|
63
63
|
- [VS Code Extension (Deprecated)](#vs-code-extension-deprecated)
|
|
64
64
|
- [Sandbox Mode](#sandbox-mode)
|
|
65
65
|
- [Multi-Provider Support](#multi-provider-support)
|
|
66
|
+
- [Environment Variables](#environment-variables)
|
|
66
67
|
- [Claude Code (CLI)](#claude-code-cli)
|
|
67
68
|
- [Claude.ai (Web)](#claudeai-web)
|
|
68
69
|
- [Anthropic API Console](#anthropic-api-console)
|
|
@@ -367,6 +368,74 @@ When using `codex`, `cline`, or `aider` providers, Loki Mode operates in **degra
|
|
|
367
368
|
|
|
368
369
|
---
|
|
369
370
|
|
|
371
|
+
## Environment Variables
|
|
372
|
+
|
|
373
|
+
Loki Mode is designed to run with zero configuration: the trust-layer and
|
|
374
|
+
quality features below are default-on and decide intelligently by inspecting
|
|
375
|
+
the work. The environment variables here are opt-out escape hatches for power
|
|
376
|
+
users, not required setup. Set the documented value to disable a feature; leave
|
|
377
|
+
the variable unset to keep the intelligent default.
|
|
378
|
+
|
|
379
|
+
### Trust-gate and completion knobs (default-on)
|
|
380
|
+
|
|
381
|
+
These are read by the orchestrator (`autonomy/run.sh`) on every run.
|
|
382
|
+
|
|
383
|
+
- `LOKI_REVIEW_INCONCLUSIVE_BLOCK` (default `1`) -- when a code-review cycle
|
|
384
|
+
returns zero usable verdicts (every reviewer produced empty output), the
|
|
385
|
+
review is treated as INCONCLUSIVE and the gate BLOCKS, because an all-empty
|
|
386
|
+
review proves nothing. A bounded one-shot retry runs first
|
|
387
|
+
(`LOKI_REVIEW_RETRY`, default `1`). Set `LOKI_REVIEW_INCONCLUSIVE_BLOCK=0` to
|
|
388
|
+
record the inconclusive result without blocking.
|
|
389
|
+
|
|
390
|
+
- `LOKI_COMPLETION_TEST_CAPTURE` (default `1`) -- before the verified-completion
|
|
391
|
+
evidence gate runs, Loki captures a fresh `test-results.json` so the gate
|
|
392
|
+
scores on real PASS/FAIL test results instead of a stale or missing file. It
|
|
393
|
+
reuses this iteration's results if already fresh, and never crashes the
|
|
394
|
+
completion path on red tests (the gate is the decider). Set
|
|
395
|
+
`LOKI_COMPLETION_TEST_CAPTURE=0` to opt out.
|
|
396
|
+
|
|
397
|
+
- `LOKI_AUTO_DOCS` (default `true`) -- auto-generates the `.loki/docs/` suite
|
|
398
|
+
before the documentation gate evaluates, so the gate scores on real generated
|
|
399
|
+
docs instead of nagging you to run `loki docs generate` by hand. Bounded:
|
|
400
|
+
runs at most once per run when docs are missing, and again only when existing
|
|
401
|
+
docs are substantially stale; best-effort, never fails the iteration loop.
|
|
402
|
+
Set `LOKI_AUTO_DOCS=false` to opt out.
|
|
403
|
+
|
|
404
|
+
### Output-token compressor (caveman, Claude-only)
|
|
405
|
+
|
|
406
|
+
Loki integrates [caveman](https://github.com/JuliusBrussee/caveman), an optional
|
|
407
|
+
Claude Code skill that compresses the model's OUTPUT tokens only (keeping all
|
|
408
|
+
technical substance). It activates on free-form generation (the main RARV dev
|
|
409
|
+
loop) and is HARD-SUPPRESSED on every trust-gate subcall (council votes, code
|
|
410
|
+
review verdicts, evidence-related parses) so determinism is never affected. It
|
|
411
|
+
is Claude-provider-only; runs are byte-identical on Codex / Cline / Aider. These
|
|
412
|
+
variables are read in `autonomy/lib/claude-flags.sh`.
|
|
413
|
+
|
|
414
|
+
- `LOKI_CAVEMAN` (default on) -- set `LOKI_CAVEMAN=0` to disable the compressor.
|
|
415
|
+
Suppression on trust-gate subcalls is unconditional and applies even when
|
|
416
|
+
caveman is globally installed but `LOKI_CAVEMAN=0`, so trust gates are never
|
|
417
|
+
exposed to compression.
|
|
418
|
+
|
|
419
|
+
- `LOKI_CAVEMAN_LEVEL` (default `full`) -- the compression level for free-form
|
|
420
|
+
activation. When you do NOT set this, the level is inferred per-invocation
|
|
421
|
+
from the run's RARV tier (planning -> `lite`, development/fast -> `full`); the
|
|
422
|
+
auto path never selects `ultra`. Setting `LOKI_CAVEMAN_LEVEL` explicitly
|
|
423
|
+
overrides the inference entirely (the opt-out escape hatch).
|
|
424
|
+
|
|
425
|
+
- `LOKI_CAVEMAN_VERSION` (default `1.9.0`) -- the pinned caveman version used by
|
|
426
|
+
the one-time bootstrap. Bump only to upgrade the compressor.
|
|
427
|
+
|
|
428
|
+
### RARV-C closure knobs (default-on)
|
|
429
|
+
|
|
430
|
+
The Phase 1 / RARV-C closure loop (findings injection, override council,
|
|
431
|
+
learnings writer, handoff doc) is default-on and documented in detail at the
|
|
432
|
+
top of this guide under [Phase 1 RARV-C closure](#phase-1-rarv-c-closure-shipped-v750-default-on-as-of-v753):
|
|
433
|
+
`LOKI_INJECT_FINDINGS`, `LOKI_OVERRIDE_COUNCIL`, `LOKI_AUTO_LEARNINGS`, and
|
|
434
|
+
`LOKI_HANDOFF_MD` (each opt out with `=0`). For the full schema and
|
|
435
|
+
reachability notes, see `skills/quality-gates.md`.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
370
439
|
## Claude Code (CLI)
|
|
371
440
|
|
|
372
441
|
Loki Mode can be installed as a skill in three ways:
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.
|
|
2
|
+
var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.42.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
|
|
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)
|
|
@@ -789,4 +789,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
789
789
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
790
790
|
`),process.stderr.write(o6),2}}p1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var ZZ=await QZ(Bun.argv.slice(2));process.exit(ZZ);
|
|
791
791
|
|
|
792
|
-
//# debugId=
|
|
792
|
+
//# debugId=D7F92E946CD3E45564756E2164756E21
|
package/mcp/__init__.py
CHANGED