loki-mode 6.76.1 → 6.77.1
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/openspec-adapter.py +12 -3
- package/autonomy/run.sh +81 -20
- package/dashboard/__init__.py +1 -1
- package/dashboard/static/index.html +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/providers/aider.sh +14 -1
- package/providers/claude.sh +16 -2
- package/providers/cline.sh +14 -1
- package/providers/model_catalog.json +82 -0
- package/providers/models.sh +79 -0
- package/references/multi-provider.md +4 -4
- package/skills/production.md +2 -2
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.77.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -272,4 +272,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
272
272
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
273
273
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
274
274
|
|
|
275
|
-
**v6.
|
|
275
|
+
**v6.77.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.77.1
|
|
@@ -345,9 +345,16 @@ def _parse_scenario(name: str, body: str) -> Dict[str, Any]:
|
|
|
345
345
|
|
|
346
346
|
# -- Tasks Parsing ------------------------------------------------------------
|
|
347
347
|
|
|
348
|
-
def parse_tasks(tasks_path: Path) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]]]:
|
|
348
|
+
def parse_tasks(tasks_path: Path, change_name: str = "") -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]]]:
|
|
349
349
|
"""Parse tasks.md into structured task list and source map.
|
|
350
350
|
|
|
351
|
+
Args:
|
|
352
|
+
tasks_path: path to the change's tasks.md
|
|
353
|
+
change_name: OpenSpec change name, used to scope task IDs so that IDs
|
|
354
|
+
from different changes cannot collide in the pending queue. Default
|
|
355
|
+
empty string preserves backward compatibility with any caller that
|
|
356
|
+
does not pass the argument.
|
|
357
|
+
|
|
351
358
|
Returns:
|
|
352
359
|
(tasks_list, source_map)
|
|
353
360
|
tasks_list: list of task objects
|
|
@@ -373,7 +380,9 @@ def parse_tasks(tasks_path: Path) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[
|
|
|
373
380
|
checked = task_match.group(1).lower() == "x"
|
|
374
381
|
task_id_num = task_match.group(2)
|
|
375
382
|
description = task_match.group(3).strip()
|
|
376
|
-
task_id =
|
|
383
|
+
task_id = (
|
|
384
|
+
f"openspec-{change_name}-{task_id_num}" if change_name else f"openspec-{task_id_num}"
|
|
385
|
+
)
|
|
377
386
|
|
|
378
387
|
task = {
|
|
379
388
|
"id": task_id,
|
|
@@ -696,7 +705,7 @@ def run(
|
|
|
696
705
|
source_map: Dict[str, Dict[str, Any]] = {}
|
|
697
706
|
tasks_path = change_dir / "tasks.md"
|
|
698
707
|
if tasks_path.exists():
|
|
699
|
-
tasks_list, source_map = parse_tasks(tasks_path)
|
|
708
|
+
tasks_list, source_map = parse_tasks(tasks_path, change_name=change_name)
|
|
700
709
|
|
|
701
710
|
# -- Parse design.md (optional) --
|
|
702
711
|
design_data: Optional[Dict[str, str]] = None
|
package/autonomy/run.sh
CHANGED
|
@@ -1453,7 +1453,7 @@ get_provider_tier_param() {
|
|
|
1453
1453
|
echo "${CLINE_DEFAULT_MODEL:-${LOKI_CLINE_MODEL:-default}}"
|
|
1454
1454
|
;;
|
|
1455
1455
|
aider)
|
|
1456
|
-
echo "${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-
|
|
1456
|
+
echo "${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-opus-4-7}}"
|
|
1457
1457
|
;;
|
|
1458
1458
|
*)
|
|
1459
1459
|
echo "development"
|
|
@@ -3143,7 +3143,7 @@ invoke_cline_capture() {
|
|
|
3143
3143
|
invoke_aider() {
|
|
3144
3144
|
local prompt="$1"
|
|
3145
3145
|
shift
|
|
3146
|
-
local model="${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-
|
|
3146
|
+
local model="${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-opus-4-7}}"
|
|
3147
3147
|
local extra_flags="${LOKI_AIDER_FLAGS:-}"
|
|
3148
3148
|
# shellcheck disable=SC2086
|
|
3149
3149
|
# < /dev/null prevents aider from blocking on stdin in non-interactive mode
|
|
@@ -3156,7 +3156,7 @@ invoke_aider() {
|
|
|
3156
3156
|
invoke_aider_capture() {
|
|
3157
3157
|
local prompt="$1"
|
|
3158
3158
|
shift
|
|
3159
|
-
local model="${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-
|
|
3159
|
+
local model="${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-opus-4-7}}"
|
|
3160
3160
|
local extra_flags="${LOKI_AIDER_FLAGS:-}"
|
|
3161
3161
|
# shellcheck disable=SC2086
|
|
3162
3162
|
aider --message "$prompt" --yes-always --no-auto-commits \
|
|
@@ -3594,9 +3594,13 @@ except: pass
|
|
|
3594
3594
|
local prd_escaped
|
|
3595
3595
|
prd_escaped=$(printf '%s' "${prd:-Codebase Analysis}" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g')
|
|
3596
3596
|
|
|
3597
|
-
# Build enriched task JSON with pending task context
|
|
3598
|
-
|
|
3599
|
-
|
|
3597
|
+
# Build enriched task JSON with pending task context.
|
|
3598
|
+
# Must initialize to empty: this script runs under `set -u` (line 152),
|
|
3599
|
+
# so `local task_json` without a value leaves it unset. When the pending
|
|
3600
|
+
# queue is empty, the enrichment `if` is skipped and the `-z` check below
|
|
3601
|
+
# would fire on an unset variable and kill the run.
|
|
3602
|
+
local task_json=""
|
|
3603
|
+
if [[ -n "${next_task_context:-}" ]]; then
|
|
3600
3604
|
task_json=$(python3 -c "
|
|
3601
3605
|
import json, sys
|
|
3602
3606
|
ctx = json.loads('''$next_task_context''')
|
|
@@ -3619,11 +3623,11 @@ if ctx.get('source'):
|
|
|
3619
3623
|
if ctx.get('project'):
|
|
3620
3624
|
task['project'] = ctx['project']
|
|
3621
3625
|
print(json.dumps(task, indent=2))
|
|
3622
|
-
" 2>/dev/null)
|
|
3626
|
+
" 2>/dev/null) || task_json=""
|
|
3623
3627
|
fi
|
|
3624
3628
|
|
|
3625
3629
|
# Fallback to basic task JSON if enrichment failed
|
|
3626
|
-
if [[ -z "$task_json" ]]; then
|
|
3630
|
+
if [[ -z "${task_json:-}" ]]; then
|
|
3627
3631
|
task_json=$(cat <<EOF
|
|
3628
3632
|
{
|
|
3629
3633
|
"id": "$task_id",
|
|
@@ -3749,7 +3753,7 @@ track_iteration_complete() {
|
|
|
3749
3753
|
elif [ "${PROVIDER_NAME:-claude}" = "cline" ]; then
|
|
3750
3754
|
model_tier="${CLINE_DEFAULT_MODEL:-${LOKI_CLINE_MODEL:-sonnet}}"
|
|
3751
3755
|
elif [ "${PROVIDER_NAME:-claude}" = "aider" ]; then
|
|
3752
|
-
model_tier="${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-
|
|
3756
|
+
model_tier="${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-opus-4-7}}"
|
|
3753
3757
|
fi
|
|
3754
3758
|
local phase="${LAST_KNOWN_PHASE:-}"
|
|
3755
3759
|
[ -z "$phase" ] && phase=$(python3 -c "import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
@@ -8901,18 +8905,68 @@ bmad_write_back() {
|
|
|
8901
8905
|
# OpenSpec Task Queue Population
|
|
8902
8906
|
#===============================================================================
|
|
8903
8907
|
|
|
8904
|
-
#
|
|
8905
|
-
#
|
|
8908
|
+
# Compute a content hash for a file (cross-platform: uses Python hashlib so
|
|
8909
|
+
# behavior is identical on macOS and Linux, no md5/md5sum fork).
|
|
8910
|
+
_openspec_content_hash() {
|
|
8911
|
+
local file="$1"
|
|
8912
|
+
[[ -f "$file" ]] || { echo "none"; return 0; }
|
|
8913
|
+
python3 -c "import hashlib,sys; print(hashlib.md5(open(sys.argv[1],'rb').read()).hexdigest())" "$file" 2>/dev/null || echo "none"
|
|
8914
|
+
}
|
|
8915
|
+
|
|
8916
|
+
# Remove all tasks with source=="openspec" from a queue JSON file, preserving
|
|
8917
|
+
# every other source (prd, bmad, mirofish). Atomic: writes tmp + renames.
|
|
8918
|
+
purge_openspec_from_queue() {
|
|
8919
|
+
local queue_file="$1"
|
|
8920
|
+
[[ -f "$queue_file" ]] || return 0
|
|
8921
|
+
local tmp="${queue_file}.tmp.$$"
|
|
8922
|
+
if jq '[.[] | select(.source != "openspec")]' "$queue_file" > "$tmp" 2>/dev/null; then
|
|
8923
|
+
local before after
|
|
8924
|
+
before=$(jq 'length' "$queue_file" 2>/dev/null || echo 0)
|
|
8925
|
+
after=$(jq 'length' "$tmp" 2>/dev/null || echo 0)
|
|
8926
|
+
mv "$tmp" "$queue_file"
|
|
8927
|
+
if [[ "$before" != "$after" ]]; then
|
|
8928
|
+
log_info "Purged $((before - after)) OpenSpec tasks from $(basename "$queue_file")"
|
|
8929
|
+
fi
|
|
8930
|
+
else
|
|
8931
|
+
rm -f "$tmp"
|
|
8932
|
+
log_warn "Could not purge OpenSpec tasks from $(basename "$queue_file") (jq failed)"
|
|
8933
|
+
return 1
|
|
8934
|
+
fi
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
# Populate the task queue from OpenSpec task artifacts.
|
|
8938
|
+
# The sentinel .loki/queue/.openspec-populated is scoped per change:
|
|
8939
|
+
# line 1 = change path, line 2 = content hash of openspec-tasks.json.
|
|
8940
|
+
# Same path + same hash -> skip (crash-restart preserves progress).
|
|
8941
|
+
# Different path -> change switched, purge stale tasks and repopulate.
|
|
8942
|
+
# Same path + different hash -> tasks.md edited, purge and repopulate.
|
|
8906
8943
|
populate_openspec_queue() {
|
|
8907
8944
|
# Skip if no OpenSpec tasks file
|
|
8908
8945
|
if [[ ! -f ".loki/openspec-tasks.json" ]]; then
|
|
8909
8946
|
return 0
|
|
8910
8947
|
fi
|
|
8911
8948
|
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8949
|
+
local sentinel=".loki/queue/.openspec-populated"
|
|
8950
|
+
local current_path="${OPENSPEC_CHANGE_PATH:-}"
|
|
8951
|
+
local current_hash
|
|
8952
|
+
current_hash="$(_openspec_content_hash ".loki/openspec-tasks.json")"
|
|
8953
|
+
|
|
8954
|
+
if [[ -f "$sentinel" ]]; then
|
|
8955
|
+
local stored_path stored_hash
|
|
8956
|
+
stored_path="$(sed -n '1p' "$sentinel")"
|
|
8957
|
+
stored_hash="$(sed -n '2p' "$sentinel")"
|
|
8958
|
+
if [[ "$stored_path" == "$current_path" ]] && [[ "$stored_hash" == "$current_hash" ]]; then
|
|
8959
|
+
log_info "OpenSpec queue already populated for this change (path + hash match), skipping"
|
|
8960
|
+
return 0
|
|
8961
|
+
fi
|
|
8962
|
+
if [[ "$stored_path" != "$current_path" ]]; then
|
|
8963
|
+
log_info "OpenSpec change switched (was: ${stored_path:-<legacy>}, now: ${current_path:-<unset>}) -- purging stale OpenSpec tasks"
|
|
8964
|
+
else
|
|
8965
|
+
log_info "OpenSpec tasks.md content changed (hash mismatch) -- purging and reloading"
|
|
8966
|
+
fi
|
|
8967
|
+
purge_openspec_from_queue ".loki/queue/pending.json"
|
|
8968
|
+
purge_openspec_from_queue ".loki/queue/in-progress.json"
|
|
8969
|
+
purge_openspec_from_queue ".loki/queue/completed.json"
|
|
8916
8970
|
fi
|
|
8917
8971
|
|
|
8918
8972
|
log_step "Populating task queue from OpenSpec tasks..."
|
|
@@ -8985,8 +9039,9 @@ OPENSPEC_QUEUE_EOF
|
|
|
8985
9039
|
return 0
|
|
8986
9040
|
fi
|
|
8987
9041
|
|
|
8988
|
-
# Mark as populated so we don't
|
|
8989
|
-
|
|
9042
|
+
# Mark as populated for this specific change + content hash so we don't
|
|
9043
|
+
# re-add on restart but DO repopulate when change-switching or tasks.md edits.
|
|
9044
|
+
printf '%s\n%s\n' "${OPENSPEC_CHANGE_PATH:-}" "$current_hash" > ".loki/queue/.openspec-populated"
|
|
8990
9045
|
log_info "OpenSpec queue population complete"
|
|
8991
9046
|
}
|
|
8992
9047
|
|
|
@@ -9836,7 +9891,13 @@ def process_stream():
|
|
|
9836
9891
|
elif tool == "Bash":
|
|
9837
9892
|
tool_desc = tool_input.get("description", tool_input.get("command", "")[:60])
|
|
9838
9893
|
elif tool == "Grep":
|
|
9839
|
-
|
|
9894
|
+
# This Python block runs inside bash `python3 -u -c '...'`,
|
|
9895
|
+
# wrapped in a bash single-quoted string. A single-quoted
|
|
9896
|
+
# Python literal here would close bash SQ mid-code and
|
|
9897
|
+
# Python would receive a bare identifier instead of the
|
|
9898
|
+
# "pattern" string, crashing with NameError on every Grep
|
|
9899
|
+
# tool call. Use double quotes + concatenation only.
|
|
9900
|
+
tool_desc = "pattern: " + tool_input.get("pattern", "")
|
|
9840
9901
|
elif tool == "Glob":
|
|
9841
9902
|
tool_desc = tool_input.get("pattern", "")
|
|
9842
9903
|
|
|
@@ -9991,8 +10052,8 @@ if __name__ == "__main__":
|
|
|
9991
10052
|
;;
|
|
9992
10053
|
aider)
|
|
9993
10054
|
# Aider: Tier 3 - degraded mode, 18+ providers
|
|
9994
|
-
echo "[loki] Aider model: ${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-
|
|
9995
|
-
echo "[loki] Aider model: ${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-
|
|
10055
|
+
echo "[loki] Aider model: ${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-opus-4-7}}, tier: $tier_param" >> "$log_file"
|
|
10056
|
+
echo "[loki] Aider model: ${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-opus-4-7}}, tier: $tier_param" >> "$agent_log"
|
|
9996
10057
|
{ invoke_aider "$prompt" 2>&1 | tee -a "$log_file" "$agent_log" "$iter_output"; \
|
|
9997
10058
|
} && exit_code=0 || exit_code=$?
|
|
9998
10059
|
;;
|
package/dashboard/__init__.py
CHANGED
|
@@ -11085,7 +11085,7 @@ var LokiDashboard=(()=>{var kt=Object.defineProperty;var Yt=Object.getOwnPropert
|
|
|
11085
11085
|
${s}
|
|
11086
11086
|
</div>
|
|
11087
11087
|
</div>
|
|
11088
|
-
`,this._bindEvents(),!this._paused){let r=t.querySelector(".activity-feed");r&&(r.scrollTop=0)}}};customElements.get("loki-activity-stream")||customElements.define("loki-activity-stream",ht);var $e={claude:{initial:"C",color:"#553DE9",bgColor:"rgba(85, 61, 233, 0.12)"},codex:{initial:"X",color:"#1FC5A8",bgColor:"rgba(31, 197, 168, 0.12)"},gemini:{initial:"G",color:"#2F71E3",bgColor:"rgba(47, 113, 227, 0.12)"},cline:{initial:"L",color:"#D4A03C",bgColor:"rgba(212, 160, 60, 0.12)"},aider:{initial:"A",color:"#C45B5B",bgColor:"rgba(196, 91, 91, 0.12)"}},Vt={healthy:"var(--loki-green, #1FC5A8)",degraded:"var(--loki-yellow, #D4A03C)",down:"var(--loki-red, #C45B5B)",unknown:"var(--loki-text-muted, #939084)"},ut=class extends u{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._providers=[],this._expandedProvider=null,this._api=null,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(t,e,i){e!==i&&(t==="api-url"&&this._api&&(this._api.baseUrl=i,this._loadData()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),1e4)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}async _loadData(){try{let t=await this._api._get("/api/v2/providers/health");this._providers=t.providers||[]}catch{this._providers.length===0&&(this._providers=this._getDemoData())}this.render()}_getDemoData(){return[{name:"claude",status:"healthy",latency_ms:245,tokens_used:125400,model:"claude-opus-4-
|
|
11088
|
+
`,this._bindEvents(),!this._paused){let r=t.querySelector(".activity-feed");r&&(r.scrollTop=0)}}};customElements.get("loki-activity-stream")||customElements.define("loki-activity-stream",ht);var $e={claude:{initial:"C",color:"#553DE9",bgColor:"rgba(85, 61, 233, 0.12)"},codex:{initial:"X",color:"#1FC5A8",bgColor:"rgba(31, 197, 168, 0.12)"},gemini:{initial:"G",color:"#2F71E3",bgColor:"rgba(47, 113, 227, 0.12)"},cline:{initial:"L",color:"#D4A03C",bgColor:"rgba(212, 160, 60, 0.12)"},aider:{initial:"A",color:"#C45B5B",bgColor:"rgba(196, 91, 91, 0.12)"}},Vt={healthy:"var(--loki-green, #1FC5A8)",degraded:"var(--loki-yellow, #D4A03C)",down:"var(--loki-red, #C45B5B)",unknown:"var(--loki-text-muted, #939084)"},ut=class extends u{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._providers=[],this._expandedProvider=null,this._api=null,this._pollInterval=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(t,e,i){e!==i&&(t==="api-url"&&this._api&&(this._api.baseUrl=i,this._loadData()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),1e4)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}async _loadData(){try{let t=await this._api._get("/api/v2/providers/health");this._providers=t.providers||[]}catch{this._providers.length===0&&(this._providers=this._getDemoData())}this.render()}_getDemoData(){return[{name:"claude",status:"healthy",latency_ms:245,tokens_used:125400,model:"claude-opus-4-7",api_version:"v1",rate_limit:{remaining:45,limit:50},cost_usd:3.42},{name:"codex",status:"degraded",latency_ms:890,tokens_used:45200,model:"gpt-5.3-codex",api_version:"v1",rate_limit:{remaining:12,limit:60},cost_usd:.87},{name:"gemini",status:"healthy",latency_ms:320,tokens_used:78600,model:"gemini-3-pro",api_version:"v1beta",rate_limit:{remaining:55,limit:60},cost_usd:1.15}]}_formatTokens(t){return t==null?"--":t>=1e6?(t/1e6).toFixed(1)+"M":t>=1e3?(t/1e3).toFixed(1)+"K":String(t)}_formatLatency(t){return t==null?"--":t<1e3?t+"ms":(t/1e3).toFixed(1)+"s"}_formatCost(t){return t==null?"--":"$"+t.toFixed(2)}_escapeHtml(t){return t?String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}_toggleExpand(t){this._expandedProvider=this._expandedProvider===t?null:t,this.render()}_bindEvents(){this.shadowRoot.querySelectorAll(".provider-card").forEach(e=>{e.addEventListener("click",()=>{this._toggleExpand(e.dataset.provider)})})}_getStyles(){return`
|
|
11089
11089
|
:host {
|
|
11090
11090
|
display: block;
|
|
11091
11091
|
}
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
package/providers/aider.sh
CHANGED
|
@@ -50,7 +50,20 @@ PROVIDER_MAX_PARALLEL=1
|
|
|
50
50
|
# Aider supports 18+ providers; model configured via LOKI_AIDER_MODEL env var
|
|
51
51
|
# or provider-specific env vars (OPENAI_API_KEY, OPENAI_API_BASE, etc.)
|
|
52
52
|
# NOTE: Aider uses litellm for model routing, so full model strings are needed (not CLI aliases)
|
|
53
|
-
|
|
53
|
+
# Aider default model -- reads from providers/model_catalog.json via models.sh when
|
|
54
|
+
# available so new model releases only require updating that single catalog file.
|
|
55
|
+
_aider_default_from_catalog() {
|
|
56
|
+
local script_dir
|
|
57
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
58
|
+
if [ -f "${script_dir}/models.sh" ]; then
|
|
59
|
+
# shellcheck source=./models.sh
|
|
60
|
+
source "${script_dir}/models.sh"
|
|
61
|
+
loki_latest_model aider development 2>/dev/null || echo "claude-opus-4-7"
|
|
62
|
+
else
|
|
63
|
+
echo "claude-opus-4-7"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
AIDER_DEFAULT_MODEL="${LOKI_AIDER_MODEL:-${LOKI_MODEL_DEVELOPMENT:-$(_aider_default_from_catalog)}}"
|
|
54
67
|
PROVIDER_MODEL_PLANNING="$AIDER_DEFAULT_MODEL"
|
|
55
68
|
PROVIDER_MODEL_DEVELOPMENT="$AIDER_DEFAULT_MODEL"
|
|
56
69
|
PROVIDER_MODEL_FAST="$AIDER_DEFAULT_MODEL"
|
package/providers/claude.sh
CHANGED
|
@@ -45,7 +45,13 @@ PROVIDER_MAX_PARALLEL=10
|
|
|
45
45
|
|
|
46
46
|
# Model Configuration (Abstract Tiers)
|
|
47
47
|
# Default: Haiku disabled for quality. Use --allow-haiku or LOKI_ALLOW_HAIKU=true to enable.
|
|
48
|
-
# Claude Code CLI resolves aliases (opus/sonnet/haiku) to latest
|
|
48
|
+
# The Claude Code CLI resolves aliases (opus/sonnet/haiku) to the latest available
|
|
49
|
+
# model at invocation time, so we pass aliases rather than dated IDs. The canonical
|
|
50
|
+
# mapping lives in providers/model_catalog.json (single source of truth):
|
|
51
|
+
# opus -> latest Opus (e.g. claude-opus-4-7 -- 1M context, adaptive thinking)
|
|
52
|
+
# sonnet -> latest Sonnet (e.g. claude-sonnet-4-6)
|
|
53
|
+
# haiku -> latest Haiku (e.g. claude-haiku-4-5)
|
|
54
|
+
# Override per tier with LOKI_CLAUDE_MODEL_PLANNING, _DEVELOPMENT, _FAST.
|
|
49
55
|
CLAUDE_DEFAULT_PLANNING="opus"
|
|
50
56
|
CLAUDE_DEFAULT_DEVELOPMENT="opus" # Opus for dev (was sonnet)
|
|
51
57
|
CLAUDE_DEFAULT_FAST="sonnet"
|
|
@@ -69,10 +75,18 @@ else
|
|
|
69
75
|
fi
|
|
70
76
|
|
|
71
77
|
# Context and Limits
|
|
72
|
-
|
|
78
|
+
# Opus 4.7 ships with 1M context at standard pricing (no long-context premium).
|
|
79
|
+
# RARV-C uses this headroom for deeper memory retrieval and longer task budgets.
|
|
80
|
+
PROVIDER_CONTEXT_WINDOW=1000000
|
|
73
81
|
PROVIDER_MAX_OUTPUT_TOKENS=128000
|
|
74
82
|
PROVIDER_RATE_LIMIT_RPM=50
|
|
75
83
|
|
|
84
|
+
# Effort / thinking defaults for Opus 4.7 (used when Loki invokes the API
|
|
85
|
+
# directly; the interactive CLI manages this automatically).
|
|
86
|
+
PROVIDER_DEFAULT_EFFORT="${LOKI_CLAUDE_EFFORT:-xhigh}" # xhigh recommended for coding
|
|
87
|
+
PROVIDER_DEFAULT_THINKING="${LOKI_CLAUDE_THINKING:-adaptive}"
|
|
88
|
+
PROVIDER_DEFAULT_TASK_BUDGET_TOKENS="${LOKI_CLAUDE_TASK_BUDGET:-0}" # 0 = unset (open-ended)
|
|
89
|
+
|
|
76
90
|
# Cost (USD per 1K tokens, approximate)
|
|
77
91
|
PROVIDER_COST_INPUT_PLANNING=0.015
|
|
78
92
|
PROVIDER_COST_OUTPUT_PLANNING=0.075
|
package/providers/cline.sh
CHANGED
|
@@ -51,7 +51,20 @@ PROVIDER_MAX_PARALLEL=1
|
|
|
51
51
|
# Cline supports 12+ providers; model configured via LOKI_CLINE_MODEL env var
|
|
52
52
|
# or `cline auth` one-time setup. Defaults are placeholders.
|
|
53
53
|
# NOTE: Cline uses its own model routing, so full model strings are needed (not CLI aliases)
|
|
54
|
-
|
|
54
|
+
# Cline default model -- reads from providers/model_catalog.json via models.sh when
|
|
55
|
+
# available so new model releases only require updating that single catalog file.
|
|
56
|
+
_cline_default_from_catalog() {
|
|
57
|
+
local script_dir
|
|
58
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
59
|
+
if [ -f "${script_dir}/models.sh" ]; then
|
|
60
|
+
# shellcheck source=./models.sh
|
|
61
|
+
source "${script_dir}/models.sh"
|
|
62
|
+
loki_latest_model cline development 2>/dev/null || echo "claude-opus-4-7"
|
|
63
|
+
else
|
|
64
|
+
echo "claude-opus-4-7"
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
CLINE_DEFAULT_MODEL="${LOKI_CLINE_MODEL:-${LOKI_MODEL_DEVELOPMENT:-$(_cline_default_from_catalog)}}"
|
|
55
68
|
PROVIDER_MODEL_PLANNING="$CLINE_DEFAULT_MODEL"
|
|
56
69
|
PROVIDER_MODEL_DEVELOPMENT="$CLINE_DEFAULT_MODEL"
|
|
57
70
|
PROVIDER_MODEL_FAST="$CLINE_DEFAULT_MODEL"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Canonical model catalog. Update this single file when a provider ships a new model. Providers/web-app/docs read from here.",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"updated": "2026-04-18",
|
|
5
|
+
"providers": {
|
|
6
|
+
"claude": {
|
|
7
|
+
"latest_planning": "claude-opus-4-7",
|
|
8
|
+
"latest_development": "claude-opus-4-7",
|
|
9
|
+
"latest_fast": "claude-sonnet-4-6",
|
|
10
|
+
"cli_aliases": {
|
|
11
|
+
"opus": "claude-opus-4-7",
|
|
12
|
+
"sonnet": "claude-sonnet-4-6",
|
|
13
|
+
"haiku": "claude-haiku-4-5"
|
|
14
|
+
},
|
|
15
|
+
"models": [
|
|
16
|
+
{
|
|
17
|
+
"id": "claude-opus-4-7",
|
|
18
|
+
"alias": "opus",
|
|
19
|
+
"tier": "planning",
|
|
20
|
+
"context_window": 1000000,
|
|
21
|
+
"max_output": 128000,
|
|
22
|
+
"notes": "Adaptive thinking, xhigh effort for agentic coding"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "claude-sonnet-4-6",
|
|
26
|
+
"alias": "sonnet",
|
|
27
|
+
"tier": "development",
|
|
28
|
+
"context_window": 1000000,
|
|
29
|
+
"max_output": 64000
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "claude-haiku-4-5",
|
|
33
|
+
"alias": "haiku",
|
|
34
|
+
"tier": "fast",
|
|
35
|
+
"context_window": 200000,
|
|
36
|
+
"max_output": 64000
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"codex": {
|
|
41
|
+
"latest_planning": "gpt-5.3-codex",
|
|
42
|
+
"latest_development": "gpt-5.3-codex",
|
|
43
|
+
"latest_fast": "gpt-5.3-codex",
|
|
44
|
+
"models": [
|
|
45
|
+
{ "id": "gpt-5.3-codex", "tier": "planning" },
|
|
46
|
+
{ "id": "o3", "tier": "planning" },
|
|
47
|
+
{ "id": "o4-mini", "tier": "fast" }
|
|
48
|
+
],
|
|
49
|
+
"notes": "Codex uses a single model with effort level (xhigh/high/low) for tier differentiation"
|
|
50
|
+
},
|
|
51
|
+
"gemini": {
|
|
52
|
+
"latest_planning": "gemini-3-pro-preview",
|
|
53
|
+
"latest_development": "gemini-3-pro-preview",
|
|
54
|
+
"latest_fast": "gemini-3-flash-preview",
|
|
55
|
+
"models": [
|
|
56
|
+
{ "id": "gemini-3-pro-preview", "tier": "planning", "context_window": 1000000 },
|
|
57
|
+
{ "id": "gemini-3-flash-preview", "tier": "fast", "context_window": 1000000 }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"cline": {
|
|
61
|
+
"latest_planning": "claude-opus-4-7",
|
|
62
|
+
"latest_development": "claude-opus-4-7",
|
|
63
|
+
"latest_fast": "claude-sonnet-4-6",
|
|
64
|
+
"models": [
|
|
65
|
+
{ "id": "claude-opus-4-7", "tier": "planning" },
|
|
66
|
+
{ "id": "claude-sonnet-4-6", "tier": "development" },
|
|
67
|
+
{ "id": "gpt-4.1", "tier": "development" }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"aider": {
|
|
71
|
+
"latest_planning": "claude-opus-4-7",
|
|
72
|
+
"latest_development": "claude-opus-4-7",
|
|
73
|
+
"latest_fast": "claude-sonnet-4-6",
|
|
74
|
+
"models": [
|
|
75
|
+
{ "id": "claude-opus-4-7", "tier": "planning" },
|
|
76
|
+
{ "id": "claude-sonnet-4-6", "tier": "development" },
|
|
77
|
+
{ "id": "gpt-4.1", "tier": "development" },
|
|
78
|
+
{ "id": "ollama_chat/deepseek-coder", "tier": "fast" }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Dynamic model-catalog loader for Loki Mode providers.
|
|
3
|
+
#
|
|
4
|
+
# Instead of hardcoding dated model IDs (e.g. claude-sonnet-4-5-20250929)
|
|
5
|
+
# throughout the codebase, every provider and caller reads from the single
|
|
6
|
+
# source of truth at providers/model_catalog.json. When a new model ships,
|
|
7
|
+
# update that one JSON file and every provider picks it up.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# source providers/models.sh
|
|
11
|
+
# model=$(loki_latest_model claude planning) # -> claude-opus-4-7
|
|
12
|
+
# model=$(loki_latest_model gemini fast) # -> gemini-3-flash-preview
|
|
13
|
+
#
|
|
14
|
+
# Env override order: LOKI_<PROVIDER>_MODEL_<TIER> > LOKI_<PROVIDER>_MODEL > catalog latest.
|
|
15
|
+
|
|
16
|
+
# Resolve catalog path relative to this script, regardless of CWD.
|
|
17
|
+
_LOKI_MODELS_SH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
18
|
+
LOKI_MODEL_CATALOG="${LOKI_MODEL_CATALOG:-$_LOKI_MODELS_SH_DIR/model_catalog.json}"
|
|
19
|
+
|
|
20
|
+
# Return the "latest_<tier>" id for a provider from the catalog.
|
|
21
|
+
# Args: $1 provider (claude|codex|gemini|cline|aider)
|
|
22
|
+
# $2 tier (planning|development|fast)
|
|
23
|
+
loki_latest_model() {
|
|
24
|
+
local provider="${1:-claude}"
|
|
25
|
+
local tier="${2:-planning}"
|
|
26
|
+
local tier_upper
|
|
27
|
+
tier_upper=$(printf '%s' "$tier" | tr '[:lower:]' '[:upper:]')
|
|
28
|
+
local provider_upper
|
|
29
|
+
provider_upper=$(printf '%s' "$provider" | tr '[:lower:]' '[:upper:]')
|
|
30
|
+
|
|
31
|
+
# Env override chain
|
|
32
|
+
local override="LOKI_${provider_upper}_MODEL_${tier_upper}"
|
|
33
|
+
if [ -n "${!override:-}" ]; then
|
|
34
|
+
printf '%s' "${!override}"
|
|
35
|
+
return 0
|
|
36
|
+
fi
|
|
37
|
+
local generic_override="LOKI_${provider_upper}_MODEL"
|
|
38
|
+
if [ -n "${!generic_override:-}" ]; then
|
|
39
|
+
printf '%s' "${!generic_override}"
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [ ! -f "$LOKI_MODEL_CATALOG" ]; then
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
# Require python3 (all Loki runtimes ship with it).
|
|
47
|
+
python3 - "$LOKI_MODEL_CATALOG" "$provider" "$tier" <<'PY'
|
|
48
|
+
import json, sys
|
|
49
|
+
catalog_path, provider, tier = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
50
|
+
with open(catalog_path) as fh:
|
|
51
|
+
data = json.load(fh)
|
|
52
|
+
p = data.get("providers", {}).get(provider)
|
|
53
|
+
if not p:
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
model = p.get(f"latest_{tier}")
|
|
56
|
+
if not model:
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
print(model)
|
|
59
|
+
PY
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Print full catalog for a provider as lines: <id>\t<tier>\t<alias?>
|
|
63
|
+
# Useful for `loki provider models <name>` output.
|
|
64
|
+
loki_list_models() {
|
|
65
|
+
local provider="${1:-claude}"
|
|
66
|
+
if [ ! -f "$LOKI_MODEL_CATALOG" ]; then
|
|
67
|
+
return 1
|
|
68
|
+
fi
|
|
69
|
+
python3 - "$LOKI_MODEL_CATALOG" "$provider" <<'PY'
|
|
70
|
+
import json, sys
|
|
71
|
+
catalog_path, provider = sys.argv[1], sys.argv[2]
|
|
72
|
+
with open(catalog_path) as fh:
|
|
73
|
+
data = json.load(fh)
|
|
74
|
+
p = data.get("providers", {}).get(provider, {})
|
|
75
|
+
for m in p.get("models", []):
|
|
76
|
+
alias = m.get("alias", "")
|
|
77
|
+
print(f"{m.get('id','')}\t{m.get('tier','')}\t{alias}")
|
|
78
|
+
PY
|
|
79
|
+
}
|
|
@@ -57,15 +57,15 @@ PROVIDER_MAX_PARALLEL=10 # Maximum concurrent agents
|
|
|
57
57
|
|
|
58
58
|
#### Model Configuration
|
|
59
59
|
```bash
|
|
60
|
-
PROVIDER_MODEL_PLANNING="claude-opus-4-
|
|
61
|
-
PROVIDER_MODEL_DEVELOPMENT="claude-sonnet-4-
|
|
60
|
+
PROVIDER_MODEL_PLANNING="claude-opus-4-7"
|
|
61
|
+
PROVIDER_MODEL_DEVELOPMENT="claude-sonnet-4-6"
|
|
62
62
|
PROVIDER_MODEL_FAST="claude-haiku-4-5-20251001"
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
#### Rate Limiting
|
|
66
66
|
```bash
|
|
67
|
-
PROVIDER_RATE_LIMIT_RPM=50
|
|
68
|
-
PROVIDER_CONTEXT_WINDOW=
|
|
67
|
+
PROVIDER_RATE_LIMIT_RPM=50 # Requests per minute
|
|
68
|
+
PROVIDER_CONTEXT_WINDOW=1000000 # Max context tokens (Opus 4.7: 1M at standard pricing)
|
|
69
69
|
PROVIDER_MAX_OUTPUT_TOKENS=128000
|
|
70
70
|
```
|
|
71
71
|
|
package/skills/production.md
CHANGED
|
@@ -226,7 +226,7 @@ def batch_code_review(files: list[str]) -> str:
|
|
|
226
226
|
Request(
|
|
227
227
|
custom_id=f"review-{i}-{file.replace('/', '-')}",
|
|
228
228
|
params=MessageCreateParamsNonStreaming(
|
|
229
|
-
model="claude-sonnet-4-
|
|
229
|
+
model="claude-sonnet-4-6",
|
|
230
230
|
max_tokens=2048,
|
|
231
231
|
messages=[{
|
|
232
232
|
"role": "user",
|
|
@@ -275,7 +275,7 @@ requests = [
|
|
|
275
275
|
Request(
|
|
276
276
|
custom_id=f"review-{file}",
|
|
277
277
|
params=MessageCreateParamsNonStreaming(
|
|
278
|
-
model="claude-sonnet-4-
|
|
278
|
+
model="claude-sonnet-4-6",
|
|
279
279
|
max_tokens=2048,
|
|
280
280
|
system=SHARED_SYSTEM, # Identical across all requests
|
|
281
281
|
messages=[{"role": "user", "content": f"Review: {code}"}]
|