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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +34 -9
- package/autonomy/issue-providers.sh +423 -0
- package/autonomy/loki +859 -19
- package/autonomy/run.sh +320 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +7 -5
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/providers/claude.sh +52 -2
- package/providers/codex.sh +39 -4
- package/providers/gemini.sh +44 -3
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
|
|
180
|
+
TEMP_SCRIPT=$(mktemp /tmp/loki-run-XXXXXX.sh)
|
|
181
181
|
cp "${BASH_SOURCE[0]}" "$TEMP_SCRIPT"
|
|
182
|
-
chmod
|
|
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
|
-
#
|
|
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
|
#===============================================================================
|
package/dashboard/__init__.py
CHANGED
|
@@ -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
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
package/providers/claude.sh
CHANGED
|
@@ -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=$(
|
|
131
|
-
|
|
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
|
}
|
package/providers/codex.sh
CHANGED
|
@@ -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
|
-
#
|
|
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=$(
|
|
160
|
+
effort=$(resolve_model_for_tier "$tier")
|
|
126
161
|
CODEX_MODEL_REASONING_EFFORT="$effort" codex exec --full-auto "$prompt" "$@"
|
|
127
162
|
}
|
package/providers/gemini.sh
CHANGED
|
@@ -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
|
-
|
|
152
|
-
|
|
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
|
|