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 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.67.0
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.67.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
409
+ **v7.68.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.67.0
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
- if [ $? -ne 0 ]; then
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': '$codebase_path',
12506
+ 'codebase': os.environ.get('LOKI_HEAL_CODEBASE', ''),
12493
12507
  'started': datetime.now().isoformat(),
12494
- 'current_phase': '$phase',
12495
- 'strict_mode': $( [ "$strict" = "true" ] && echo "True" || echo "False" ),
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('$heal_dir/healing-progress.json', 'w') as f:
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('$heal_dir/healing-progress.json')).get('current_phase', 'archaeology'))" 2>/dev/null || echo "archaeology")
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, sys
22856
+ pkg_name=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22857
+ import json, os
22840
22858
  try:
22841
- d = json.load(open('$target_path/package.json'))
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, sys
22866
+ project_description=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22867
+ import json, os
22850
22868
  try:
22851
- d = json.load(open('$target_path/package.json'))
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, sys
22873
+ project_version=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22874
+ import json, os
22857
22875
  try:
22858
- d = json.load(open('$target_path/package.json'))
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, sys
22880
+ entry_points=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22881
+ import json, os
22864
22882
  try:
22865
- d = json.load(open('$target_path/package.json'))
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
- scripts_json=$(python3 -c "
23246
- import json
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('$target_path/package.json'))
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
- pkg_meta=$(python3 -c "
23641
- import json
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('$target_path/package.json'))
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': '$project_name', 'description': '''$(echo "$project_description" | sed "s/'/\\\\'/g")''', 'version': '$project_version', 'path': '$target_path'},
23876
- 'stack': {'languages': '${languages}'.split() if '${languages}'.strip() else [], 'frameworks': '${frameworks}'.split() if '${frameworks}'.strip() else [], 'build_system': '$build_system' or None, 'package_manager': '$package_manager' or None, 'test_framework': '${test_framework}'.split() if '${test_framework}'.strip() else [], 'ci': '${ci_system}'.strip() or None},
23877
- 'patterns': '${detected_patterns}'.split() if '${detected_patterns}'.strip() else [],
23878
- 'files': {'total': $total_files, 'source': $src_count, 'test': $test_count, 'docs': $doc_count, 'config': $config_count},
23879
- 'commands': {'build': '${build_cmd}' or None, 'run': '${run_cmd}' or None, 'test': '${test_cmd}' or None, 'lint': '${lint_cmd}' or None},
23880
- 'entry_points': '${major_files}'.split() if '${major_files}'.strip() else [],
23881
- 'monorepo': $( [ "$is_monorepo" = true ] && echo "True" || echo "False" ),
23882
- 'has_docker': $( [ "$has_docker" = true ] && echo "True" || echo "False" )
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 (v6.0.0)
1936
- # Wraps provider invocation with timeout + retries.
1937
- # Default: 120s timeout, 2 retries.
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
- results = retriever.retrieve_task_aware(context, top_k=3)
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]:
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.67.0"
10
+ __version__ = "7.68.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
- if not from_phase or not to_phase:
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
- passed, reason = pipeline.check_phase_gate(from_phase, to_phase)
8837
- if not passed:
8838
- raise HTTPException(status_code=409, detail=reason)
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
@@ -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.67.0
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.67.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.68.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -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.67.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}
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=E02AAB86B1B0FD5664756E2164756E21
796
+ //# debugId=33EB4AF09F8F2C5564756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.67.0'
60
+ __version__ = '7.68.0'
@@ -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 + 0.05, 0.99),
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,
@@ -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.67.0",
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.67.0",
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",