loki-mode 8.6.0 → 8.8.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/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '8.6.0'
78
+ __version__ = '8.8.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "8.6.0",
4
+ "version": "8.8.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "8.6.0",
5
+ "version": "8.8.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
@@ -58,12 +58,18 @@ _aider_default_from_catalog() {
58
58
  if [ -f "${script_dir}/models.sh" ]; then
59
59
  # shellcheck source=./models.sh
60
60
  source "${script_dir}/models.sh"
61
- loki_latest_model aider development 2>/dev/null || echo "claude-opus-4-8"
61
+ loki_latest_model aider development 2>/dev/null || echo "openrouter/deepseek/deepseek-v3.2"
62
62
  else
63
- echo "claude-opus-4-8"
63
+ echo "openrouter/deepseek/deepseek-v3.2"
64
64
  fi
65
65
  }
66
- AIDER_DEFAULT_MODEL="${LOKI_AIDER_MODEL:-${LOKI_MODEL_DEVELOPMENT:-$(_aider_default_from_catalog)}}"
66
+ # Resolution: provider-scoped env > catalog. The GLOBAL tier var
67
+ # (LOKI_MODEL_DEVELOPMENT) is deliberately NOT in this chain: aider takes
68
+ # litellm full model strings ("openrouter/deepseek/deepseek-v3.2"), while the
69
+ # global tier var carries Claude CLI aliases ("sonnet"), so a global set for
70
+ # one provider silently produced `aider --model sonnet`. Namespace-incompatible,
71
+ # not merely mis-ordered. Do not re-add it.
72
+ AIDER_DEFAULT_MODEL="${LOKI_AIDER_MODEL:-$(_aider_default_from_catalog)}"
67
73
  PROVIDER_MODEL_PLANNING="$AIDER_DEFAULT_MODEL"
68
74
  PROVIDER_MODEL_DEVELOPMENT="$AIDER_DEFAULT_MODEL"
69
75
  PROVIDER_MODEL_FAST="$AIDER_DEFAULT_MODEL"
@@ -134,6 +140,32 @@ resolve_model_for_tier() {
134
140
  echo "$AIDER_DEFAULT_MODEL"
135
141
  }
136
142
 
143
+ # provider_invoke_argv <tier> <prompt> -- see providers/claude.sh for the full
144
+ # rationale. Populates _LOKI_INVOKE_ARGV so a caller can wrap it in `timeout`,
145
+ # which cannot exec a shell function.
146
+ #
147
+ # Unlike Codex, provider_get_tier_param here returns a real MODEL NAME, so it is
148
+ # safe to pass to --model. An empty value omits the flag rather than sending
149
+ # `--model ""`, which aider rejects.
150
+ provider_invoke_argv() {
151
+ local tier="${1:-development}"
152
+ local prompt="${2:-}"
153
+ local model
154
+ model="$(provider_get_tier_param "$tier" 2>/dev/null || printf '%s' "")"
155
+ _LOKI_INVOKE_ARGV=(aider --yes-always --no-auto-commits)
156
+ [ -n "$model" ] && _LOKI_INVOKE_ARGV+=(--model "$model")
157
+ # LOKI_AIDER_FLAGS is a documented operator knob (CLI --aider-flags) that
158
+ # provider_invoke honours. Dropping it here would make the timeout-safe path
159
+ # silently ignore operator config that the normal path applies. Deliberately
160
+ # word-split, matching provider_invoke's unquoted $extra_flags expansion.
161
+ if [ -n "${LOKI_AIDER_FLAGS:-}" ]; then
162
+ # shellcheck disable=SC2206
163
+ _LOKI_INVOKE_ARGV+=(${LOKI_AIDER_FLAGS})
164
+ fi
165
+ # Aider takes the prompt via --message, not positionally.
166
+ _LOKI_INVOKE_ARGV+=(--message "$prompt")
167
+ }
168
+
137
169
  # Tier-aware invocation
138
170
  # Aider uses a single model configured externally, tier has no effect
