loki-mode 5.59.0 → 6.0.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/autonomy/run.sh CHANGED
@@ -177,9 +177,9 @@ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
177
177
  # Solution: Copy ourselves to /tmp and run from there. The original can be safely edited.
178
178
  #===============================================================================
179
179
  if [[ -z "${LOKI_RUNNING_FROM_TEMP:-}" ]] && [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
180
- TEMP_SCRIPT="/tmp/loki-run-$$.sh"
180
+ TEMP_SCRIPT=$(mktemp /tmp/loki-run-XXXXXX.sh)
181
181
  cp "${BASH_SOURCE[0]}" "$TEMP_SCRIPT"
182
- chmod +x "$TEMP_SCRIPT"
182
+ chmod 700 "$TEMP_SCRIPT"
183
183
  export LOKI_RUNNING_FROM_TEMP=1
184
184
  export LOKI_ORIGINAL_SCRIPT_DIR="$SCRIPT_DIR"
185
185
  export LOKI_ORIGINAL_PROJECT_DIR="$PROJECT_DIR"
@@ -471,6 +471,47 @@ parse_yaml_with_yq() {
471
471
  # Load config file before setting defaults
472
472
  load_config_file
473
473
 
474
+ # Load JSON settings from loki config set (v6.0.0)
475
+ _load_json_settings() {
476
+ local settings_file="${TARGET_DIR:-.}/.loki/config/settings.json"
477
+ [ -f "$settings_file" ] || return 0
478
+ eval "$(_LOKI_SETTINGS_FILE="$settings_file" python3 -c "
479
+ import json, sys, os, shlex
480
+
481
+ def get_nested(d, key):
482
+ \"\"\"Resolve dotted keys through nested dicts (model.planning -> data['model']['planning'])\"\"\"
483
+ parts = key.split('.')
484
+ cur = d
485
+ for p in parts:
486
+ if isinstance(cur, dict):
487
+ cur = cur.get(p)
488
+ else:
489
+ return None
490
+ return cur
491
+
492
+ try:
493
+ with open(os.environ['_LOKI_SETTINGS_FILE']) as f:
494
+ data = json.load(f)
495
+ except Exception:
496
+ sys.exit(0)
497
+ mapping = {
498
+ 'maxTier': 'LOKI_MAX_TIER',
499
+ 'model.planning': 'LOKI_MODEL_PLANNING',
500
+ 'model.development': 'LOKI_MODEL_DEVELOPMENT',
501
+ 'model.fast': 'LOKI_MODEL_FAST',
502
+ 'notify.slack': 'LOKI_SLACK_WEBHOOK',
503
+ 'notify.discord': 'LOKI_DISCORD_WEBHOOK',
504
+ }
505
+ for key, env_var in mapping.items():
506
+ # Try nested dict lookup first, then flat key, then underscore variant
507
+ val = get_nested(data, key) or data.get(key) or data.get(key.replace('.', '_'))
508
+ if val and isinstance(val, str):
509
+ safe_val = shlex.quote(val)
510
+ print(f'[ -z \"\${{{env_var}:-}}\" ] && export {env_var}={safe_val}')
511
+ " 2>/dev/null)" 2>/dev/null || true
512
+ }
513
+ _LOKI_SETTINGS_FILE="${TARGET_DIR:-.}/.loki/config/settings.json" _load_json_settings
514
+
474
515
  # Configuration
475
516
  MAX_RETRIES=${LOKI_MAX_RETRIES:-50}
476
517
  BASE_WAIT=${LOKI_BASE_WAIT:-60}
@@ -1305,10 +1346,25 @@ get_rarv_phase_name() {
1305
1346
  }
1306
1347
 
1307
1348
  # Get provider-specific tier parameter based on current tier
1308
- # Uses provider config variables for the tier mapping
1349
+ # v6.0.0: Delegates to resolve_model_for_tier() if available (dynamic resolution).
1350
+ # Falls back to static mapping for backward compatibility.
1309
1351
  get_provider_tier_param() {
1310
1352
  local tier="${1:-$CURRENT_TIER}"
1311
1353
 
1354
+ # v6.0.0: Use dynamic resolution if provider has resolve_model_for_tier
1355
+ if type resolve_model_for_tier &>/dev/null; then
1356
+ local resolved
1357
+ resolved=$(resolve_model_for_tier "$tier")
1358
+ # For Claude, extract short name (opus/sonnet/haiku) from full model ID
1359
+ if [ "${PROVIDER_NAME:-claude}" = "claude" ]; then
1360
+ echo "$resolved" | sed 's/claude-\([a-z]*\).*/\1/'
1361
+ else
1362
+ echo "$resolved"
1363
+ fi
1364
+ return
1365
+ fi
1366
+
1367
+ # Legacy fallback: static tier mapping
1312
1368
  case "${PROVIDER_NAME:-claude}" in
1313
1369
  claude)
1314
1370
  case "$tier" in
@@ -1340,6 +1396,57 @@ get_provider_tier_param() {
1340
1396
  esac
1341
1397
  }
1342
1398
 
1399
+ #===============================================================================
1400
+ # Provider Spawn Timeout (v6.0.0)
1401
+ # Wraps provider invocation with timeout + retries.
1402
+ # Default: 120s timeout, 2 retries.
1403
+ #===============================================================================
1404
+
1405
+ PROVIDER_SPAWN_TIMEOUT=${LOKI_SPAWN_TIMEOUT:-120}
1406
+ PROVIDER_SPAWN_RETRIES=${LOKI_SPAWN_RETRIES:-2}
1407
+
1408
+ # Invoke a command with timeout and retry logic
1409
+ # Usage: invoke_with_timeout <timeout_seconds> <retries> <command...>
1410
+ invoke_with_timeout() {
1411
+ local timeout="$1"
1412
+ local max_retries="$2"
1413
+ shift 2
1414
+
1415
+ local attempt=0
1416
+ while [ $attempt -le $max_retries ]; do
1417
+ if [ $attempt -gt 0 ]; then
1418
+ log_warn "Provider spawn retry $attempt/$max_retries..."
1419
+ fi
1420
+
1421
+ local exit_code=0
1422
+ # Use timeout command if available (GNU coreutils or macOS)
1423
+ if command -v timeout &>/dev/null; then
1424
+ timeout "$timeout" "$@"
1425
+ exit_code=$?
1426
+ elif command -v gtimeout &>/dev/null; then
1427
+ gtimeout "$timeout" "$@"
1428
+ exit_code=$?
1429
+ else
1430
+ # Fallback: no timeout wrapper, run directly
1431
+ log_warn "timeout/gtimeout not available - running without timeout enforcement"
1432
+ "$@"
1433
+ exit_code=$?
1434
+ fi
1435
+
1436
+ # Exit code 124 = timeout
1437
+ if [ $exit_code -eq 124 ]; then
1438
+ log_warn "Provider spawn timed out after ${timeout}s (attempt $((attempt+1))/$((max_retries+1)))"
1439
+ ((attempt++))
1440
+ continue
1441
+ fi
1442
+
1443
+ return $exit_code
1444
+ done
1445
+
1446
+ log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
1447
+ return 124
1448
+ }
1449
+
1343
1450
  #===============================================================================
1344
1451
  # GitHub Integration Functions (v4.1.0)
1345
1452
  #===============================================================================
@@ -5125,6 +5232,141 @@ AGG_SCRIPT
5125
5232
  return 0
5126
5233
  }
5127
5234
 
5235
+ #===============================================================================
5236
+ # Adversarial Testing (v6.0.0) - For Standard+ complexity tiers
5237
+ # Spawns an adversarial agent that tries to break the implementation.
5238
+ # Only runs when complexity >= standard (6+ agents).
5239
+ #===============================================================================
5240
+
5241
+ run_adversarial_testing() {
5242
+ local loki_dir="${TARGET_DIR:-.}/.loki"
5243
+ local adversarial_dir="$loki_dir/quality/adversarial"
5244
+ local test_id
5245
+ test_id="adversarial-$(date -u +%Y%m%dT%H%M%SZ)-${ITERATION_COUNT:-0}"
5246
+ mkdir -p "$adversarial_dir/$test_id"
5247
+
5248
+ # Only run for Standard+ complexity
5249
+ local complexity="${LOKI_COMPLEXITY:-auto}"
5250
+ if [ "$complexity" = "simple" ]; then
5251
+ log_debug "Adversarial testing skipped: simple complexity tier"
5252
+ return 0
5253
+ fi
5254
+
5255
+ # Check if adversarial testing is disabled
5256
+ if [ "${LOKI_ADVERSARIAL_TESTING:-true}" = "false" ]; then
5257
+ log_debug "Adversarial testing disabled via LOKI_ADVERSARIAL_TESTING=false"
5258
+ return 0
5259
+ fi
5260
+
5261
+ log_header "ADVERSARIAL TESTING: $test_id"
5262
+
5263
+ # Get diff for adversarial analysis
5264
+ local diff_content
5265
+ diff_content=$(git -C "${TARGET_DIR:-.}" diff HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --cached 2>/dev/null || echo "")
5266
+ if [ -z "$diff_content" ]; then
5267
+ log_info "Adversarial testing: No diff to test, skipping"
5268
+ return 0
5269
+ fi
5270
+
5271
+ local changed_files
5272
+ changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached 2>/dev/null || echo "")
5273
+
5274
+ # Write analysis files
5275
+ local diff_file="$adversarial_dir/$test_id/diff.txt"
5276
+ local files_file="$adversarial_dir/$test_id/files.txt"
5277
+ echo "$diff_content" > "$diff_file"
5278
+ echo "$changed_files" > "$files_file"
5279
+
5280
+ # Build adversarial prompt
5281
+ local adversarial_prompt="You are an ADVERSARIAL TESTER. Your goal is to BREAK the implementation.
5282
+
5283
+ CHANGED FILES:
5284
+ $(cat "$files_file")
5285
+
5286
+ DIFF:
5287
+ $(head -500 "$diff_file")
5288
+
5289
+ YOUR MISSION:
5290
+ 1. Find edge cases that will cause crashes or incorrect behavior
5291
+ 2. Identify inputs that bypass validation
5292
+ 3. Find race conditions or concurrency issues
5293
+ 4. Discover security vulnerabilities (injection, auth bypass, SSRF)
5294
+ 5. Find resource exhaustion vectors (unbounded loops, memory leaks)
5295
+ 6. Identify error handling gaps (missing try/catch, unchecked returns)
5296
+
5297
+ OUTPUT FORMAT (STRICT):
5298
+ ATTACK_VECTORS:
5299
+ - [severity] [category] description | reproduction steps
5300
+ Severity: Critical, High, Medium, Low
5301
+ Category: crash, security, correctness, performance, resource
5302
+
5303
+ SUGGESTED_TESTS:
5304
+ - Test description that would catch this issue
5305
+
5306
+ OVERALL_RISK: HIGH or MEDIUM or LOW"
5307
+
5308
+ local result_file="$adversarial_dir/$test_id/result.txt"
5309
+
5310
+ # Run adversarial agent
5311
+ log_info "Spawning adversarial agent..."
5312
+ case "${PROVIDER_NAME:-claude}" in
5313
+ claude)
5314
+ if command -v claude &>/dev/null; then
5315
+ claude --dangerously-skip-permissions -p "$adversarial_prompt" \
5316
+ --output-format text > "$result_file" 2>/dev/null || true
5317
+ fi
5318
+ ;;
5319
+ codex)
5320
+ if command -v codex &>/dev/null; then
5321
+ codex exec --full-auto "$adversarial_prompt" \
5322
+ > "$result_file" 2>/dev/null || true
5323
+ fi
5324
+ ;;
5325
+ gemini)
5326
+ if command -v gemini &>/dev/null; then
5327
+ invoke_gemini_capture "$adversarial_prompt" \
5328
+ > "$result_file" 2>/dev/null || true
5329
+ fi
5330
+ ;;
5331
+ *)
5332
+ echo "ATTACK_VECTORS: None (unknown provider)" > "$result_file"
5333
+ echo "OVERALL_RISK: LOW" >> "$result_file"
5334
+ ;;
5335
+ esac
5336
+
5337
+ if [ ! -s "$result_file" ]; then
5338
+ log_warn "Adversarial agent produced no output"
5339
+ return 0
5340
+ fi
5341
+
5342
+ # Parse risk level
5343
+ local risk_level
5344
+ risk_level=$(grep -i "OVERALL_RISK:" "$result_file" | head -1 | sed 's/.*OVERALL_RISK:[[:space:]]*//' | awk '{print toupper($1)}')
5345
+
5346
+ # Count critical/high attack vectors
5347
+ local critical_count high_count
5348
+ critical_count=$(grep -ci "\[critical\]" "$result_file" 2>/dev/null || echo "0")
5349
+ high_count=$(grep -ci "\[high\]" "$result_file" 2>/dev/null || echo "0")
5350
+
5351
+ log_info "Adversarial testing complete: risk=$risk_level, critical=$critical_count, high=$high_count"
5352
+
5353
+ emit_event_json "adversarial_test_complete" \
5354
+ "test_id=$test_id" \
5355
+ "risk_level=${risk_level:-UNKNOWN}" \
5356
+ "critical_count=$critical_count" \
5357
+ "high_count=$high_count" \
5358
+ "iteration=$ITERATION_COUNT"
5359
+
5360
+ # Block on critical findings
5361
+ if [ "$critical_count" -gt 0 ]; then
5362
+ log_error "ADVERSARIAL TEST BLOCKED: $critical_count critical attack vectors found"
5363
+ log_error "Details: $adversarial_dir/$test_id/result.txt"
5364
+ return 1
5365
+ fi
5366
+
5367
+ return 0
5368
+ }
5369
+
5128
5370
  load_solutions_context() {
5129
5371
  # Load relevant structured solutions for the current task context
5130
5372
  local context="$1"
@@ -6444,6 +6686,81 @@ except Exception as e:
6444
6686
  PYEOF
6445
6687
  }
