loki-mode 7.67.0 → 7.68.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 +110 -37
- package/autonomy/run.sh +39 -54
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +25 -4
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +15 -1
- package/memory/retrieval.py +18 -1
- package/memory/storage.py +136 -0
- 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.68.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.68.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.68.0
|
package/autonomy/loki
CHANGED
|
@@ -3071,7 +3071,7 @@ cmd_status_json() {
|
|
|
3071
3071
|
local dashboard_port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
3072
3072
|
local env_provider="${LOKI_PROVIDER:-claude}"
|
|
3073
3073
|
|
|
3074
|
-
python3 -c "
|
|
3074
|
+
if ! python3 -c "
|
|
3075
3075
|
import json, os, sys, time
|
|
3076
3076
|
|
|
3077
3077
|
skill_dir = sys.argv[1]
|
|
@@ -3359,9 +3359,14 @@ if os.path.isfile(gate_count_file):
|
|
|
3359
3359
|
result['phase1'] = phase1
|
|
3360
3360
|
|
|
3361
3361
|
print(json.dumps(result, indent=2))
|
|
3362
|
-
" "$skill_dir" "$loki_dir" "$dashboard_port" "$env_provider"
|
|
3363
|
-
|
|
3364
|
-
|
|
3362
|
+
" "$skill_dir" "$loki_dir" "$dashboard_port" "$env_provider"; then
|
|
3363
|
+
# WAVE8 loki-F2: under `set -euo pipefail` a bare python3 call aborts
|
|
3364
|
+
# the whole function on non-zero exit, so the old post-call
|
|
3365
|
+
# `if [ $? -ne 0 ]` fallback was DEAD code -- a missing/broken python3
|
|
3366
|
+
# crashed `loki status --json` instead of degrading. Guarding the call
|
|
3367
|
+
# with `if ! ...; then` catches the non-zero exit and emits the honest
|
|
3368
|
+
# error object. (Most malformed state files already degrade internally
|
|
3369
|
+
# via per-file try/except; this covers the interpreter-failure case.)
|
|
3365
3370
|
echo '{"error": "Failed to generate JSON status. Ensure python3 is available."}' >&2
|
|
3366
3371
|
return 1
|
|
3367
3372
|
fi
|
|
@@ -12485,18 +12490,27 @@ for f in data.get('frictions', []):
|
|
|
12485
12490
|
|
|
12486
12491
|
# Initialize or update healing progress
|
|
12487
12492
|
if [[ ! -f "$heal_dir/healing-progress.json" ]] || [ "$do_resume" != "true" ]; then
|
|
12493
|
+
# WAVE8 loki-est: pass codebase/phase/strict/out-path via env instead of
|
|
12494
|
+
# interpolating raw bash into the python source. A codebase path or phase
|
|
12495
|
+
# containing an apostrophe made this a SyntaxError; under `|| true` the
|
|
12496
|
+
# progress file was then silently never written (and the later
|
|
12497
|
+
# prev_phase read would fail), breaking healing resume.
|
|
12498
|
+
LOKI_HEAL_CODEBASE="$codebase_path" \
|
|
12499
|
+
LOKI_HEAL_PHASE_VAL="$phase" \
|
|
12500
|
+
LOKI_HEAL_STRICT_VAL="$strict" \
|
|
12501
|
+
LOKI_HEAL_OUT="$heal_dir/healing-progress.json" \
|
|
12488
12502
|
python3 -c "
|
|
12489
|
-
import json
|
|
12503
|
+
import json, os
|
|
12490
12504
|
from datetime import datetime
|
|
12491
12505
|
progress = {
|
|
12492
|
-
'codebase': '
|
|
12506
|
+
'codebase': os.environ.get('LOKI_HEAL_CODEBASE', ''),
|
|
12493
12507
|
'started': datetime.now().isoformat(),
|
|
12494
|
-
'current_phase': '
|
|
12495
|
-
'strict_mode':
|
|
12508
|
+
'current_phase': os.environ.get('LOKI_HEAL_PHASE_VAL', ''),
|
|
12509
|
+
'strict_mode': os.environ.get('LOKI_HEAL_STRICT_VAL', '') == 'true',
|
|
12496
12510
|
'components': [],
|
|
12497
12511
|
'overall_health': 0.0
|
|
12498
12512
|
}
|
|
12499
|
-
with open(
|
|
12513
|
+
with open(os.environ['LOKI_HEAL_OUT'], 'w') as f:
|
|
12500
12514
|
json.dump(progress, f, indent=2)
|
|
12501
12515
|
" || true
|
|
12502
12516
|
fi
|
|
@@ -12504,7 +12518,7 @@ with open('$heal_dir/healing-progress.json', 'w') as f:
|
|
|
12504
12518
|
# BUG-HEAL-004: Validate phase gate when resuming from a previous phase
|
|
12505
12519
|
if [ "$do_resume" = "true" ] && [[ -f "$heal_dir/healing-progress.json" ]] && type hook_healing_phase_gate &>/dev/null; then
|
|
12506
12520
|
local prev_phase
|
|
12507
|
-
prev_phase=$(python3 -c "import json; print(json.load(open(
|
|
12521
|
+
prev_phase=$(LOKI_HEAL_PROG="$heal_dir/healing-progress.json" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_HEAL_PROG'])).get('current_phase', 'archaeology'))" 2>/dev/null || echo "archaeology")
|
|
12508
12522
|
if [[ "$prev_phase" != "$phase" ]]; then
|
|
12509
12523
|
local gate_result
|
|
12510
12524
|
if ! gate_result=$(hook_healing_phase_gate "$prev_phase" "$phase" 2>&1); then
|
|
@@ -22834,35 +22848,39 @@ cmd_onboard() {
|
|
|
22834
22848
|
fi
|
|
22835
22849
|
# Extract metadata from package.json
|
|
22836
22850
|
if command -v python3 &>/dev/null; then
|
|
22851
|
+
# WAVE8 loki-est (same class as cmd_explain): pass the repo path via
|
|
22852
|
+
# env (os.environ) instead of interpolating into the python source.
|
|
22853
|
+
# A path with an apostrophe made each heredoc a SyntaxError, silently
|
|
22854
|
+
# dropping the package.json name/version/description under `|| true`.
|
|
22837
22855
|
local pkg_name
|
|
22838
|
-
pkg_name=$(python3 -c "
|
|
22839
|
-
import json,
|
|
22856
|
+
pkg_name=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22857
|
+
import json, os
|
|
22840
22858
|
try:
|
|
22841
|
-
d = json.load(open(
|
|
22859
|
+
d = json.load(open(os.environ['LOKI_ONB_PKG']))
|
|
22842
22860
|
print(d.get('name', ''))
|
|
22843
22861
|
except: pass
|
|
22844
22862
|
" 2>/dev/null || true)
|
|
22845
22863
|
if [ -n "$pkg_name" ]; then
|
|
22846
22864
|
project_name="$pkg_name"
|
|
22847
22865
|
fi
|
|
22848
|
-
project_description=$(python3 -c "
|
|
22849
|
-
import json,
|
|
22866
|
+
project_description=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22867
|
+
import json, os
|
|
22850
22868
|
try:
|
|
22851
|
-
d = json.load(open(
|
|
22869
|
+
d = json.load(open(os.environ['LOKI_ONB_PKG']))
|
|
22852
22870
|
print(d.get('description', ''))
|
|
22853
22871
|
except: pass
|
|
22854
22872
|
" 2>/dev/null || true)
|
|
22855
|
-
project_version=$(python3 -c "
|
|
22856
|
-
import json,
|
|
22873
|
+
project_version=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22874
|
+
import json, os
|
|
22857
22875
|
try:
|
|
22858
|
-
d = json.load(open(
|
|
22876
|
+
d = json.load(open(os.environ['LOKI_ONB_PKG']))
|
|
22859
22877
|
print(d.get('version', ''))
|
|
22860
22878
|
except: pass
|
|
22861
22879
|
" 2>/dev/null || true)
|
|
22862
|
-
entry_points=$(python3 -c "
|
|
22863
|
-
import json,
|
|
22880
|
+
entry_points=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22881
|
+
import json, os
|
|
22864
22882
|
try:
|
|
22865
|
-
d = json.load(open(
|
|
22883
|
+
d = json.load(open(os.environ['LOKI_ONB_PKG']))
|
|
22866
22884
|
main = d.get('main', '')
|
|
22867
22885
|
if main: print(main)
|
|
22868
22886
|
scripts = d.get('scripts', {})
|
|
@@ -23242,10 +23260,11 @@ $imports"
|
|
|
23242
23260
|
if [ -f "$target_path/package.json" ]; then
|
|
23243
23261
|
if command -v python3 &>/dev/null; then
|
|
23244
23262
|
local scripts_json
|
|
23245
|
-
|
|
23246
|
-
|
|
23263
|
+
# WAVE8 loki-est: env-passed path (see cmd_onboard metadata block).
|
|
23264
|
+
scripts_json=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
23265
|
+
import json, os
|
|
23247
23266
|
try:
|
|
23248
|
-
d = json.load(open(
|
|
23267
|
+
d = json.load(open(os.environ['LOKI_ONB_PKG']))
|
|
23249
23268
|
s = d.get('scripts', {})
|
|
23250
23269
|
for k in ['build', 'dev', 'start', 'test', 'lint', 'format', 'check', 'typecheck']:
|
|
23251
23270
|
if k in s:
|
|
@@ -23637,10 +23656,14 @@ cmd_explain() {
|
|
|
23637
23656
|
|
|
23638
23657
|
if command -v python3 &>/dev/null; then
|
|
23639
23658
|
local pkg_meta
|
|
23640
|
-
|
|
23641
|
-
|
|
23659
|
+
# WAVE8 loki-est: pass the path via env (os.environ) instead of
|
|
23660
|
+
# interpolating into the python source -- a repo path containing an
|
|
23661
|
+
# apostrophe broke the string literal and dropped all package.json
|
|
23662
|
+
# metadata.
|
|
23663
|
+
pkg_meta=$(LOKI_EXP_PKG="$target_path/package.json" python3 -c "
|
|
23664
|
+
import json, os
|
|
23642
23665
|
try:
|
|
23643
|
-
d = json.load(open(
|
|
23666
|
+
d = json.load(open(os.environ['LOKI_EXP_PKG']))
|
|
23644
23667
|
print(d.get('name', ''))
|
|
23645
23668
|
print(d.get('description', ''))
|
|
23646
23669
|
print(d.get('version', ''))
|
|
@@ -23869,17 +23892,67 @@ $devdeps_list"
|
|
|
23869
23892
|
|
|
23870
23893
|
# --- JSON output ---
|
|
23871
23894
|
if [ "$output_json" = true ]; then
|
|
23895
|
+
# WAVE8 loki-est: pass every value via the environment and read it with
|
|
23896
|
+
# os.environ instead of interpolating raw bash into the python source.
|
|
23897
|
+
# Interpolation broke on any apostrophe/quote/newline in a project name,
|
|
23898
|
+
# version, description, or path (e.g. a dir named `my'app`), silently
|
|
23899
|
+
# degrading real analysis to `{"error": "JSON generation failed"}`.
|
|
23900
|
+
# env-passing is injection-proof and keeps the same output shape.
|
|
23901
|
+
LOKI_EXP_NAME="$project_name" \
|
|
23902
|
+
LOKI_EXP_DESC="$project_description" \
|
|
23903
|
+
LOKI_EXP_VERSION="$project_version" \
|
|
23904
|
+
LOKI_EXP_PATH="$target_path" \
|
|
23905
|
+
LOKI_EXP_LANGUAGES="$languages" \
|
|
23906
|
+
LOKI_EXP_FRAMEWORKS="$frameworks" \
|
|
23907
|
+
LOKI_EXP_BUILD="$build_system" \
|
|
23908
|
+
LOKI_EXP_PKGMGR="$package_manager" \
|
|
23909
|
+
LOKI_EXP_TESTFW="$test_framework" \
|
|
23910
|
+
LOKI_EXP_CI="$ci_system" \
|
|
23911
|
+
LOKI_EXP_PATTERNS="$detected_patterns" \
|
|
23912
|
+
LOKI_EXP_TOTAL="$total_files" \
|
|
23913
|
+
LOKI_EXP_SRC="$src_count" \
|
|
23914
|
+
LOKI_EXP_TEST="$test_count" \
|
|
23915
|
+
LOKI_EXP_DOC="$doc_count" \
|
|
23916
|
+
LOKI_EXP_CONFIG="$config_count" \
|
|
23917
|
+
LOKI_EXP_BUILDCMD="$build_cmd" \
|
|
23918
|
+
LOKI_EXP_RUNCMD="$run_cmd" \
|
|
23919
|
+
LOKI_EXP_TESTCMD="$test_cmd" \
|
|
23920
|
+
LOKI_EXP_LINTCMD="$lint_cmd" \
|
|
23921
|
+
LOKI_EXP_ENTRY="$major_files" \
|
|
23922
|
+
LOKI_EXP_MONOREPO="$is_monorepo" \
|
|
23923
|
+
LOKI_EXP_DOCKER="$has_docker" \
|
|
23872
23924
|
python3 -c "
|
|
23873
|
-
import json
|
|
23925
|
+
import json, os
|
|
23926
|
+
|
|
23927
|
+
def _s(name):
|
|
23928
|
+
return os.environ.get(name, '')
|
|
23929
|
+
|
|
23930
|
+
def _list(name):
|
|
23931
|
+
v = _s(name).strip()
|
|
23932
|
+
return v.split() if v else []
|
|
23933
|
+
|
|
23934
|
+
def _opt(name):
|
|
23935
|
+
v = _s(name).strip()
|
|
23936
|
+
return v or None
|
|
23937
|
+
|
|
23938
|
+
def _int(name):
|
|
23939
|
+
try:
|
|
23940
|
+
return int(_s(name).strip())
|
|
23941
|
+
except (ValueError, TypeError):
|
|
23942
|
+
return 0
|
|
23943
|
+
|
|
23944
|
+
def _bool(name):
|
|
23945
|
+
return _s(name).strip() == 'true'
|
|
23946
|
+
|
|
23874
23947
|
data = {
|
|
23875
|
-
'project': {'name': '
|
|
23876
|
-
'stack': {'languages':
|
|
23877
|
-
'patterns':
|
|
23878
|
-
'files': {'total':
|
|
23879
|
-
'commands': {'build': '
|
|
23880
|
-
'entry_points':
|
|
23881
|
-
'monorepo':
|
|
23882
|
-
'has_docker':
|
|
23948
|
+
'project': {'name': _s('LOKI_EXP_NAME'), 'description': _s('LOKI_EXP_DESC'), 'version': _s('LOKI_EXP_VERSION'), 'path': _s('LOKI_EXP_PATH')},
|
|
23949
|
+
'stack': {'languages': _list('LOKI_EXP_LANGUAGES'), 'frameworks': _list('LOKI_EXP_FRAMEWORKS'), 'build_system': _opt('LOKI_EXP_BUILD'), 'package_manager': _opt('LOKI_EXP_PKGMGR'), 'test_framework': _list('LOKI_EXP_TESTFW'), 'ci': _opt('LOKI_EXP_CI')},
|
|
23950
|
+
'patterns': _list('LOKI_EXP_PATTERNS'),
|
|
23951
|
+
'files': {'total': _int('LOKI_EXP_TOTAL'), 'source': _int('LOKI_EXP_SRC'), 'test': _int('LOKI_EXP_TEST'), 'docs': _int('LOKI_EXP_DOC'), 'config': _int('LOKI_EXP_CONFIG')},
|
|
23952
|
+
'commands': {'build': _opt('LOKI_EXP_BUILDCMD'), 'run': _opt('LOKI_EXP_RUNCMD'), 'test': _opt('LOKI_EXP_TESTCMD'), 'lint': _opt('LOKI_EXP_LINTCMD')},
|
|
23953
|
+
'entry_points': _list('LOKI_EXP_ENTRY'),
|
|
23954
|
+
'monorepo': _bool('LOKI_EXP_MONOREPO'),
|
|
23955
|
+
'has_docker': _bool('LOKI_EXP_DOCKER')
|
|
23883
23956
|
}
|
|
23884
23957
|
print(json.dumps(data, indent=2))
|
|
23885
23958
|
" 2>/dev/null || echo '{"error": "JSON generation failed"}'
|
package/autonomy/run.sh
CHANGED
|
@@ -1932,61 +1932,32 @@ get_provider_tier_param() {
|
|
|
1932
1932
|
}
|
|
1933
1933
|
|
|
1934
1934
|
#===============================================================================
|
|
1935
|
-
# Provider Spawn Timeout (
|
|
1936
|
-
#
|
|
1937
|
-
#
|
|
1935
|
+
# Provider Spawn Timeout (removed WAVE9 / provider-F2)
|
|
1936
|
+
#
|
|
1937
|
+
# A former invoke_with_timeout() helper (v6.0.0) wrapped a command in
|
|
1938
|
+
# `timeout <s> "$@"` with a retry loop. It was never wired to the main
|
|
1939
|
+
# provider invocation and is intentionally not revived, for two reasons:
|
|
1940
|
+
#
|
|
1941
|
+
# 1. No safe generous default. The main provider call is a long-running
|
|
1942
|
+
# autonomous coding agent. Any fixed timeout short enough to catch a
|
|
1943
|
+
# hang would also kill legitimate multi-minute iterations, and there is
|
|
1944
|
+
# no "generous enough" value that is both safe and useful by default.
|
|
1945
|
+
# 2. Wrong retry semantics. The helper re-ran the same command on timeout.
|
|
1946
|
+
# Re-running a coding agent mid-work (it may have already edited files)
|
|
1947
|
+
# is actively harmful, not protective.
|
|
1948
|
+
#
|
|
1949
|
+
# The main invocation is also a pipeline (`claude | tee | python3`), which a
|
|
1950
|
+
# positional-arg `timeout "$@"` wrapper cannot wrap at all. Interrupting a
|
|
1951
|
+
# hung provider is handled by the SIGINT trap (kill_provider_child) instead.
|
|
1952
|
+
#
|
|
1953
|
+
# The `loki config spawn_timeout` / `spawn_retries` knobs (autonomy/loki) and
|
|
1954
|
+
# the config->env mapping in this file (`'spawn_timeout':'LOKI_SPAWN_TIMEOUT'`
|
|
1955
|
+
# in the config loader) still export LOKI_SPAWN_TIMEOUT / LOKI_SPAWN_RETRIES,
|
|
1956
|
+
# but nothing consumes them now. The mapping line is intentionally left in place
|
|
1957
|
+
# (a config-schema test may enumerate it); the full inert-knob removal spans
|
|
1958
|
+
# autonomy/loki too and is a separate cross-file follow-up.
|
|
1938
1959
|
#===============================================================================
|
|
1939
1960
|
|
|
1940
|
-
PROVIDER_SPAWN_TIMEOUT=${LOKI_SPAWN_TIMEOUT:-120}
|
|
1941
|
-
PROVIDER_SPAWN_RETRIES=${LOKI_SPAWN_RETRIES:-2}
|
|
1942
|
-
|
|
1943
|
-
# Invoke a command with timeout and retry logic
|
|
1944
|
-
# Usage: invoke_with_timeout <timeout_seconds> <retries> <command...>
|
|
1945
|
-
invoke_with_timeout() {
|
|
1946
|
-
local timeout="$1"
|
|
1947
|
-
local max_retries="$2"
|
|
1948
|
-
shift 2
|
|
1949
|
-
|
|
1950
|
-
local attempt=0
|
|
1951
|
-
while [ $attempt -le $max_retries ]; do
|
|
1952
|
-
if [ $attempt -gt 0 ]; then
|
|
1953
|
-
log_warn "Provider spawn retry $attempt/$max_retries..."
|
|
1954
|
-
fi
|
|
1955
|
-
|
|
1956
|
-
local exit_code=0
|
|
1957
|
-
# Use timeout command if available (GNU coreutils or macOS)
|
|
1958
|
-
if command -v timeout &>/dev/null; then
|
|
1959
|
-
timeout "$timeout" "$@"
|
|
1960
|
-
exit_code=$?
|
|
1961
|
-
elif command -v gtimeout &>/dev/null; then
|
|
1962
|
-
gtimeout "$timeout" "$@"
|
|
1963
|
-
exit_code=$?
|
|
1964
|
-
else
|
|
1965
|
-
# Fallback: no timeout wrapper, run directly
|
|
1966
|
-
log_warn "timeout/gtimeout not available - running without timeout enforcement"
|
|
1967
|
-
"$@"
|
|
1968
|
-
exit_code=$?
|
|
1969
|
-
fi
|
|
1970
|
-
|
|
1971
|
-
# Exit code 124 = timeout
|
|
1972
|
-
if [ $exit_code -eq 124 ]; then
|
|
1973
|
-
log_warn "Provider spawn timed out after ${timeout}s (attempt $((attempt+1))/$((max_retries+1)))"
|
|
1974
|
-
((attempt++))
|
|
1975
|
-
continue
|
|
1976
|
-
fi
|
|
1977
|
-
|
|
1978
|
-
return $exit_code
|
|
1979
|
-
done
|
|
1980
|
-
|
|
1981
|
-
log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
|
|
1982
|
-
# Crash friction (retry_loop): provider spawn exhausted all retries -- a
|
|
1983
|
-
# clear threshold (not a single retry). Best-effort, never blocks.
|
|
1984
|
-
if type loki_crash_friction &>/dev/null; then
|
|
1985
|
-
loki_crash_friction "retry_loop" "provider spawn failed after $((max_retries+1)) attempts" >/dev/null 2>&1 || true
|
|
1986
|
-
fi
|
|
1987
|
-
return 124
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
1961
|
#===============================================================================
|
|
1991
1962
|
# GitHub Integration Functions (v4.1.0)
|
|
1992
1963
|
#===============================================================================
|
|
@@ -9978,6 +9949,16 @@ CPEOF
|
|
|
9978
9949
|
find "$checkpoint_dir" -maxdepth 1 -type d -name "cp-*" 2>/dev/null \
|
|
9979
9950
|
| while read -r p; do basename "$p"; done | sort -t'-' -k3 -n \
|
|
9980
9951
|
| head -n "$to_remove" | while read -r old_cp; do
|
|
9952
|
+
# WAVE9 (checkpoint leak): also delete the anchored worktree-snapshot
|
|
9953
|
+
# ref so its stash commit becomes eligible for `git gc`. Without this,
|
|
9954
|
+
# refs/loki/cp/<id> (and its commit) leaked forever even after the
|
|
9955
|
+
# checkpoint directory was pruned. Targeted deletion of exactly the
|
|
9956
|
+
# ids being pruned ONLY -- never a blanket refs/loki/cp/* sweep, since
|
|
9957
|
+
# git refs are shared across worktrees of one repo while checkpoint
|
|
9958
|
+
# dirs are per-TARGET_DIR; a parallel worktree may still need a ref we
|
|
9959
|
+
# are not pruning here. `|| true` because not every checkpoint has a
|
|
9960
|
+
# ref (only those where `git stash create` returned a non-empty sha).
|
|
9961
|
+
git update-ref -d "refs/loki/cp/${old_cp}" 2>/dev/null || true
|
|
9981
9962
|
old_cp="${checkpoint_dir}/${old_cp}"
|
|
9982
9963
|
rm -rf "$old_cp" 2>/dev/null || true
|
|
9983
9964
|
done
|
|
@@ -11492,7 +11473,11 @@ try:
|
|
|
11492
11473
|
storage = MemoryStorage(f'{target_dir}/.loki/memory')
|
|
11493
11474
|
retriever = MemoryRetrieval(storage)
|
|
11494
11475
|
context = {'goal': goal, 'phase': phase}
|
|
11495
|
-
|
|
11476
|
+
# The autonomous RARV loop opts into persist_boost so retrieved memories are
|
|
11477
|
+
# reinforced on disk ("use it or lose it"). Manual surfaces (loki memory CLI,
|
|
11478
|
+
# dashboard, MCP) keep the default persist_boost=False so a human browsing
|
|
11479
|
+
# memories does not silently inflate their importance.
|
|
11480
|
+
results = retriever.retrieve_task_aware(context, top_k=3, persist_boost=True)
|
|
11496
11481
|
if results:
|
|
11497
11482
|
print('RELEVANT MEMORIES:')
|
|
11498
11483
|
for r in results[:3]:
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -8675,6 +8675,17 @@ def _get_migration_imports():
|
|
|
8675
8675
|
return _migration_imports
|
|
8676
8676
|
|
|
8677
8677
|
|
|
8678
|
+
def _get_migration_terminal_phase():
|
|
8679
|
+
"""Return the last phase in the migration PHASE_ORDER (the terminal phase),
|
|
8680
|
+
or None if the migration engine is unavailable. Used to let the terminal
|
|
8681
|
+
phase be advanced/completed without a successor to_phase (WAVE9 F1)."""
|
|
8682
|
+
try:
|
|
8683
|
+
from dashboard.migration_engine import PHASE_ORDER
|
|
8684
|
+
return PHASE_ORDER[-1] if PHASE_ORDER else None
|
|
8685
|
+
except (ImportError, IndexError):
|
|
8686
|
+
return None
|
|
8687
|
+
|
|
8688
|
+
|
|
8678
8689
|
@app.get("/api/migration/list", dependencies=[Depends(auth.require_scope("read"))])
|
|
8679
8690
|
def list_migrations_endpoint():
|
|
8680
8691
|
"""List all migrations."""
|
|
@@ -8825,7 +8836,16 @@ def advance_migration(migration_id: str, request_body: dict):
|
|
|
8825
8836
|
MigrationPipeline, list_migrations = imports
|
|
8826
8837
|
from_phase = request_body.get("from_phase")
|
|
8827
8838
|
to_phase = request_body.get("to_phase")
|
|
8828
|
-
|
|
8839
|
+
# The terminal phase (the last in PHASE_ORDER, e.g. "verify") has no
|
|
8840
|
+
# successor, so check_phase_gate can never pass for it and to_phase is
|
|
8841
|
+
# meaningless. Without this carve-out the terminal phase could never be
|
|
8842
|
+
# completed via the API, so overall_status could never reach "completed"
|
|
8843
|
+
# (WAVE9 migration-F1). For the terminal phase we require only from_phase
|
|
8844
|
+
# and skip the gate; advance_phase still validates the phase and the
|
|
8845
|
+
# (ValueError, RuntimeError) -> 409 handler below preserves idempotency.
|
|
8846
|
+
terminal_phase = _get_migration_terminal_phase()
|
|
8847
|
+
is_terminal = terminal_phase is not None and from_phase == terminal_phase
|
|
8848
|
+
if not from_phase or (not to_phase and not is_terminal):
|
|
8829
8849
|
raise HTTPException(status_code=400, detail="from_phase and to_phase are required")
|
|
8830
8850
|
# Load pipeline and check phase gate before the try/except to let
|
|
8831
8851
|
# HTTPException and FileNotFoundError propagate naturally.
|
|
@@ -8833,9 +8853,10 @@ def advance_migration(migration_id: str, request_body: dict):
|
|
|
8833
8853
|
pipeline = MigrationPipeline.load(migration_id)
|
|
8834
8854
|
except FileNotFoundError:
|
|
8835
8855
|
raise HTTPException(status_code=404, detail=f"Migration not found: {migration_id}")
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8856
|
+
if not is_terminal:
|
|
8857
|
+
passed, reason = pipeline.check_phase_gate(from_phase, to_phase)
|
|
8858
|
+
if not passed:
|
|
8859
|
+
raise HTTPException(status_code=409, detail=reason)
|
|
8839
8860
|
try:
|
|
8840
8861
|
result = pipeline.advance_phase(from_phase)
|
|
8841
8862
|
return asdict(result) if hasattr(result, '__dataclass_fields__') else result
|
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.68.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.68.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var t6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)t6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};b(D1,{lokiDir:()=>j,homeLokiDir:()=>s$,findRepoRootForVersion:()=>a$,REPO_ROOT:()=>g});import{resolve as a,dirname as n$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=S1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=n$($);if(Z===$)break;$=Z}return a(S1,"..","..","..")}function a$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=n$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function j(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function s$(){return a(QQ(),".loki")}var S1,g;var C=P(()=>{S1=n$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var t6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)t6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};b(D1,{lokiDir:()=>j,homeLokiDir:()=>s$,findRepoRootForVersion:()=>a$,REPO_ROOT:()=>g});import{resolve as a,dirname as n$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=S1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=n$($);if(Z===$)break;$=Z}return a(S1,"..","..","..")}function a$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=n$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function j(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function s$(){return a(QQ(),".loki")}var S1,g;var C=P(()=>{S1=n$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.68.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=KQ(qQ(import.meta.url)),Z=a$(Q);Q$=zQ(XQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var r$=P(()=>{C()});var b1={};b(b1,{runOrThrow:()=>VQ,run:()=>R,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>t$});async function R($,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[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function VQ($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new t$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=JQ($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function JQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var t$;var d=P(()=>{t$=class t$ 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 s($){return UQ?"":$}var UQ,T,S,_,_Z,I,k,h,V;var c=P(()=>{UQ=(process.env.NO_COLOR??"").length>0;T=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as _Q}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(_Q($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var Y$;var V$=P(()=>{d()});var e1={};b(e1,{runStatus:()=>cQ});import{existsSync as y,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as D,basename as CQ}from"path";import{homedir as bQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*x$/Q);if(X>x$)X=x$;let q=x$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${k}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function yQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
|
|
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)
|
|
@@ -793,4 +793,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
793
793
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
794
794
|
`),process.stderr.write(r6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
|
|
795
795
|
|
|
796
|
-
//# debugId=
|
|
796
|
+
//# debugId=33EB4AF09F8F2C5564756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/consolidation.py
CHANGED
|
@@ -839,6 +839,20 @@ class ConsolidationPipeline:
|
|
|
839
839
|
if best_match is None or best_similarity < 0.5:
|
|
840
840
|
return new_pattern
|
|
841
841
|
|
|
842
|
+
# Idempotency guard (consolidation-C4): only boost confidence when the
|
|
843
|
+
# merge actually introduces NEW evidence. consolidate() reloads every
|
|
844
|
+
# episode in the since-window on each run (storage.list_episodes has no
|
|
845
|
+
# consolidated-state filter), so re-running over an unchanged episode set
|
|
846
|
+
# re-extracts identical patterns that re-match this existing pattern. A
|
|
847
|
+
# flat +0.05 every time would ratchet confidence up artificially with no
|
|
848
|
+
# new data. Comparing source_episodes (which round-trips through storage)
|
|
849
|
+
# makes the merge a no-op for confidence when no new source episode is
|
|
850
|
+
# present, while still rewarding a genuinely new similar episode.
|
|
851
|
+
new_source_episodes = (
|
|
852
|
+
set(new_pattern.source_episodes) - set(best_match.source_episodes)
|
|
853
|
+
)
|
|
854
|
+
confidence_boost = 0.05 if new_source_episodes else 0.0
|
|
855
|
+
|
|
842
856
|
# Merge patterns
|
|
843
857
|
merged = SemanticPattern(
|
|
844
858
|
id=best_match.id,
|
|
@@ -847,7 +861,7 @@ class ConsolidationPipeline:
|
|
|
847
861
|
conditions=list(set(best_match.conditions + new_pattern.conditions)),
|
|
848
862
|
correct_approach=best_match.correct_approach or new_pattern.correct_approach,
|
|
849
863
|
incorrect_approach=best_match.incorrect_approach or new_pattern.incorrect_approach,
|
|
850
|
-
confidence=min(best_match.confidence +
|
|
864
|
+
confidence=min(best_match.confidence + confidence_boost, 0.99),
|
|
851
865
|
source_episodes=list(set(best_match.source_episodes + new_pattern.source_episodes)),
|
|
852
866
|
usage_count=best_match.usage_count,
|
|
853
867
|
last_used=best_match.last_used,
|
package/memory/retrieval.py
CHANGED
|
@@ -414,6 +414,7 @@ class MemoryRetrieval:
|
|
|
414
414
|
context: Dict[str, Any],
|
|
415
415
|
top_k: int = 5,
|
|
416
416
|
token_budget: Optional[int] = None,
|
|
417
|
+
persist_boost: bool = False,
|
|
417
418
|
) -> List[Dict[str, Any]]:
|
|
418
419
|
"""
|
|
419
420
|
Retrieve memories with task-type-aware weighting.
|
|
@@ -427,6 +428,12 @@ class MemoryRetrieval:
|
|
|
427
428
|
token_budget: Optional maximum token budget for returned memories.
|
|
428
429
|
If specified, results will be optimized to fit within
|
|
429
430
|
this budget using importance/recency/relevance scoring.
|
|
431
|
+
persist_boost: When True, persist the retrieval-time importance boost
|
|
432
|
+
to disk ("use it or lose it" reinforcement). Default
|
|
433
|
+
False so manual/on-demand retrievals (dashboard, MCP)
|
|
434
|
+
do NOT silently reinforce importance; only the autonomous
|
|
435
|
+
RARV loop opts in. The in-memory boost that shapes the
|
|
436
|
+
returned ranking is applied either way.
|
|
430
437
|
|
|
431
438
|
Returns:
|
|
432
439
|
List of memory items with source field indicating origin
|
|
@@ -476,10 +483,20 @@ class MemoryRetrieval:
|
|
|
476
483
|
# Apply recency boost
|
|
477
484
|
merged = self._apply_recency_boost(merged, boost_factor=0.1)
|
|
478
485
|
|
|
479
|
-
# Boost importance for retrieved memories (use it or lose it)
|
|
486
|
+
# Boost importance for retrieved memories (use it or lose it). The
|
|
487
|
+
# in-memory boost shapes the returned ranking; persist_boost writes the
|
|
488
|
+
# reinforcement to disk (retrieval-F1: boost_on_retrieval alone never
|
|
489
|
+
# persisted). Persistence is best-effort: a locked/missing record must
|
|
490
|
+
# never break retrieval, so failures are swallowed (mirrors other
|
|
491
|
+
# best-effort writes).
|
|
480
492
|
if hasattr(self.storage, 'boost_on_retrieval'):
|
|
481
493
|
for memory in merged[:top_k]:
|
|
482
494
|
self.storage.boost_on_retrieval(memory, boost=0.05)
|
|
495
|
+
if persist_boost and hasattr(self.storage, 'persist_boost'):
|
|
496
|
+
try:
|
|
497
|
+
self.storage.persist_boost(memory, boost=0.05)
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
483
500
|
|
|
484
501
|
# Apply token budget optimization if specified
|
|
485
502
|
if token_budget is not None and token_budget > 0:
|
package/memory/storage.py
CHANGED
|
@@ -1383,6 +1383,142 @@ class MemoryStorage:
|
|
|
1383
1383
|
|
|
1384
1384
|
return memory
|
|
1385
1385
|
|
|
1386
|
+
def persist_boost(
|
|
1387
|
+
self,
|
|
1388
|
+
memory: Dict[str, Any],
|
|
1389
|
+
boost: float = 0.1,
|
|
1390
|
+
) -> bool:
|
|
1391
|
+
"""
|
|
1392
|
+
Persist a retrieval-time boost to disk ("use it or lose it").
|
|
1393
|
+
|
|
1394
|
+
boost_on_retrieval mutates an in-memory dict only; without this the
|
|
1395
|
+
stored importance/access_count never rises, so repeated retrieval can
|
|
1396
|
+
never reinforce a memory against decay (retrieval-F1). This method
|
|
1397
|
+
applies the SAME boost math to the record as it currently exists on
|
|
1398
|
+
disk, under one exclusive _file_lock spanning a FRESH read -> mutate
|
|
1399
|
+
-> _atomic_write (mirrors _decay_episodic / _decay_semantic).
|
|
1400
|
+
|
|
1401
|
+
Race-safety: the boost is applied to the freshly-read record, NOT to
|
|
1402
|
+
the passed-in `memory` dict. So a concurrent content edit landed by
|
|
1403
|
+
another writer is preserved (we only overwrite importance,
|
|
1404
|
+
access_count, last_accessed), and no retrieval-only transient fields
|
|
1405
|
+
(_score, _source, _collection) leak into the stored record. This is
|
|
1406
|
+
the lost-update-safe pattern WAVE6 established for decay.
|
|
1407
|
+
|
|
1408
|
+
Keyed by memory["id"] and the collection marker retrieval attaches
|
|
1409
|
+
(_source, falling back to _collection). Covers episodic (per-file) and
|
|
1410
|
+
semantic patterns.json. Collections without an updater degrade
|
|
1411
|
+
gracefully (return False, no crash):
|
|
1412
|
+
- skills are keyed on disk by name, not id, so an id-keyed boost
|
|
1413
|
+
cannot reliably target the file; skipped honestly.
|
|
1414
|
+
- the legacy semantic/anti-patterns.json store has NO updater
|
|
1415
|
+
anywhere in this module, so there is nothing to write back to;
|
|
1416
|
+
skipped honestly rather than fabricating a writer.
|
|
1417
|
+
|
|
1418
|
+
Args:
|
|
1419
|
+
memory: A retrieved memory dict (must carry "id" and a source
|
|
1420
|
+
marker). The dict itself is not written to disk.
|
|
1421
|
+
boost: Amount to boost importance (default 0.1).
|
|
1422
|
+
|
|
1423
|
+
Returns:
|
|
1424
|
+
True if a record was found and persisted, False otherwise.
|
|
1425
|
+
"""
|
|
1426
|
+
memory_id = memory.get("id")
|
|
1427
|
+
if not memory_id:
|
|
1428
|
+
return False
|
|
1429
|
+
|
|
1430
|
+
source = memory.get("_source") or memory.get("_collection") or ""
|
|
1431
|
+
|
|
1432
|
+
if source == "episodic":
|
|
1433
|
+
return self._persist_boost_episodic(str(memory_id), boost)
|
|
1434
|
+
if source == "semantic":
|
|
1435
|
+
return self._persist_boost_semantic(str(memory_id), boost)
|
|
1436
|
+
|
|
1437
|
+
# skills (keyed by name on disk) and the legacy anti-patterns.json
|
|
1438
|
+
# store (no updater exists in this module) cannot be safely targeted
|
|
1439
|
+
# by an id-keyed boost; skip rather than fabricate a writer.
|
|
1440
|
+
return False
|
|
1441
|
+
|
|
1442
|
+
def _persist_boost_episodic(self, memory_id: str, boost: float) -> bool:
|
|
1443
|
+
"""Apply and persist a boost to one episodic record, keyed by id.
|
|
1444
|
+
|
|
1445
|
+
Locates the per-file record (task-<id>.json across date dirs) then does
|
|
1446
|
+
a lock-spanning fresh-read -> boost -> atomic-write, mirroring
|
|
1447
|
+
_decay_episodic. The id is sanitized exactly as save_episode does so a
|
|
1448
|
+
sanitized-on-write filename is still found.
|
|
1449
|
+
"""
|
|
1450
|
+
episodic_dir = self.base_path / "episodic"
|
|
1451
|
+
if not episodic_dir.exists():
|
|
1452
|
+
return False
|
|
1453
|
+
|
|
1454
|
+
safe_id = "".join(
|
|
1455
|
+
c if c.isalnum() or c in "-_" else "_"
|
|
1456
|
+
for c in memory_id
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
for date_dir in episodic_dir.iterdir():
|
|
1460
|
+
if not date_dir.is_dir():
|
|
1461
|
+
continue
|
|
1462
|
+
file_path = date_dir / f"task-{safe_id}.json"
|
|
1463
|
+
if not file_path.exists():
|
|
1464
|
+
continue
|
|
1465
|
+
|
|
1466
|
+
# One exclusive lock spanning read-mutate-write. boost_on_retrieval
|
|
1467
|
+
# mutates the freshly-read record in place (importance/access_count/
|
|
1468
|
+
# last_accessed only), so a concurrent content edit on disk is
|
|
1469
|
+
# preserved. _atomic_write re-enters the same reentrant lock.
|
|
1470
|
+
with self._file_lock(file_path, exclusive=True):
|
|
1471
|
+
if not file_path.exists():
|
|
1472
|
+
return False
|
|
1473
|
+
try:
|
|
1474
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
1475
|
+
data = json.load(f)
|
|
1476
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
1477
|
+
return False
|
|
1478
|
+
if not data:
|
|
1479
|
+
return False
|
|
1480
|
+
self.boost_on_retrieval(data, boost=boost)
|
|
1481
|
+
self._atomic_write(file_path, data)
|
|
1482
|
+
return True
|
|
1483
|
+
|
|
1484
|
+
return False
|
|
1485
|
+
|
|
1486
|
+
def _persist_boost_semantic(self, memory_id: str, boost: float) -> bool:
|
|
1487
|
+
"""Apply and persist a boost to one semantic pattern, keyed by id.
|
|
1488
|
+
|
|
1489
|
+
Patterns live in a single semantic/patterns.json list. Lock-spanning
|
|
1490
|
+
fresh read -> boost the matching entry -> atomic write, mirroring
|
|
1491
|
+
_decay_semantic / save_pattern.
|
|
1492
|
+
"""
|
|
1493
|
+
patterns_path = self.base_path / "semantic" / "patterns.json"
|
|
1494
|
+
if not patterns_path.exists():
|
|
1495
|
+
return False
|
|
1496
|
+
|
|
1497
|
+
with self._file_lock(patterns_path, exclusive=True):
|
|
1498
|
+
if not patterns_path.exists():
|
|
1499
|
+
return False
|
|
1500
|
+
try:
|
|
1501
|
+
with open(patterns_path, "r", encoding="utf-8") as f:
|
|
1502
|
+
patterns_file = json.load(f)
|
|
1503
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
1504
|
+
return False
|
|
1505
|
+
if not patterns_file:
|
|
1506
|
+
return False
|
|
1507
|
+
|
|
1508
|
+
patterns = patterns_file.get("patterns", [])
|
|
1509
|
+
for pattern in patterns:
|
|
1510
|
+
if not isinstance(pattern, dict):
|
|
1511
|
+
continue
|
|
1512
|
+
if pattern.get("id") == memory_id:
|
|
1513
|
+
self.boost_on_retrieval(pattern, boost=boost)
|
|
1514
|
+
patterns_file["last_updated"] = datetime.now(
|
|
1515
|
+
timezone.utc
|
|
1516
|
+
).isoformat()
|
|
1517
|
+
self._atomic_write(patterns_path, patterns_file)
|
|
1518
|
+
return True
|
|
1519
|
+
|
|
1520
|
+
return False
|
|
1521
|
+
|
|
1386
1522
|
def batch_apply_decay(
|
|
1387
1523
|
self,
|
|
1388
1524
|
collection: str = "all",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.68.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.68.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|