139
171
  provider_invoke_with_tier() {
@@ -381,7 +381,18 @@ provider_invoke_argv() {
381
381
  local prompt="${2:-}"
382
382
  _loki_build_claude_auto_flags "$tier" "${LOKI_COMPLEXITY:-standard}" ""
383
383
  local model
384
- model="$(loki_tier_route_model "$tier" 2>/dev/null || provider_get_tier_param "$tier")"
384
+ # Use the SAME resolver as the non-argv path. This previously called
385
+ # loki_tier_route_model with one argument, but that function takes TWO
386
+ # (tier, model) and echoes its second arg when routing is off -- so it
387
+ # returned an EMPTY string with rc=0, the `||` fallback never fired, and
388
+ # every argv-based claude invocation shipped with no --model flag at all.
389
+ #
390
+ # Do NOT "fix" this by chaining provider_get_tier_param + loki_tier_route_model
391
+ # here: that reproduces base+route but SKIPS loki_apply_max_tier_clamp, so
392
+ # LOKI_MAX_TIER=sonnet still emitted `--model opus` and the cost ceiling was
393
+ # silently unenforced on exactly the timeout-safe seam. resolve_model_for_tier
394
+ # is base -> route -> clamp -> alt-provider in one call; keep both paths on it.
395
+ model="$(resolve_model_for_tier "$tier" 2>/dev/null || provider_get_tier_param "$tier")"
385
396
  _LOKI_INVOKE_ARGV=(
386
397
  claude --dangerously-skip-permissions
387
398
  "${_LOKI_CLAUDE_AUTO_FLAGS[@]+"${_LOKI_CLAUDE_AUTO_FLAGS[@]}"}"
@@ -59,12 +59,18 @@ _cline_default_from_catalog() {
59
59
  if [ -f "${script_dir}/models.sh" ]; then
60
60
  # shellcheck source=./models.sh
61
61
  source "${script_dir}/models.sh"
62
- loki_latest_model cline development 2>/dev/null || echo "claude-opus-4-8"
62
+ loki_latest_model cline development 2>/dev/null || echo "openrouter/deepseek/deepseek-v3.2"
63
63
  else
64
- echo "claude-opus-4-8"
64
+ echo "openrouter/deepseek/deepseek-v3.2"
65
65
  fi
66
66
  }
67
- CLINE_DEFAULT_MODEL="${LOKI_CLINE_MODEL:-${LOKI_MODEL_DEVELOPMENT:-$(_cline_default_from_catalog)}}"
67
+ # Resolution: provider-scoped env > catalog. The GLOBAL tier var
68
+ # (LOKI_MODEL_DEVELOPMENT) is deliberately NOT in this chain: cline takes
69
+ # full routed model strings ("openrouter/deepseek/deepseek-v3.2"), while the
70
+ # global tier var carries Claude CLI aliases ("sonnet"), so a global set for
71
+ # one provider silently changed cline's model too. Namespace-incompatible,
72
+ # not merely mis-ordered. Do not re-add it.
73
+ CLINE_DEFAULT_MODEL="${LOKI_CLINE_MODEL:-$(_cline_default_from_catalog)}"
68
74
  PROVIDER_MODEL_PLANNING="$CLINE_DEFAULT_MODEL"
69
75
  PROVIDER_MODEL_DEVELOPMENT="$CLINE_DEFAULT_MODEL"
70
76
  PROVIDER_MODEL_FAST="$CLINE_DEFAULT_MODEL"
@@ -132,6 +138,23 @@ resolve_model_for_tier() {
132
138
  echo "$CLINE_DEFAULT_MODEL"
133
139
  }
134
140
 
141
+ # provider_invoke_argv <tier> <prompt> -- see providers/claude.sh for the full
142
+ # rationale. Populates _LOKI_INVOKE_ARGV so a caller can wrap it in `timeout`,
143
+ # which cannot exec a shell function.
144
+ #
145
+ # Unlike Codex, provider_get_tier_param here returns a real MODEL NAME, so it is
146
+ # safe to pass to -m. An empty value omits the flag rather than sending `-m ""`,
147
+ # matching provider_invoke's existing model_args guard.
148
+ provider_invoke_argv() {
149
+ local tier="${1:-development}"
150
+ local prompt="${2:-}"
151
+ local model
152
+ model="$(provider_get_tier_param "$tier" 2>/dev/null || printf '%s' "")"
153
+ _LOKI_INVOKE_ARGV=(cline -y)
154
+ [ -n "$model" ] && _LOKI_INVOKE_ARGV+=(-m "$model")
155
+ _LOKI_INVOKE_ARGV+=("$prompt")
156
+ }
157
+
135
158
  # Tier-aware invocation
136
159
  # Cline uses a single model configured externally, tier has no effect
137
160
  provider_invoke_with_tier() {
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "_comment": "Canonical model catalog. Update this single file when a provider ships a new model. Providers/web-app/docs read from here.",
3
+ "_source_of_truth": "models[] is authoritative. Each entry's `tier` decides what that tier resolves to, first match wins, so order within models[] is load-bearing. The top-level latest_<tier> keys are DERIVED mirrors kept only for existing readers (dashboard/server.py, tools/probe-model-catalog.py, tests); never hand-edit them out of step with models[] -- tests/test-model-catalog-single-source.sh fails when they disagree. A provider whose models[] has no entry for a tier must declare `tier_fallback` saying which tier substitutes; there is no implicit fallback.",
3
4
  "schema_version": 1,
4
5
  "updated": "2026-07-29",
5
6
  "providers": {
6
7
  "claude": {
7
8
  "latest_planning": "claude-opus-4-8",
8
9
  "latest_development": "claude-sonnet-5",
9
- "latest_fast": "claude-sonnet-5",
10
+ "latest_fast": "claude-haiku-4-5",
10
11
  "cli_aliases": {
11
12
  "fable": "claude-fable-5",
12
13
  "opus": "claude-opus-4-8",
@@ -51,6 +52,10 @@
51
52
  "latest_planning": "gpt-5.3-codex",
52
53
  "latest_development": "gpt-5.3-codex",
53
54
  "latest_fast": "gpt-5.3-codex",
55
+ "tier_fallback": {
56
+ "development": "planning",
57
+ "fast": "planning"
58
+ },
54
59
  "models": [
55
60
  {
56
61
  "id": "gpt-5.3-codex",
@@ -58,14 +63,16 @@
58
63
  },
59
64
  {
60
65
  "id": "o3",
61
- "tier": "planning"
66
+ "tier": "planning",
67
+ "notes": "Alternative planning model. NOT the default -- gpt-5.3-codex is listed first and first match wins."
62
68
  },
63
69
  {
64
70
  "id": "o4-mini",
65
- "tier": "fast"
71
+ "tier": "legacy",
72
+ "notes": "Was tier=fast, which made the cheap tier resolve to o4-mini while latest_fast said gpt-5.3-codex. Codex does not differentiate tiers by model, so it is not any tier's default; parked at tier=legacy so it stays discoverable without being dispatched."
66
73
  }
67
74
  ],
68
- "notes": "Codex uses a single model with effort level (xhigh/high/low) for tier differentiation"
75
+ "notes": "Codex uses a single model with effort level (xhigh/high/low) for tier differentiation, so development and fast declare an explicit tier_fallback to planning rather than naming separate models."
69
76
  },
70
77
  "cline": {
71
78
  "latest_planning": "openrouter/deepseek/deepseek-v3.2",
@@ -164,10 +171,14 @@
164
171
  ]
165
172
  },
166
173
  "generic": {
167
- "_comment": "Fallback for ANY provider not named above (bring-your-own endpoint). All three tiers collapse to one open model, the same shape codex uses. Override per tier with LOKI_<PROVIDER>_MODEL_<TIER> or wholesale with LOKI_<PROVIDER>_MODEL.",
174
+ "_comment": "Fallback for ANY provider not named above (bring-your-own endpoint). All three tiers collapse to one open model, the same shape codex uses, declared via tier_fallback. Override per tier with LOKI_<PROVIDER>_MODEL_<TIER> or wholesale with LOKI_<PROVIDER>_MODEL.",
168
175
  "latest_planning": "openrouter/deepseek/deepseek-v3.2",
169
176
  "latest_development": "openrouter/deepseek/deepseek-v3.2",
170
- "latest_fast": "openrouter/deepseek/deepseek-chat",
177
+ "latest_fast": "openrouter/deepseek/deepseek-v3.2",
178
+ "tier_fallback": {
179
+ "planning": "development",
180
+ "fast": "development"
181
+ },
171
182
  "models": [
172
183
  {
173
184
  "id": "openrouter/deepseek/deepseek-v3.2",
@@ -16,12 +16,40 @@
16
16
  _LOKI_MODELS_SH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
17
17
  LOKI_MODEL_CATALOG="${LOKI_MODEL_CATALOG:-$_LOKI_MODELS_SH_DIR/model_catalog.json}"
18
18
 
19
+ # GENERIC CAPABILITY TIERS. Callers should ask for small|medium|high and never
20
+ # name a vendor model. medium is the DEFAULT.
21
+ #
22
+ # small -> the cheap/fast model (claude: haiku, codex: luna)
23
+ # medium -> the workhorse (claude: sonnet, codex: terra) [default]
24
+ # high -> the expensive/best (claude: opus, codex: sol)
25
+ #
26
+ # WHY AN ALIAS LAYER RATHER THAN A RENAME: the catalog already keys models by
27
+ # planning/development/fast and several call sites depend on those names. This
28
+ # maps onto them instead of duplicating the mapping, so there is exactly one
29
+ # place a model id is written down. Hardcoding "medium = claude-sonnet-5"
30
+ # anywhere would recreate the drift class this repo keeps paying for -- a doc
31
+ # that named a default matching neither the code nor the catalog, and a test
32
+ # that demanded open-weights providers resolve to a Claude model.
33
+ loki_tier_alias() {
34
+ case "${1:-medium}" in
35
+ small|fast) printf 'fast' ;;
36
+ high|planning|best) printf 'planning' ;;
37
+ medium|development|'') printf 'development' ;;
38
+ # Unknown value falls back to the DEFAULT rather than failing the run:
39
+ # a typo in a pipeline config should not change which model you pay for
40
+ # silently, and it should not halt a build either. The caller can see
41
+ # what resolved via loki_latest_model.
42
+ *) printf 'development' ;;
43
+ esac
44
+ }
45
+
19
46
  # Return the "latest_<tier>" id for a provider from the catalog.
20
47
  # Args: $1 provider (claude|codex|cline|aider)
21
- # $2 tier (planning|development|fast)
48
+ # $2 tier (planning|development|fast, or small|medium|high)
22
49
  loki_latest_model() {
23
50
  local provider="${1:-claude}"
24
- local tier="${2:-planning}"
51
+ local tier
52
+ tier="$(loki_tier_alias "${2:-planning}")"
25
53
  local tier_upper
26
54
  tier_upper=$(printf '%s' "$tier" | tr '[:lower:]' '[:upper:]')
27
55
  local provider_upper
@@ -74,7 +102,34 @@ if not p:
74
102
  p = providers.get("generic")
75
103
  if not p:
76
104
  sys.exit(1)
77
- model = p.get(f"latest_{tier}")
105
+
106
+ # SINGLE SOURCE OF TRUTH: models[] is authoritative, top-level latest_<tier> is
107
+ # derived. Before this, both existed independently and drifted -- claude
108
+ # latest_fast said claude-sonnet-5 while models[tier=fast] said claude-haiku-4-5,
109
+ # so asking for the cheap tier silently billed the mid tier. models[] wins
110
+ # because it is the richer structure (context_window, max_output, open_weights,
111
+ # alias, notes per model); the top-level keys carry nothing it does not.
112
+ #
113
+ # ORDER IS LOAD-BEARING: a provider may declare several models at one tier
114
+ # (codex lists gpt-5.3-codex and o3 both as planning). First match wins, so the
115
+ # FIRST entry at a tier is the default. Reordering models[] changes what real
116
+ # builds dispatch. tests/test-model-catalog-single-source.sh pins this.
117
+ models = [m for m in p.get("models", []) if isinstance(m, dict)]
118
+ model = next((m.get("id") for m in models if m.get("tier") == tier), None)
119
+
120
+ if not model:
121
+ # No entry at this tier. Falling back must be DECLARED, never accidental:
122
+ # codex genuinely runs one model for every tier and varies reasoning effort
123
+ # instead, so an absent tier is legitimate for it -- but silently walking to
124
+ # some other tier is how you end up dispatching a model nobody chose. The
125
+ # provider states the substitution in the catalog as tier_fallback, and we
126
+ # honor only that.
127
+ fallback_tier = (p.get("tier_fallback") or {}).get(tier)
128
+ if fallback_tier:
129
+ model = next(
130
+ (m.get("id") for m in models if m.get("tier") == fallback_tier), None
131
+ )
132
+
78
133
  if not model:
79
134
  sys.exit(1)
80
135
  print(model)
@@ -67,14 +67,20 @@ PROVIDER_DEGRADED=false
67
67
  PROVIDER_MAX_OUTPUT_TOKENS=128000
68
68
 
69
69
  # Model defaults. opencode names models "provider/model"; the catalog supplies
70
- # the value and the standard LOKI_<PROVIDER>_MODEL_<TIER> chain overrides it.
70
+ # the value and LOKI_OPENCODE_MODEL overrides it.
71
71
  _opencode_default_from_catalog() {
72
72
  if type loki_latest_model >/dev/null 2>&1; then
73
73
  loki_latest_model opencode development 2>/dev/null && return 0
74
74
  fi
75
75
  printf 'openrouter/deepseek/deepseek-v3.2'
76
76
  }
77
- OPENCODE_DEFAULT_MODEL="${LOKI_OPENCODE_MODEL:-${LOKI_MODEL_DEVELOPMENT:-$(_opencode_default_from_catalog)}}"
77
+ # Resolution: provider-scoped env > catalog. The GLOBAL tier var
78
+ # (LOKI_MODEL_DEVELOPMENT) is deliberately NOT in this chain: opencode takes
79
+ # "provider/model" strings ("openrouter/deepseek/deepseek-v3.2"), while the
80
+ # global tier var carries Claude CLI aliases ("sonnet"), so a global set for
81
+ # one provider silently changed opencode's model too. Namespace-incompatible,
82
+ # not merely mis-ordered. Do not re-add it.
83
+ OPENCODE_DEFAULT_MODEL="${LOKI_OPENCODE_MODEL:-$(_opencode_default_from_catalog)}"
78
84
  PROVIDER_MODEL_PLANNING="$OPENCODE_DEFAULT_MODEL"
79
85
  PROVIDER_MODEL_DEVELOPMENT="$OPENCODE_DEFAULT_MODEL"
80
86
  PROVIDER_MODEL_FAST="$OPENCODE_DEFAULT_MODEL"
@@ -71,6 +71,15 @@ PROVIDER_CONTEXT_WINDOW=1000000 # Max context tokens (Opus 4.7: 1M at standard p
71
71
  PROVIDER_MAX_OUTPUT_TOKENS=128000
72
72
  ```
73
73
 
74
+ `PROVIDER_CONTEXT_WINDOW` also caps code-review size. The review diff and prompt
75
+ limits derive from it (assuming ~3 bytes per token and ~75% of the window for
76
+ input), so a small-window local model is not handed a prompt sized for a 200k
77
+ one. The derived value is only ever used to LOWER a cap, and a window at or above
78
+ roughly 188889 tokens clamps to the historical 400000/425000 byte defaults, so no
79
+ provider shipping today changes behavior (a provider that declares no window
80
+ keeps the defaults as well). `LOKI_REVIEW_MAX_DIFF_BYTES` and
81
+ `LOKI_REVIEW_MAX_PROMPT_BYTES` still override both.
82
+
74
83
  #### Degraded Mode
75
84
  ```bash
76
85
  PROVIDER_DEGRADED=false # true for Codex/Aider
@@ -73,6 +73,40 @@ Quality gates, the completion council, and the Evidence Receipt are all
73
73
  model-agnostic: they assert on the artifact that was built, not on which model
74
74
  built it. A cheaper or local model gets the same verification as Opus.
75
75
 
76
+ ### Keeping the model catalog current
77
+
78
+ `providers/model_catalog.json` is hand-maintained and carries an `updated` date.
79
+ Models ship constantly, so the catalog rots. `loki doctor` reports its age in a
80
+ `Model catalog:` section and warns once it passes 90 days:
81
+
82
+ ```
83
+ WARN Last updated 2026-01-02 (210 days ago) -- may be missing newer models
84
+ Refresh: python3 tools/probe-model-catalog.py
85
+ ```
86
+
87
+ The warning is **advisory only**. It never changes an exit code and never fails
88
+ a build (`tests/test-model-catalog-staleness.sh` asserts exactly that). It also
89
+ makes no network call: doctor reads the local file's `updated` field and nothing
90
+ else, so air-gapped operation (`docs/air-gapped.md`) is unaffected.
91
+
92
+ To refresh:
93
+
94
+ ```bash
95
+ python3 tools/probe-model-catalog.py # report new model IDs found in provider docs
96
+ ```
97
+
98
+ The probe reads public provider documentation and reports model IDs that are not
99
+ yet in the catalog. **It never rewrites the catalog.** You edit
100
+ `providers/model_catalog.json` by hand -- bump the relevant `latest_<tier>` and
101
+ add the model to `models[]` -- then set `updated` to today. Model adoption is a
102
+ human decision: cost, capability, and behavioural changes all need a person to
103
+ weigh them, and a model ID that does not exist would break every run that
104
+ selects it. Being stale is recoverable; a fabricated model ID is not.
105
+
106
+ The probe is also wired to a weekly CI job (`.github/workflows/model-catalog-probe.yml`)
107
+ that opens a draft PR with its findings. That job is CI-only -- it is not on any
108
+ runtime path.
109
+
76
110
  ## Claude Code (Default, Full Features)
77
111
 
78
112
  **Best for:** All use cases. Full autonomous capability.
@@ -246,7 +280,7 @@ aider --message "$prompt" --yes-always --no-auto-commits --model model_name
246
280
  **Environment Variables:**
247
281
  | Variable | Description |
248
282
  |----------|-------------|
249
- | `LOKI_AIDER_MODEL` | Model to use (default: claude-3.7-sonnet) |
283
+ | `LOKI_AIDER_MODEL` | Model to use. Default comes from `providers/model_catalog.json` (`aider.latest_development`), not a hardcoded string. The global `LOKI_MODEL_*` tier vars do NOT apply to aider. |
250
284
  | `LOKI_AIDER_FLAGS` | Extra aider flags (e.g., --architect) |
251
285
 
252
286
  ---