6446
6688
 
6689
+ #===============================================================================
6690
+ # Knowledge Graph Integration (v6.0.0)
6691
+ # Enrich prompts with cross-project patterns and store new learnings.
6692
+ #===============================================================================
6693
+
6694
+ # Enrich prompt context with relevant cross-project patterns
6695
+ enrich_from_knowledge_graph() {
6696
+ local context="$1"
6697
+ local max_patterns="${2:-5}"
6698
+
6699
+ _LOKI_KG_CONTEXT="$context" _LOKI_KG_MAX="$max_patterns" \
6700
+ _LOKI_PROJECT_DIR="$PROJECT_DIR" \
6701
+ python3 << 'PYEOF' 2>/dev/null || echo ""
6702
+ import sys
6703
+ import os
6704
+ import json
6705
+
6706
+ project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
6707
+ context = os.environ.get('_LOKI_KG_CONTEXT', '')
6708
+ max_results = int(os.environ.get('_LOKI_KG_MAX', '5'))
6709
+
6710
+ if not project_dir:
6711
+ sys.exit(0)
6712
+ sys.path.insert(0, project_dir)
6713
+ try:
6714
+ from memory.knowledge_graph import OrganizationKnowledgeGraph
6715
+ kg = OrganizationKnowledgeGraph()
6716
+ patterns = kg.query_patterns(context, max_results=max_results)
6717
+ if patterns:
6718
+ output = "\n## Cross-Project Knowledge (from knowledge graph)\n"
6719
+ for p in patterns:
6720
+ name = p.get('name', p.get('pattern', 'unnamed'))
6721
+ category = p.get('category', '')
6722
+ desc = p.get('description', '')
6723
+ output += f"- **{name}** ({category}): {desc}\n"
6724
+ print(output)
6725
+ except Exception:
6726
+ pass
6727
+ PYEOF
6728
+ }
6729
+
6730
+ # Store new patterns to the knowledge graph after successful iterations
6731
+ store_to_knowledge_graph() {
6732
+ local target_dir="${TARGET_DIR:-.}"
6733
+
6734
+ _LOKI_PROJECT_DIR="$PROJECT_DIR" _LOKI_TARGET_DIR="$target_dir" \
6735
+ python3 << 'PYEOF' 2>/dev/null || true
6736
+ import sys
6737
+ import os
6738
+
6739
+ project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
6740
+ target_dir = os.environ.get('_LOKI_TARGET_DIR', '.')
6741
+
6742
+ sys.path.insert(0, project_dir)
6743
+ try:
6744
+ from memory.knowledge_graph import OrganizationKnowledgeGraph
6745
+ from pathlib import Path
6746
+
6747
+ kg = OrganizationKnowledgeGraph()
6748
+ project_dirs = [Path(target_dir)]
6749
+
6750
+ # Extract and store patterns
6751
+ patterns = kg.extract_patterns(project_dirs)
6752
+ if patterns:
6753
+ patterns = kg.deduplicate_patterns(patterns)
6754
+ kg.save_patterns(patterns)
6755
+
6756
+ # Rebuild graph
6757
+ kg.build_graph(project_dirs)
6758
+ kg.save_graph()
6759
+ except Exception:
6760
+ pass
6761
+ PYEOF
6762
+ }
6763
+
6447
6764
  #===============================================================================
