loki-mode 8.0.3 → 8.2.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/README.md +54 -10
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +45 -4
- package/autonomy/council-v2.sh +114 -16
- package/autonomy/grill.sh +32 -0
- package/autonomy/lib/done-recognition.sh +16 -4
- package/autonomy/lib/fast_verify.py +346 -0
- package/autonomy/lib/no_mock_scan.py +21 -1
- package/autonomy/lib/prd-enrich.sh +12 -1
- package/autonomy/lib/proof-generator.py +173 -6
- package/autonomy/lib/proof-template.html +77 -12
- package/autonomy/lib/proof-verify.py +52 -0
- package/autonomy/loki +246 -7
- package/autonomy/run.sh +14 -0
- package/autonomy/verify.sh +12 -6
- package/dashboard/__init__.py +1 -1
- package/events/emit.sh +105 -18
- package/loki-ts/dist/loki.js +319 -316
- package/mcp/__init__.py +30 -12
- package/mcp/server.py +150 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +72 -0
- package/providers/codex.sh +13 -0
- package/providers/loader.sh +10 -4
- package/providers/model_catalog.json +114 -17
- package/providers/models.sh +26 -3
- package/providers/opencode.sh +145 -0
- package/references/design-archetypes.md +85 -0
package/mcp/__init__.py
CHANGED
|
@@ -25,17 +25,35 @@ import logging as _logging
|
|
|
25
25
|
|
|
26
26
|
_logger = _logging.getLogger('loki-mcp')
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
# LAZY (PEP 562): `from .server import mcp` at package-import time dragged in
|
|
29
|
+
# the whole pip MCP SDK (fastmcp + mcp.types + client.session) -- ~1.4s of the
|
|
30
|
+
# ~1.9s it cost to `import mcp.lsp_proxy`. That is paid by EVERY consumer of
|
|
31
|
+
# the package, including the one-shot `python3 -m mcp.lsp_proxy
|
|
32
|
+
# --write-diagnostics` the lsp_diagnostics quality gate spawns once per
|
|
33
|
+
# iteration, which never starts a server. The autonomous loop spent ~2.7s per
|
|
34
|
+
# iteration on a writer that reported `measured: false`. Resolving `mcp` and
|
|
35
|
+
# `_SERVER_AVAILABLE` through __getattr__ keeps both names working (same
|
|
36
|
+
# values, same SystemExit fallback) while charging the SDK load only to code
|
|
37
|
+
# that actually touches them. Submodule imports (`from mcp import server`,
|
|
38
|
+
# `import mcp.lsp_proxy`) are unaffected -- Python resolves those directly.
|
|
39
|
+
_LAZY_SERVER = ('mcp', '_SERVER_AVAILABLE')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __getattr__(name):
|
|
43
|
+
if name in _LAZY_SERVER:
|
|
44
|
+
try:
|
|
45
|
+
from .server import mcp as _server_mcp
|
|
46
|
+
values = {'mcp': _server_mcp, '_SERVER_AVAILABLE': True}
|
|
47
|
+
except SystemExit:
|
|
48
|
+
values = {'mcp': None, '_SERVER_AVAILABLE': False}
|
|
49
|
+
_logger.warning(
|
|
50
|
+
"MCP server not available: pip 'mcp' SDK not installed. "
|
|
51
|
+
"Install with: pip install mcp"
|
|
52
|
+
)
|
|
53
|
+
globals().update(values)
|
|
54
|
+
return values[name]
|
|
55
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
56
|
+
|
|
39
57
|
|
|
40
58
|
# Import learning collector if available
|
|
41
59
|
try:
|
|
@@ -57,4 +75,4 @@ try:
|
|
|
57
75
|
except ImportError:
|
|
58
76
|
__all__ = ['mcp']
|
|
59
77
|
|
|
60
|
-
__version__ = '8.0
|
|
78
|
+
__version__ = '8.2.0'
|
package/mcp/server.py
CHANGED
|
@@ -2469,6 +2469,156 @@ async def loki_learnings(limit: int = 50) -> str:
|
|
|
2469
2469
|
return json.dumps({"error": str(e)})
|
|
2470
2470
|
|
|
2471
2471
|
|
|
2472
|
+
@mcp.tool()
|
|
2473
|
+
async def loki_graph_query(question: str, budget: int = 1500, path: str = ".") -> str:
|
|
2474
|
+
"""Answer a codebase question from a knowledge graph instead of reading files.
|
|
2475
|
+
|
|
2476
|
+
WHY THIS EXISTS
|
|
2477
|
+
Loading a large repo into context is the dominant token cost of working
|
|
2478
|
+
on it, and on a big codebase it is simply impossible. Measured on this
|
|
2479
|
+
repo: `autonomy/` alone is 85 files / 3,194,940 bytes, roughly 798,735
|
|
2480
|
+
tokens if naively read. No context window holds that.
|
|
2481
|
+
|
|
2482
|
+
MEASURED on this repo, same question, same subtree:
|
|
2483
|
+
naive file load 101,739 bytes ~= 25,434 tokens
|
|
2484
|
+
graph query 3,335 bytes ~= 833 tokens
|
|
2485
|
+
A ~30x reduction, and the answer arrives with exact file:line citations
|
|
2486
|
+
plus a provenance label on every edge (EXTRACTED / INFERRED /
|
|
2487
|
+
AMBIGUOUS) -- the same facts-vs-inference split the Evidence Receipt
|
|
2488
|
+
uses, which is why it composes cleanly with the rest of this server.
|
|
2489
|
+
|
|
2490
|
+
This is the brownfield unlock: a ten-year-old enterprise repo is
|
|
2491
|
+
unreachable by reading, and reachable by querying.
|
|
2492
|
+
|
|
2493
|
+
REQUIRES a graph built by graphify (`graphify <path>`), which is
|
|
2494
|
+
deterministic AST parsing with no LLM and no network. If no graph exists
|
|
2495
|
+
this returns a structured hint rather than silently degrading to a guess.
|
|
2496
|
+
|
|
2497
|
+
Args:
|
|
2498
|
+
question: natural-language question about the codebase
|
|
2499
|
+
budget: cap the answer at roughly this many tokens (default 1500)
|
|
2500
|
+
path: repo root containing graphify-out/ (default: current directory)
|
|
2501
|
+
|
|
2502
|
+
Returns:
|
|
2503
|
+
JSON: {ok, answer, tokens_estimate, budget, source} or {ok:false, hint}
|
|
2504
|
+
"""
|
|
2505
|
+
_emit_tool_event_async('loki_graph_query', 'start',
|
|
2506
|
+
parameters={'question': question, 'budget': budget})
|
|
2507
|
+
try:
|
|
2508
|
+
graph = os.path.join(path or '.', 'graphify-out', 'graph.json')
|
|
2509
|
+
if not os.path.exists(graph):
|
|
2510
|
+
_emit_tool_event_async('loki_graph_query', 'complete',
|
|
2511
|
+
result_status='error')
|
|
2512
|
+
return json.dumps({
|
|
2513
|
+
'ok': False,
|
|
2514
|
+
'hint': ('no knowledge graph found at %s. Build one first: '
|
|
2515
|
+
'`graphify %s` (deterministic AST parse, no LLM, no '
|
|
2516
|
+
'network).' % (graph, path or '.')),
|
|
2517
|
+
}, indent=2)
|
|
2518
|
+
|
|
2519
|
+
proc = subprocess.run(
|
|
2520
|
+
['graphify', 'query', question, '--budget', str(budget)],
|
|
2521
|
+
cwd=path or '.', capture_output=True, text=True, timeout=120)
|
|
2522
|
+
out = (proc.stdout or '').strip()
|
|
2523
|
+
if proc.returncode != 0 or not out:
|
|
2524
|
+
_emit_tool_event_async('loki_graph_query', 'complete',
|
|
2525
|
+
result_status='error')
|
|
2526
|
+
return json.dumps({
|
|
2527
|
+
'ok': False,
|
|
2528
|
+
'hint': (proc.stderr or 'graphify query returned nothing').strip()[:400],
|
|
2529
|
+
}, indent=2)
|
|
2530
|
+
|
|
2531
|
+
_emit_tool_event_async('loki_graph_query', 'complete',
|
|
2532
|
+
result_status='success')
|
|
2533
|
+
return json.dumps({
|
|
2534
|
+
'ok': True,
|
|
2535
|
+
'answer': out,
|
|
2536
|
+
# Deliberately an ESTIMATE, labelled as one: this is bytes/4, not a
|
|
2537
|
+
# tokenizer count. Reporting it as exact would be the kind of
|
|
2538
|
+
# precise-sounding wrong number this project argues against.
|
|
2539
|
+
'tokens_estimate': len(out) // 4,
|
|
2540
|
+
'budget': budget,
|
|
2541
|
+
'source': 'graphify knowledge graph (deterministic AST, no LLM)',
|
|
2542
|
+
}, indent=2)
|
|
2543
|
+
except FileNotFoundError:
|
|
2544
|
+
_emit_tool_event_async('loki_graph_query', 'complete', result_status='error')
|
|
2545
|
+
return json.dumps({'ok': False, 'hint': 'graphify is not installed on PATH'})
|
|
2546
|
+
except Exception as exc:
|
|
2547
|
+
_emit_tool_event_async('loki_graph_query', 'complete',
|
|
2548
|
+
result_status='error', error=str(exc))
|
|
2549
|
+
return json.dumps({'ok': False, 'error': str(exc)})
|
|
2550
|
+
|
|
2551
|
+
|
|
2552
|
+
@mcp.tool()
|
|
2553
|
+
async def loki_verify_fast(path: str = ".", diff_base: str = "") -> str:
|
|
2554
|
+
"""Verify code deterministically in milliseconds. No model call, no network.
|
|
2555
|
+
|
|
2556
|
+
This is the embeddable verification primitive: an IDE, another agent, a CI
|
|
2557
|
+
step, or a third-party tool can call it and get a structured verdict back
|
|
2558
|
+
faster than a keystroke round-trip.
|
|
2559
|
+
|
|
2560
|
+
MEASURED on loki-mode itself (1,932 tracked source files):
|
|
2561
|
+
full repo, cold 298 ms
|
|
2562
|
+
full repo, warm 87 ms
|
|
2563
|
+
diff-scoped 19 ms
|
|
2564
|
+
against an 11,040 ms shell-based baseline. The speedup came from
|
|
2565
|
+
architecture, not micro-optimization: walk the tree ONCE via the git index,
|
|
2566
|
+
run every detector as a pure function in ONE process, and cache findings by
|
|
2567
|
+
file CONTENT hash so an unchanged file is never re-read.
|
|
2568
|
+
|
|
2569
|
+
WHY THERE IS NO LLM HERE, AND WHY THAT IS THE POINT
|
|
2570
|
+
Everything this returns is reproducible by anyone with the same commit.
|
|
2571
|
+
A verdict you can re-derive is a FACT; a verdict a model produced is an
|
|
2572
|
+
OPINION. Keeping this path purely deterministic is what makes it both
|
|
2573
|
+
fast and safe to embed in someone else's product -- they do not have to
|
|
2574
|
+
trust our model choices, only our arithmetic.
|
|
2575
|
+
|
|
2576
|
+
Args:
|
|
2577
|
+
path: repository or directory to verify (default: current directory)
|
|
2578
|
+
diff_base: optional git ref. When given, only files changed against it
|
|
2579
|
+
are verified, which is the normal case for a pull request and the
|
|
2580
|
+
fastest path.
|
|
2581
|
+
|
|
2582
|
+
Returns:
|
|
2583
|
+
JSON: verdict (PASS | FAIL | INCONCLUSIVE), findings[] with
|
|
2584
|
+
rule/path/line/message/severity, files_scanned, files_from_cache,
|
|
2585
|
+
elapsed_ms, and exogenous=true.
|
|
2586
|
+
"""
|
|
2587
|
+
_emit_tool_event_async('loki_verify_fast', 'start',
|
|
2588
|
+
parameters={'path': path, 'diff_base': diff_base})
|
|
2589
|
+
try:
|
|
2590
|
+
import importlib.util as _ilu
|
|
2591
|
+
_fv_path = os.path.join(os.path.dirname(os.path.dirname(
|
|
2592
|
+
os.path.abspath(__file__))), 'autonomy', 'lib', 'fast_verify.py')
|
|
2593
|
+
if not os.path.exists(_fv_path):
|
|
2594
|
+
result = json.dumps({'error': 'fast_verify not found', 'path': _fv_path})
|
|
2595
|
+
_emit_tool_event_async('loki_verify_fast', 'complete',
|
|
2596
|
+
result_status='error')
|
|
2597
|
+
return result
|
|
2598
|
+
_spec = _ilu.spec_from_file_location('loki_fast_verify', _fv_path)
|
|
2599
|
+
_mod = _ilu.module_from_spec(_spec)
|
|
2600
|
+
# Register BEFORE exec: @dataclass resolves its own module via
|
|
2601
|
+
# sys.modules[cls.__module__], and on Python 3.12+ that lookup raises
|
|
2602
|
+
# AttributeError on None if the module is absent. Loading by file path
|
|
2603
|
+
# without this line crashes the tool -- found by actually calling it
|
|
2604
|
+
# through the embedding path rather than importing it normally.
|
|
2605
|
+
sys.modules.setdefault('loki_fast_verify', _mod)
|
|
2606
|
+
_spec.loader.exec_module(_mod)
|
|
2607
|
+
|
|
2608
|
+
res = _mod.verify(path or '.', diff_base or '', True)
|
|
2609
|
+
# dataclasses.asdict keeps this in lockstep with the engine's own shape,
|
|
2610
|
+
# so a field added there reaches every consumer without an edit here.
|
|
2611
|
+
from dataclasses import asdict as _asdict
|
|
2612
|
+
payload = _asdict(res)
|
|
2613
|
+
_emit_tool_event_async('loki_verify_fast', 'complete',
|
|
2614
|
+
result_status='success')
|
|
2615
|
+
return json.dumps(payload, indent=2)
|
|
2616
|
+
except Exception as exc: # never let verification crash the caller
|
|
2617
|
+
_emit_tool_event_async('loki_verify_fast', 'complete',
|
|
2618
|
+
result_status='error', error=str(exc))
|
|
2619
|
+
return json.dumps({'error': str(exc), 'verdict': 'INCONCLUSIVE'})
|
|
2620
|
+
|
|
2621
|
+
|
|
2472
2622
|
@mcp.tool()
|
|
2473
2623
|
async def loki_counter_evidence_template(iteration: int) -> str:
|
|
2474
2624
|
"""Generate a counter-evidence file template for the given iteration.
|
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.0
|
|
4
|
+
"version": "8.2.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.0
|
|
5
|
+
"version": "8.2.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",
|
package/providers/claude.sh
CHANGED
|
@@ -122,6 +122,33 @@ if [ -f "$_loki_claude_flags_helper" ]; then
|
|
|
122
122
|
. "$_loki_claude_flags_helper"
|
|
123
123
|
fi
|
|
124
124
|
|
|
125
|
+
# Absolute path to the curated design archetype library appended to the
|
|
126
|
+
# iteration-1 system prompt by _loki_autonomy_override_text below. Resolved via
|
|
127
|
+
# the same LOKI_SKILL_DIR / PROJECT_DIR precedence run.sh uses (run.sh:1311)
|
|
128
|
+
# rather than BASH_SOURCE: this file is sourced, and under a plain
|
|
129
|
+
# `. providers/claude.sh` BASH_SOURCE[0] can be EMPTY, which silently resolves a
|
|
130
|
+
# dirname-based path one level above the repo root. The two helper paths above
|
|
131
|
+
# still carry that latent defect; they survive it only because each is guarded by
|
|
132
|
+
# a `[ -f ]` that quietly fails open.
|
|
133
|
+
# Resolved FILE-RELATIVE first, matching how the Bun route resolves it from
|
|
134
|
+
# import.meta.url. Deliberately no $PWD anywhere in this chain: at source time
|
|
135
|
+
# $PWD is the TARGET PROJECT's directory during a real build, so a project that
|
|
136
|
+
# happened to contain references/design-archetypes.md would have its own file
|
|
137
|
+
# read straight into the autonomy system prompt (arbitrary user-controlled text,
|
|
138
|
+
# and silent drift from the Bun route, which can never pick up a project file).
|
|
139
|
+
# The env vars are the fallback only for the empty-BASH_SOURCE case.
|
|
140
|
+
_loki_design_archetypes_path=""
|
|
141
|
+
for _loki_dap_root in \
|
|
142
|
+
"$([ -n "${BASH_SOURCE[0]:-}" ] && cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && cd .. 2>/dev/null && pwd)" \
|
|
143
|
+
"${LOKI_SKILL_DIR:-}" \
|
|
144
|
+
"${PROJECT_DIR:-}"; do
|
|
145
|
+
if [ -n "$_loki_dap_root" ] && [ -f "$_loki_dap_root/references/design-archetypes.md" ]; then
|
|
146
|
+
_loki_design_archetypes_path="$_loki_dap_root/references/design-archetypes.md"
|
|
147
|
+
break
|
|
148
|
+
fi
|
|
149
|
+
done
|
|
150
|
+
unset _loki_dap_root
|
|
151
|
+
|
|
125
152
|
# Source the v7.5.22 Phase D mcp-config helper (idempotent).
|
|
126
153
|
# shellcheck source=../autonomy/lib/mcp-config.sh
|
|
127
154
|
_loki_mcp_config_helper="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/autonomy/lib/mcp-config.sh"
|
|
@@ -302,6 +329,15 @@ LOKI_AUTONOMY_EOF
|
|
|
302
329
|
4. DESIGN: commit to ONE named aesthetic direction up front (editorial, brutalist, luxury, retro-futuristic, soft/pastel, industrial, etc. -- chosen from the product domain) and hold it on every surface. Use real content (never lorem). AVOID the AI-slop tells that instantly read as machine-generated: NO indigo/blue-to-purple gradient (the #1 tell), NO Inter/Roboto/system-font headlines (pick a real display+body pairing), NO three-equal-rounded-cards-in-a-row skeleton, NO flat 1px gray card borders or colored left-border strips, NO untouched shadcn defaults, NO reflexive dark mode. Cap the palette at ~3 hues (60/30/10), tinted not pure #fff/#000, separate sections by whitespace then a slight background shift before any border. Aim for Linear/Stripe/Duolingo-tier taste: "this does not look AI-generated".
|
|
303
330
|
Deliver the finished, self-verified, genuinely-designed result in THIS pass. Additional iterations should be the exception, not the plan.
|
|
304
331
|
LOKI_FIRSTPASS_EOF
|
|
332
|
+
|
|
333
|
+
# Positive half of the DESIGN directive. Item 4 above only says what NOT
|
|
334
|
+
# to do, which leaves the model on the priors that ARE the slop. This
|
|
335
|
+
# appends a curated archetype library (real Radix hexes, real OFL fonts)
|
|
336
|
+
# so it has something concrete to commit to. Emitted VERBATIM on both
|
|
337
|
+
# routes -- no interpolation, no selection logic -- so the byte-parity
|
|
338
|
+
# surface stays a plain file compare and the MODEL picks the archetype.
|
|
339
|
+
# Fails open: a missing file degrades to the negative-only directive.
|
|
340
|
+
[ -f "$_loki_design_archetypes_path" ] && cat "$_loki_design_archetypes_path"
|
|
305
341
|
fi
|
|
306
342
|
}
|
|
307
343
|
|
|
@@ -318,6 +354,42 @@ provider_invoke() {
|
|
|
318
354
|
claude --dangerously-skip-permissions "${_LOKI_CLAUDE_AUTO_FLAGS[@]+"${_LOKI_CLAUDE_AUTO_FLAGS[@]}"}" -p "$prompt" "$@"
|
|
319
355
|
}
|
|
320
356
|
|
|
357
|
+
# provider_invoke_argv <tier> <prompt> -- populate _LOKI_INVOKE_ARGV with the
|
|
358
|
+
# exact command line, WITHOUT executing it.
|
|
359
|
+
#
|
|
360
|
+
# WHY THIS EXISTS (the timeout seam)
|
|
361
|
+
# Eight auxiliary judge sites bypass provider_invoke entirely and shell out to
|
|
362
|
+
# `claude ... -p` directly. The reason is documented in the source
|
|
363
|
+
# (done-recognition.sh:49, prd-enrich.sh:40): "`timeout` needs a real command,
|
|
364
|
+
# not a shell function." That is true -- `timeout provider_invoke ...` cannot
|
|
365
|
+
# work, because timeout(1) execs a binary.
|
|
366
|
+
#
|
|
367
|
+
# So the naive fix ("route the judges through provider_invoke") would have to
|
|
368
|
+
# drop the timeout, reintroducing exactly the hang class that left 59 orphaned
|
|
369
|
+
# emit.sh processes alive for 21 hours (v8.1.0). Never trade a hang guard for
|
|
370
|
+
# an abstraction.
|
|
371
|
+
#
|
|
372
|
+
# The seam that satisfies both: a builder that PRINTS argv into an array the
|
|
373
|
+
# caller can hand to `timeout`:
|
|
374
|
+
#
|
|
375
|
+
# provider_invoke_argv development "$prompt"
|
|
376
|
+
# timeout 120 "${_LOKI_INVOKE_ARGV[@]}"
|
|
377
|
+
#
|
|
378
|
+
# Now the judges get provider-agnostic dispatch AND keep their timeout.
|
|
379
|
+
provider_invoke_argv() {
|
|
380
|
+
local tier="${1:-development}"
|
|
381
|
+
local prompt="${2:-}"
|
|
382
|
+
_loki_build_claude_auto_flags "$tier" "${LOKI_COMPLEXITY:-standard}" ""
|
|
383
|
+
local model
|
|
384
|
+
model="$(loki_tier_route_model "$tier" 2>/dev/null || provider_get_tier_param "$tier")"
|
|
385
|
+
_LOKI_INVOKE_ARGV=(
|
|
386
|
+
claude --dangerously-skip-permissions
|
|
387
|
+
"${_LOKI_CLAUDE_AUTO_FLAGS[@]+"${_LOKI_CLAUDE_AUTO_FLAGS[@]}"}"
|
|
388
|
+
)
|
|
389
|
+
[ -n "$model" ] && _LOKI_INVOKE_ARGV+=(--model "$model")
|
|
390
|
+
_LOKI_INVOKE_ARGV+=(-p "$prompt")
|
|
391
|
+
}
|
|
392
|
+
|
|
321
393
|
# Model tier to Task tool model parameter value
|
|
322
394
|
# Respects LOKI_ALLOW_HAIKU flag for tier mapping
|
|
323
395
|
provider_get_tier_param() {
|
package/providers/codex.sh
CHANGED
|
@@ -335,3 +335,16 @@ provider_invoke_with_tier() {
|
|
|
335
335
|
"${extra_flags[@]+"${extra_flags[@]}"}" \
|
|
336
336
|
"$prompt" "$@"
|
|
337
337
|
}
|
|
338
|
+
|
|
339
|
+
# provider_invoke_argv <tier> <prompt> -- see providers/claude.sh for the full
|
|
340
|
+
# rationale. Prints argv into _LOKI_INVOKE_ARGV so a caller can wrap it in
|
|
341
|
+
# `timeout`, which cannot wrap a shell function.
|
|
342
|
+
provider_invoke_argv() {
|
|
343
|
+
local tier="${1:-development}"
|
|
344
|
+
local prompt="${2:-}"
|
|
345
|
+
local model
|
|
346
|
+
model="$(provider_get_tier_param "$tier" 2>/dev/null || printf '%s' "${CODEX_DEFAULT_MODEL:-}")"
|
|
347
|
+
_LOKI_INVOKE_ARGV=(codex exec --sandbox workspace-write)
|
|
348
|
+
[ -n "$model" ] && _LOKI_INVOKE_ARGV+=(--model "$model")
|
|
349
|
+
_LOKI_INVOKE_ARGV+=("$prompt")
|
|
350
|
+
}
|
package/providers/loader.sh
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
PROVIDERS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
6
|
|
|
7
7
|
# List of supported providers
|
|
8
|
-
SUPPORTED_PROVIDERS=("claude" "codex" "cline" "aider")
|
|
8
|
+
SUPPORTED_PROVIDERS=("claude" "codex" "cline" "aider" "opencode")
|
|
9
9
|
|
|
10
10
|
# Default provider
|
|
11
11
|
DEFAULT_PROVIDER="claude"
|
|
@@ -67,16 +67,22 @@ validate_provider_config() {
|
|
|
67
67
|
PROVIDER_NAME
|
|
68
68
|
PROVIDER_DISPLAY_NAME
|
|
69
69
|
PROVIDER_CLI
|
|
70
|
-
PROVIDER_AUTONOMOUS_FLAG
|
|
71
70
|
PROVIDER_PROMPT_POSITIONAL
|
|
72
71
|
PROVIDER_HAS_SUBAGENTS
|
|
73
72
|
PROVIDER_HAS_PARALLEL
|
|
74
73
|
PROVIDER_DEGRADED
|
|
75
74
|
)
|
|
76
75
|
|
|
77
|
-
# Variables that must be defined but can be empty string
|
|
76
|
+
# Variables that must be defined but can be empty string.
|
|
77
|
+
#
|
|
78
|
+
# PROVIDER_AUTONOMOUS_FLAG moved here in v8.2.0: not every CLI needs a flag
|
|
79
|
+
# to run non-interactively. `opencode run <prompt>` is already autonomous, so
|
|
80
|
+
# requiring a non-empty value rejected a perfectly valid provider as
|
|
81
|
+
# "incomplete". The variable must still be DEFINED -- an author who forgets
|
|
82
|
+
# it entirely is still caught -- but an intentional empty value is legal.
|
|
78
83
|
local allow_empty_vars=(
|
|
79
84
|
PROVIDER_PROMPT_FLAG
|
|
85
|
+
PROVIDER_AUTONOMOUS_FLAG
|
|
80
86
|
)
|
|
81
87
|
|
|
82
88
|
for var in "${required_vars[@]}"; do
|
|
@@ -174,7 +180,7 @@ print_capability_matrix() {
|
|
|
174
180
|
# BUG-PROV-007 fix: includes all 4 supported providers in priority order
|
|
175
181
|
# Priority: Claude (Tier 1, full) > Cline (Tier 2, near-full) > Codex/Aider (Tier 3, degraded)
|
|
176
182
|
auto_detect_provider() {
|
|
177
|
-
for p in claude cline codex aider; do
|
|
183
|
+
for p in claude cline codex aider opencode; do
|
|
178
184
|
if check_provider_installed "$p"; then
|
|
179
185
|
echo "$p"
|
|
180
186
|
return 0
|
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
"schema_version": 1,
|
|
4
|
-
"updated": "2026-
|
|
4
|
+
"updated": "2026-07-29",
|
|
5
5
|
"providers": {
|
|
6
6
|
"claude": {
|
|
7
7
|
"latest_planning": "claude-opus-4-8",
|
|
@@ -52,31 +52,128 @@
|
|
|
52
52
|
"latest_development": "gpt-5.3-codex",
|
|
53
53
|
"latest_fast": "gpt-5.3-codex",
|
|
54
54
|
"models": [
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
{
|
|
56
|
+
"id": "gpt-5.3-codex",
|
|
57
|
+
"tier": "planning"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "o3",
|
|
61
|
+
"tier": "planning"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "o4-mini",
|
|
65
|
+
"tier": "fast"
|
|
66
|
+
}
|
|
58
67
|
],
|
|
59
68
|
"notes": "Codex uses a single model with effort level (xhigh/high/low) for tier differentiation"
|
|
60
69
|
},
|
|
61
70
|
"cline": {
|
|
62
|
-
"latest_planning": "
|
|
63
|
-
"latest_development": "
|
|
64
|
-
"latest_fast": "
|
|
71
|
+
"latest_planning": "openrouter/deepseek/deepseek-v3.2",
|
|
72
|
+
"latest_development": "openrouter/deepseek/deepseek-v3.2",
|
|
73
|
+
"latest_fast": "openrouter/deepseek/deepseek-chat",
|
|
65
74
|
"models": [
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
{
|
|
76
|
+
"id": "openrouter/deepseek/deepseek-v3.2",
|
|
77
|
+
"tier": "planning",
|
|
78
|
+
"open_weights": true
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "openrouter/deepseek/deepseek-v3.2",
|
|
82
|
+
"tier": "development",
|
|
83
|
+
"open_weights": true
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "openrouter/deepseek/deepseek-chat",
|
|
87
|
+
"tier": "fast",
|
|
88
|
+
"open_weights": true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "openrouter/z-ai/glm-4.6",
|
|
92
|
+
"tier": "development",
|
|
93
|
+
"open_weights": true
|
|
94
|
+
}
|
|
69
95
|
]
|
|
70
96
|
},
|
|
71
97
|
"aider": {
|
|
72
|
-
"latest_planning": "
|
|
73
|
-
"latest_development": "
|
|
74
|
-
"latest_fast": "
|
|
98
|
+
"latest_planning": "openrouter/deepseek/deepseek-v3.2",
|
|
99
|
+
"latest_development": "openrouter/deepseek/deepseek-v3.2",
|
|
100
|
+
"latest_fast": "openrouter/deepseek/deepseek-chat",
|
|
101
|
+
"models": [
|
|
102
|
+
{
|
|
103
|
+
"id": "openrouter/deepseek/deepseek-v3.2",
|
|
104
|
+
"tier": "planning",
|
|
105
|
+
"open_weights": true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "openrouter/deepseek/deepseek-v3.2",
|
|
109
|
+
"tier": "development",
|
|
110
|
+
"open_weights": true
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "openrouter/deepseek/deepseek-chat",
|
|
114
|
+
"tier": "fast",
|
|
115
|
+
"open_weights": true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "openrouter/minimax/minimax-m2.1",
|
|
119
|
+
"tier": "development",
|
|
120
|
+
"open_weights": true
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "ollama_chat/deepseek-coder",
|
|
124
|
+
"tier": "fast",
|
|
125
|
+
"open_weights": true
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"opencode": {
|
|
130
|
+
"latest_planning": "openrouter/deepseek/deepseek-v3.2",
|
|
131
|
+
"latest_development": "openrouter/deepseek/deepseek-v3.2",
|
|
132
|
+
"latest_fast": "openrouter/deepseek/deepseek-chat",
|
|
75
133
|
"models": [
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
{
|
|
135
|
+
"id": "openrouter/deepseek/deepseek-v3.2",
|
|
136
|
+
"tier": "planning",
|
|
137
|
+
"open_weights": true
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "openrouter/deepseek/deepseek-v3.2",
|
|
141
|
+
"tier": "development",
|
|
142
|
+
"open_weights": true
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "openrouter/deepseek/deepseek-chat",
|
|
146
|
+
"tier": "fast",
|
|
147
|
+
"open_weights": true
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "openrouter/z-ai/glm-4.6",
|
|
151
|
+
"tier": "development",
|
|
152
|
+
"open_weights": true
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "openrouter/minimax/minimax-m2.1",
|
|
156
|
+
"tier": "development",
|
|
157
|
+
"open_weights": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "ollama/qwen2.5-coder",
|
|
161
|
+
"tier": "fast",
|
|
162
|
+
"open_weights": true
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
"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.",
|
|
168
|
+
"latest_planning": "openrouter/deepseek/deepseek-v3.2",
|
|
169
|
+
"latest_development": "openrouter/deepseek/deepseek-v3.2",
|
|
170
|
+
"latest_fast": "openrouter/deepseek/deepseek-chat",
|
|
171
|
+
"models": [
|
|
172
|
+
{
|
|
173
|
+
"id": "openrouter/deepseek/deepseek-v3.2",
|
|
174
|
+
"tier": "development",
|
|
175
|
+
"open_weights": true
|
|
176
|
+
}
|
|
80
177
|
]
|
|
81
178
|
}
|
|
82
179
|
}
|
package/providers/models.sh
CHANGED
|
@@ -25,7 +25,15 @@ loki_latest_model() {
|
|
|
25
25
|
local tier_upper
|
|
26
26
|
tier_upper=$(printf '%s' "$tier" | tr '[:lower:]' '[:upper:]')
|
|
27
27
|
local provider_upper
|
|
28
|
-
|
|
28
|
+
# Uppercase AND normalize to a legal shell identifier. A provider named with
|
|
29
|
+
# a hyphen (e.g. "some-new-vendor") would otherwise build
|
|
30
|
+
# LOKI_SOME-NEW-VENDOR_MODEL_DEVELOPMENT, which is not a valid variable name;
|
|
31
|
+
# the indirect expansion below then fails and takes the whole lookup with it,
|
|
32
|
+
# so the provider silently resolves to nothing. Found when adding the generic
|
|
33
|
+
# registry fallback: "notaprovider" worked and "some-new-vendor" did not.
|
|
34
|
+
provider_upper=$(printf '%s' "$provider" \
|
|
35
|
+
| tr '[:lower:]' '[:upper:]' \
|
|
36
|
+
| tr -c 'A-Z0-9_' '_' )
|
|
29
37
|
|
|
30
38
|
# Env override chain
|
|
31
39
|
local override="LOKI_${provider_upper}_MODEL_${tier_upper}"
|
|
@@ -48,9 +56,24 @@ import json, sys
|
|
|
48
56
|
catalog_path, provider, tier = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
49
57
|
with open(catalog_path) as fh:
|
|
50
58
|
data = json.load(fh)
|
|
51
|
-
|
|
59
|
+
providers = data.get("providers", {})
|
|
60
|
+
p = providers.get(provider)
|
|
52
61
|
if not p:
|
|
53
|
-
|
|
62
|
+
# REGISTRY FALLBACK (v8.2.0). A fixed table of provider keys means every
|
|
63
|
+
# unknown provider is a fallthrough that resolves to NOTHING -- verified
|
|
64
|
+
# before this change: `loki_latest_model notaprovider development` returned
|
|
65
|
+
# empty with rc=1. That makes "bring your own endpoint" a dead end, which is
|
|
66
|
+
# the opposite of model-agnostic.
|
|
67
|
+
#
|
|
68
|
+
# A "generic" key turns the unknown case into a first-class one. It is the
|
|
69
|
+
# same shape codex already uses (all three tiers collapsed onto one model),
|
|
70
|
+
# so this generalizes an accepted pattern rather than inventing a mechanism.
|
|
71
|
+
#
|
|
72
|
+
# The env-override chain above still wins, so an operator naming a specific
|
|
73
|
+
# model per tier is never overridden by this default.
|
|
74
|
+
p = providers.get("generic")
|
|
75
|
+
if not p:
|
|
76
|
+
sys.exit(1)
|
|
54
77
|
model = p.get(f"latest_{tier}")
|
|
55
78
|
if not model:
|
|
56
79
|
sys.exit(1)
|