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 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.76.1
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.76.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
275
+ **v6.77.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.76.1
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 = f"openspec-{task_id_num}"
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-sonnet-4-5-20250929}}"
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-sonnet-4-5-20250929}}"
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-sonnet-4-5-20250929}}"
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
- local task_json
3599
- if [[ -n "$next_task_context" ]]; then
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-sonnet-4-5-20250929}}"
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
- # Populate the task queue from OpenSpec task artifacts
8905
- # Only runs once -- skips if queue was already populated from OpenSpec
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
- # Skip if already populated (marker file)
8913
- if [[ -f ".loki/queue/.openspec-populated" ]]; then
8914
- log_info "OpenSpec queue already populated, skipping"
8915
- return 0
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 re-add on restart
8989
- touch ".loki/queue/.openspec-populated"
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
- tool_desc = f"pattern: {tool_input.get('pattern', '')}"
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-sonnet-4-5-20250929}}, tier: $tier_param" >> "$log_file"
9995
- echo "[loki] Aider model: ${AIDER_DEFAULT_MODEL:-${LOKI_AIDER_MODEL:-claude-sonnet-4-5-20250929}}, tier: $tier_param" >> "$agent_log"
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
  ;;
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.76.1"
10
+ __version__ = "6.77.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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-6",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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_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`
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_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
  }
@@ -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:** v6.76.1
5
+ **Version:** v6.77.1
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__ = '6.76.1'
60
+ __version__ = '6.77.1'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.76.1",
3
+ "version": "6.77.1",
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",
@@ -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
- AIDER_DEFAULT_MODEL="${LOKI_AIDER_MODEL:-${LOKI_MODEL_DEVELOPMENT:-claude-sonnet-4-5-20250929}}"
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"
@@ -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 versions automatically.
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
- PROVIDER_CONTEXT_WINDOW=200000 # 200K default; 1M available in extended context beta
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
@@ -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
- CLINE_DEFAULT_MODEL="${LOKI_CLINE_MODEL:-${LOKI_MODEL_DEVELOPMENT:-claude-sonnet-4-5-20250929}}"
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-6-20260201"
61
- PROVIDER_MODEL_DEVELOPMENT="claude-sonnet-4-5-20250929"
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 # Requests per minute
68
- PROVIDER_CONTEXT_WINDOW=200000 # Max context tokens
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
 
@@ -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-5",
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-5",
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}"}]