6448
6765
  # Save/Load Wrapper State
6449
6766
  #===============================================================================
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "5.59.0"
10
+ __version__ = "6.0.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -665,14 +665,16 @@ class MigrationPipeline:
665
665
  completed_phases: list[str] = []
666
666
  for phase in PHASE_ORDER:
667
667
  status = manifest.phases.get(phase, {}).get("status", "pending")
668
- if status == "in_progress":
669
- current_phase = phase
670
- overall_status = "in_progress"
671
- break
672
668
  if status == "completed":
673
- current_phase = phase
674
669
  completed_phases.append(phase)
670
+ current_phase = phase
675
671
  overall_status = "in_progress" # partial completion
672
+ elif status == "in_progress":
673
+ current_phase = phase
674
+ overall_status = "in_progress"
675
+ elif status == "failed":
676
+ current_phase = phase
677
+ overall_status = "failed"
676
678
 
677
679
  # Feature stats
678
680
  features_total = 0
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v5.59.0
5
+ **Version:** v6.0.0
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '5.59.0'
60
+ __version__ = '6.0.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "5.59.0",
3
+ "version": "6.0.0",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "agent",
@@ -121,12 +121,62 @@ provider_get_tier_param() {
121
121
  fi
122
122
  }
123
123
 
124
+ # Dynamic model resolution (v6.0.0)
125
+ # Resolves a capability tier to a concrete model name at runtime.
126
+ # Respects LOKI_MAX_TIER to cap cost (e.g., maxTier=sonnet prevents opus usage).
127
+ # Capability aliases: "best" -> planning tier, "fast" -> fast tier, "balanced" -> development tier
128
+ resolve_model_for_tier() {
129
+ local tier="$1"
130
+
131
+ # Handle capability aliases
132
+ case "$tier" in
133
+ best) tier="planning" ;;
134
+ balanced) tier="development" ;;
135
+ cheap) tier="fast" ;;
136
+ esac
137
+
138
+ local max_tier="${LOKI_MAX_TIER:-}"
139
+ local model=""
140
+
141
+ # Resolve tier to model
142
+ case "$tier" in
143
+ planning) model="$PROVIDER_MODEL_PLANNING" ;;
144
+ development) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
145
+ fast) model="$PROVIDER_MODEL_FAST" ;;
146
+ *) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
147
+ esac
148
+
149
+ # Apply maxTier ceiling if set
150
+ if [ -n "$max_tier" ]; then
151
+ case "$max_tier" in
152
+ haiku)
153
+ # Cap everything to haiku/fast
154
+ model="$PROVIDER_MODEL_FAST"
155
+ ;;
156
+ sonnet)
157
+ # Cap planning to development
158
+ if [ "$tier" = "planning" ]; then
159
+ model="$PROVIDER_MODEL_DEVELOPMENT"
160
+ fi
161
+ ;;
162
+ opus)
163
+ # No cap needed, opus is max
164
+ ;;
165
+ esac
166
+ fi
167
+
168
+ echo "$model"
169
+ }
170
+
124
171
  # Tier-aware invocation (Claude supports model selection via --model flag)
125
172
  provider_invoke_with_tier() {
126
173
  local tier="$1"
127
174
  local prompt="$2"
128
175
  shift 2
129
176
  local model
130
- model=$(provider_get_tier_param "$tier")
131
- claude --dangerously-skip-permissions --model "$model" -p "$prompt" "$@"
177
+ model=$(resolve_model_for_tier "$tier")
178
+ # Extract short name for Task tool (opus/sonnet/haiku)
179
+ local short_model
180
+ short_model=$(echo "$model" | sed 's/claude-\([a-z]*\).*/\1/')
181
+ claude --dangerously-skip-permissions --model "$short_model" -p "$prompt" "$@"
132
182
  }
@@ -113,15 +113,50 @@ provider_get_tier_param() {
113
113
  esac
114
114
  }
115
115
 
116
+ # Dynamic model resolution (v6.0.0)
117
+ # Resolves a capability tier to a concrete effort level at runtime.
118
+ # Codex uses a single model with effort parameter, so maxTier maps to effort cap.
119
+ resolve_model_for_tier() {
120
+ local tier="$1"
121
+
122
+ # Handle capability aliases
123
+ case "$tier" in
124
+ best) tier="planning" ;;
125
+ balanced) tier="development" ;;
126
+ cheap) tier="fast" ;;
127
+ esac
128
+
129
+ local max_tier="${LOKI_MAX_TIER:-}"
130
+ local effort=""
131
+
132
+ case "$tier" in
133
+ planning) effort="$PROVIDER_EFFORT_PLANNING" ;;
134
+ development) effort="$PROVIDER_EFFORT_DEVELOPMENT" ;;
135
+ fast) effort="$PROVIDER_EFFORT_FAST" ;;
136
+ *) effort="$PROVIDER_EFFORT_DEVELOPMENT" ;;
137
+ esac
138
+
139
+ # Apply maxTier ceiling (maps to effort levels)
140
+ if [ -n "$max_tier" ]; then
141
+ case "$max_tier" in
142
+ haiku|low) effort="low" ;;
143
+ sonnet|high)
144
+ if [ "$effort" = "xhigh" ]; then effort="high"; fi
145
+ ;;
146
+ opus|xhigh) ;; # No cap
147
+ esac
148
+ fi
149
+
150
+ echo "$effort"
151
+ }
152
+
116
153
  # Tier-aware invocation
117
- # Note: Codex CLI does not support effort CLI flags
118
- # Effort must be configured via environment: CODEX_MODEL_REASONING_EFFORT
119
- # This function sets the env var before invocation
154
+ # Codex CLI uses CODEX_MODEL_REASONING_EFFORT env var for effort control
120
155
  provider_invoke_with_tier() {
121
156
  local tier="$1"
122
157
  local prompt="$2"
123
158
  shift 2
124
159
  local effort
125
- effort=$(provider_get_tier_param "$tier")
160
+ effort=$(resolve_model_for_tier "$tier")
126
161
  CODEX_MODEL_REASONING_EFFORT="$effort" codex exec --full-auto "$prompt" "$@"
127
162
  }
@@ -139,6 +139,48 @@ provider_get_tier_param() {
139
139
  esac
140
140
  }
141
141
 
142
+ # Dynamic model resolution (v6.0.0)
143
+ # Resolves a capability tier to a concrete model name at runtime.
144
+ # Respects LOKI_MAX_TIER to cap cost.
145
+ resolve_model_for_tier() {
146
+ local tier="$1"
147
+
148
+ # Handle capability aliases
149
+ case "$tier" in
150
+ best) tier="planning" ;;
151
+ balanced) tier="development" ;;
152
+ cheap) tier="fast" ;;
153
+ esac
154
+
155
+ local max_tier="${LOKI_MAX_TIER:-}"
156
+ local model=""
157
+
158
+ case "$tier" in
159
+ planning) model="$PROVIDER_MODEL_PLANNING" ;;
160
+ development) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
161
+ fast) model="$PROVIDER_MODEL_FAST" ;;
162
+ *) model="$PROVIDER_MODEL_DEVELOPMENT" ;;
163
+ esac
164
+
165
+ # Apply maxTier ceiling
166
+ if [ -n "$max_tier" ]; then
167
+ case "$max_tier" in
168
+ haiku|flash)
169
+ model="$PROVIDER_MODEL_FAST"
170
+ ;;
171
+ sonnet|pro)
172
+ # Cap planning to development (pro)
173
+ if [ "$tier" = "planning" ]; then
174
+ model="$PROVIDER_MODEL_DEVELOPMENT"
175
+ fi
176
+ ;;
177
+ opus) ;; # No cap
178
+ esac
179
+ fi
180
+
181
+ echo "$model"
182
+ }
183
+
142
184
  # Tier-aware invocation with rate limit fallback
143
185
  # Uses --model flag to specify model
144
186
  # Falls back to flash model if pro hits rate limit
@@ -148,9 +190,8 @@ provider_invoke_with_tier() {
148
190
  local prompt="$2"
149
191
  shift 2
150
192
 
151
- # Select model based on tier
152
- local model="$PROVIDER_MODEL"
153
- [[ "$tier" == "fast" ]] && model="$PROVIDER_MODEL_FAST"
193
+ local model
194
+ model=$(resolve_model_for_tier "$tier")
154
195
 
155
196
  echo "[loki] Using tier: $tier, model: $model" >&2
156
197