loki-mode 8.0.3 → 8.1.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 +22 -9
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/council-v2.sh +52 -16
- package/autonomy/lib/prd-enrich.sh +5 -0
- package/autonomy/lib/proof-generator.py +27 -0
- package/autonomy/lib/proof-template.html +17 -0
- package/autonomy/loki +37 -5
- package/autonomy/run.sh +14 -0
- package/dashboard/__init__.py +1 -1
- package/events/emit.sh +45 -18
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/model_catalog.json +64 -17
package/README.md
CHANGED
|
@@ -239,18 +239,31 @@ This needs Bun on your PATH (the SDK loop runs on the Bun runtime). `loki doctor
|
|
|
239
239
|
|
|
240
240
|
**With a coding-agent CLI.** The classic path, and still the default: Loki drives a separate CLI (Claude Code is the recommended one) plus a couple of common tools on your PATH.
|
|
241
241
|
|
|
242
|
-
**With a different model or provider.** Loki is not tied to Anthropic
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
**With a different model or provider.** Loki is not tied to Anthropic, but
|
|
243
|
+
*how* you reach another model depends on which API the endpoint speaks. There
|
|
244
|
+
are two routes, and picking the wrong one fails confusingly.
|
|
245
|
+
|
|
246
|
+
*Route 1 -- OpenAI-shaped endpoints (OpenRouter, and most hosted open models).*
|
|
247
|
+
Use a provider that speaks that API natively. `aider` and `cline` both do, and
|
|
248
|
+
Loki now defaults them to open-weight models rather than Claude:
|
|
246
249
|
|
|
247
250
|
```bash
|
|
248
|
-
|
|
249
|
-
export
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
loki provider set aider
|
|
252
|
+
export OPENROUTER_API_KEY=sk-or-...
|
|
253
|
+
loki start prd.md # defaults to deepseek-v3.2
|
|
254
|
+
|
|
255
|
+
export LOKI_AIDER_MODEL=openrouter/z-ai/glm-4.6 # or pick your own
|
|
256
|
+
```
|
|
253
257
|
|
|
258
|
+
OpenRouter serves **only** the OpenAI-shaped `/v1/chat/completions`; it has no
|
|
259
|
+
Anthropic `/v1/messages` endpoint. Pointing `ANTHROPIC_BASE_URL` at it does not
|
|
260
|
+
work, which earlier versions of this README incorrectly suggested.
|
|
261
|
+
|
|
262
|
+
*Route 2 -- Anthropic-protocol gateways.* `ANTHROPIC_BASE_URL` routes Claude
|
|
263
|
+
Code itself, so the endpoint must speak the Anthropic Messages API. LiteLLM,
|
|
264
|
+
Bedrock proxies, and self-hosted gateways can:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
254
267
|
# Ollama, fully local (no API key, no per-token cost)
|
|
255
268
|
export ANTHROPIC_BASE_URL=http://localhost:11434/v1
|
|
256
269
|
export LOKI_MODEL_OVERRIDE=<model you have pulled, e.g. the output of `ollama list`>
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v8.0
|
|
6
|
+
# Loki Mode v8.1.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
469
469
|
|
|
470
470
|
---
|
|
471
471
|
|
|
472
|
-
**v8.0
|
|
472
|
+
**v8.1.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.0
|
|
1
|
+
8.1.0
|
package/autonomy/council-v2.sh
CHANGED
|
@@ -82,6 +82,9 @@ council_v2_vote() {
|
|
|
82
82
|
local first=true
|
|
83
83
|
local approve_count=0
|
|
84
84
|
local reject_count=0
|
|
85
|
+
# Reviewers we could not obtain a verdict from. Never folded into
|
|
86
|
+
# reject_count -- see the TRUST comment in the tally below.
|
|
87
|
+
local inconclusive_count=0
|
|
85
88
|
for vote_file in "${vote_files[@]}"; do
|
|
86
89
|
if [ -f "$vote_file" ]; then
|
|
87
90
|
local vote_content
|
|
@@ -95,16 +98,40 @@ council_v2_vote() {
|
|
|
95
98
|
|
|
96
99
|
local verdict
|
|
97
100
|
verdict=$(echo "$vote_content" | python3 -c "import sys,json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "UNKNOWN")
|
|
101
|
+
# TRUST: a reviewer that could not be REACHED did not vote REJECT.
|
|
102
|
+
#
|
|
103
|
+
# Every provider arm below used to substitute a literal
|
|
104
|
+
# {"verdict":"REJECT","reasoning":"review execution failed"} on any
|
|
105
|
+
# miss -- CLI absent, timeout, transient error, unparseable output --
|
|
106
|
+
# and this tally counted "anything not APPROVE" as a rejection. The
|
|
107
|
+
# engine therefore fabricated a reviewer vote the model never gave,
|
|
108
|
+
# and the run was BLOCKED by it.
|
|
109
|
+
#
|
|
110
|
+
# That is the same defect class as a receipt attesting to a diff stat
|
|
111
|
+
# it did not measure: the artifact claims a fact nobody established.
|
|
112
|
+
# On a weak model that formats poorly it is worse still -- low format
|
|
113
|
+
# compliance becomes a permanent BLOCK that reads to the user as
|
|
114
|
+
# "Loki is broken" rather than "your model could not answer".
|
|
115
|
+
#
|
|
116
|
+
# INCONCLUSIVE is counted separately and never as a rejection.
|
|
98
117
|
if [ "$verdict" = "APPROVE" ]; then
|
|
99
118
|
((approve_count++))
|
|
100
|
-
|
|
119
|
+
elif [ "$verdict" = "REJECT" ]; then
|
|
101
120
|
((reject_count++))
|
|
121
|
+
else
|
|
122
|
+
((inconclusive_count++))
|
|
102
123
|
fi
|
|
103
124
|
fi
|
|
104
125
|
done
|
|
105
126
|
votes_json="$votes_json]"
|
|
106
127
|
|
|
107
|
-
|
|
128
|
+
if [ "$inconclusive_count" -gt 0 ]; then
|
|
129
|
+
log_warn "Blind review results: $approve_count APPROVE / $reject_count REJECT / $inconclusive_count INCONCLUSIVE"
|
|
130
|
+
log_warn " $inconclusive_count reviewer(s) produced no verdict (CLI missing, timeout, or unparseable output)."
|
|
131
|
+
log_warn " These are NOT rejections. The council decides on the verdicts it actually obtained."
|
|
132
|
+
else
|
|
133
|
+
log_info "Blind review results: $approve_count APPROVE / $reject_count REJECT"
|
|
134
|
+
fi
|
|
108
135
|
|
|
109
136
|
# Step 4: Sycophancy detection
|
|
110
137
|
local sycophancy_score
|
|
@@ -137,8 +164,13 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
|
|
|
137
164
|
|
|
138
165
|
if [ -f "$da_vote" ]; then
|
|
139
166
|
local da_verdict
|
|
140
|
-
|
|
141
|
-
|
|
167
|
+
# Same trust rule as the main tally: an unparseable devil's
|
|
168
|
+
# advocate did not vote REJECT. Defaulting to REJECT here would
|
|
169
|
+
# silently overturn a unanimous APPROVE on a transient failure.
|
|
170
|
+
da_verdict=$(cat "$da_vote" | python3 -c "import sys,json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "INCONCLUSIVE")
|
|
171
|
+
if [ "$da_verdict" = "INCONCLUSIVE" ]; then
|
|
172
|
+
log_warn "Devil's advocate produced no verdict -- unanimous approval left UNCHANGED (not overturned)"
|
|
173
|
+
elif [ "$da_verdict" = "REJECT" ]; then
|
|
142
174
|
log_warn "Devil's advocate REJECTED unanimous approval"
|
|
143
175
|
approve_count=$((approve_count - 1))
|
|
144
176
|
reject_count=$((reject_count + 1))
|
|
@@ -374,42 +406,42 @@ except Exception:
|
|
|
374
406
|
fi
|
|
375
407
|
# Fall through to the plain text call (+ sed-carve) on any miss.
|
|
376
408
|
if [ -z "$result" ]; then
|
|
377
|
-
result=$(echo "$full_prompt" | CAVEMAN_DEFAULT_MODE=off claude "${_c2_argv[@]}" -p 2>/dev/null || echo '{"verdict":"
|
|
409
|
+
result=$(echo "$full_prompt" | CAVEMAN_DEFAULT_MODE=off claude "${_c2_argv[@]}" -p 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
|
|
378
410
|
fi
|
|
379
411
|
else
|
|
380
|
-
result='{"verdict":"
|
|
412
|
+
result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
|
|
381
413
|
fi
|
|
382
414
|
;;
|
|
383
415
|
codex)
|
|
384
416
|
if command -v codex &>/dev/null; then
|
|
385
|
-
result=$(codex exec -q "$full_prompt" 2>/dev/null || echo '{"verdict":"
|
|
417
|
+
result=$(codex exec -q "$full_prompt" 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
|
|
386
418
|
else
|
|
387
|
-
result='{"verdict":"
|
|
419
|
+
result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
|
|
388
420
|
fi
|
|
389
421
|
;;
|
|
390
422
|
gemini)
|
|
391
423
|
if command -v gemini &>/dev/null; then
|
|
392
|
-
result=$(echo "$full_prompt" | gemini 2>/dev/null || echo '{"verdict":"
|
|
424
|
+
result=$(echo "$full_prompt" | gemini 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
|
|
393
425
|
else
|
|
394
|
-
result='{"verdict":"
|
|
426
|
+
result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
|
|
395
427
|
fi
|
|
396
428
|
;;
|
|
397
429
|
cline)
|
|
398
430
|
if command -v cline &>/dev/null; then
|
|
399
|
-
result=$(cline -y "$full_prompt" 2>/dev/null || echo '{"verdict":"
|
|
431
|
+
result=$(cline -y "$full_prompt" 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
|
|
400
432
|
else
|
|
401
|
-
result='{"verdict":"
|
|
433
|
+
result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
|
|
402
434
|
fi
|
|
403
435
|
;;
|
|
404
436
|
aider)
|
|
405
437
|
if command -v aider &>/dev/null; then
|
|
406
|
-
result=$(aider --message "$full_prompt" --yes-always --no-auto-commits --no-git 2>/dev/null || echo '{"verdict":"
|
|
438
|
+
result=$(aider --message "$full_prompt" --yes-always --no-auto-commits --no-git 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
|
|
407
439
|
else
|
|
408
|
-
result='{"verdict":"
|
|
440
|
+
result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
|
|
409
441
|
fi
|
|
410
442
|
;;
|
|
411
443
|
*)
|
|
412
|
-
result='{"verdict":"
|
|
444
|
+
result='{"verdict":"INCONCLUSIVE","reasoning":"review not supported for this provider (no verdict obtained; NOT a rejection)","issues":[]}'
|
|
413
445
|
;;
|
|
414
446
|
esac
|
|
415
447
|
|
|
@@ -421,7 +453,11 @@ except Exception:
|
|
|
421
453
|
extracted=$(echo "$result" | sed 's/^```json//;s/^```//' | sed -n '/^{/,/^}/p' | head -50)
|
|
422
454
|
fi
|
|
423
455
|
if [ -z "$extracted" ]; then
|
|
424
|
-
|
|
456
|
+
# The weak-model case, and the reason this matters most: a model whose
|
|
457
|
+
# prose could not be carved into JSON did not vote REJECT. Recording one
|
|
458
|
+
# here would turn "your model formats poorly" into "your code was
|
|
459
|
+
# rejected" -- a block the user cannot act on and did not earn.
|
|
460
|
+
extracted='{"verdict":"INCONCLUSIVE","reasoning":"failed to parse review output (no verdict obtained; NOT a rejection)","issues":[]}'
|
|
425
461
|
fi
|
|
426
462
|
|
|
427
463
|
echo "$extracted" > "$output_file"
|
|
@@ -224,6 +224,11 @@ loki_prd_enrich() {
|
|
|
224
224
|
# improve tasks deterministically (content-derived user_story) so even
|
|
225
225
|
# offline users get informative tasks, then return without a model call.
|
|
226
226
|
if ! _loki_prd_enrich_provider_ok; then
|
|
227
|
+
# HONESTY: say so. This used to degrade to the deterministic path with no
|
|
228
|
+
# log line at all, so a user on a non-Claude provider believed their PRD
|
|
229
|
+
# had been model-enriched when it had not. A capability the run did not
|
|
230
|
+
# get must never be indistinguishable from one it did.
|
|
231
|
+
log_warn "PRD enrichment: model-assisted pass SKIPPED (provider='${LOKI_PROVIDER:-claude}'), using the deterministic pass only."
|
|
227
232
|
_loki_prd_enrich_deterministic "$pending_path"
|
|
228
233
|
return 0
|
|
229
234
|
fi
|
|
@@ -429,6 +429,19 @@ def _collect_termination(loki_dir, session_exit_code=None):
|
|
|
429
429
|
"exit_code": None,
|
|
430
430
|
"outcome": "",
|
|
431
431
|
"run_status": "",
|
|
432
|
+
# Which gate stopped the run, if one did. The receipt previously named
|
|
433
|
+
# only a bare outcome ("intervention"), so a user reading the artifact
|
|
434
|
+
# could not tell WHICH gate blocked them or how close it came to its
|
|
435
|
+
# threshold -- the single most actionable fact about a blocked run. The
|
|
436
|
+
# engine already writes it to .loki/signals/GATE_ESCALATION.json and
|
|
437
|
+
# already surfaces it in COMPLETION.txt and PAUSED.md; the signed
|
|
438
|
+
# receipt was the one surface that stayed silent.
|
|
439
|
+
#
|
|
440
|
+
# These are deterministic FACTS read from a file the engine wrote, not
|
|
441
|
+
# an AI assessment, so they belong in the facts block.
|
|
442
|
+
"blocking_gate": "",
|
|
443
|
+
"blocking_gate_failures": None,
|
|
444
|
+
"blocking_gate_threshold": None,
|
|
432
445
|
}
|
|
433
446
|
state_paths = [os.path.join(loki_dir, "autonomy-state.json")]
|
|
434
447
|
sessions_dir = os.path.join(loki_dir, "sessions")
|
|
@@ -455,6 +468,20 @@ def _collect_termination(loki_dir, session_exit_code=None):
|
|
|
455
468
|
out["status"] = outcome
|
|
456
469
|
if outcome not in ("complete", "completed", "success"):
|
|
457
470
|
out["reason"] = outcome
|
|
471
|
+
# Gate escalation: name the gate that stopped the run. Best-effort and
|
|
472
|
+
# non-fatal -- a missing or corrupt signal simply leaves the fields empty,
|
|
473
|
+
# which reads as "no gate escalation recorded", never as a false claim.
|
|
474
|
+
gate = _read_json(
|
|
475
|
+
os.path.join(loki_dir, "signals", "GATE_ESCALATION.json"), default=None
|
|
476
|
+
)
|
|
477
|
+
if isinstance(gate, dict):
|
|
478
|
+
gate_name = str(gate.get("gate") or "").strip()
|
|
479
|
+
if gate_name:
|
|
480
|
+
out["blocking_gate"] = gate_name
|
|
481
|
+
count = gate.get("count")
|
|
482
|
+
thr = gate.get("threshold")
|
|
483
|
+
out["blocking_gate_failures"] = _to_int(count, None)
|
|
484
|
+
out["blocking_gate_threshold"] = _to_int(thr, None)
|
|
458
485
|
if session_exit_code is not None:
|
|
459
486
|
out["exit_code"] = session_exit_code
|
|
460
487
|
if session_exit_code != 0 and not out["reason"]:
|
|
@@ -640,6 +640,23 @@
|
|
|
640
640
|
? " (" + esc(tOutcome) + ")" : "") +
|
|
641
641
|
"</div>";
|
|
642
642
|
}
|
|
643
|
+
// Name the GATE that stopped the run, with how close it came to its
|
|
644
|
+
// threshold. "intervention" tells a reader that something blocked them;
|
|
645
|
+
// "code_review (failed 3 times, threshold 3)" tells them what to fix. The
|
|
646
|
+
// engine already surfaced this in COMPLETION.txt and PAUSED.md -- the
|
|
647
|
+
// signed receipt was the only surface that stayed silent about it.
|
|
648
|
+
// Display-only, and silent when no gate escalation was recorded.
|
|
649
|
+
var tGate = String(g(p, "facts.execution.blocking_gate", "") || "").trim();
|
|
650
|
+
if (tGate) {
|
|
651
|
+
var gFail = g(p, "facts.execution.blocking_gate_failures", null);
|
|
652
|
+
var gThr = g(p, "facts.execution.blocking_gate_threshold", null);
|
|
653
|
+
var detail = "";
|
|
654
|
+
if (typeof gFail === "number" && typeof gThr === "number") {
|
|
655
|
+
detail = " (failed " + esc(String(gFail)) + " times, threshold " +
|
|
656
|
+
esc(String(gThr)) + ")";
|
|
657
|
+
}
|
|
658
|
+
term += '<div class="h-sub">Blocked by: ' + esc(tGate) + detail + "</div>";
|
|
659
|
+
}
|
|
643
660
|
el.className = "honesty " + v.cls;
|
|
644
661
|
el.innerHTML =
|
|
645
662
|
'<span class="h-mark" aria-hidden="true">' + v.mark + "</span>" +
|
package/autonomy/loki
CHANGED
|
@@ -954,10 +954,20 @@ show_help() {
|
|
|
954
954
|
echo " # Providers + model routing"
|
|
955
955
|
echo " loki provider list # Show 4 providers (claude/codex/cline/aider)"
|
|
956
956
|
echo " loki provider set codex # Switch active provider"
|
|
957
|
-
echo " #
|
|
958
|
-
echo "
|
|
959
|
-
echo "
|
|
960
|
-
echo " loki start ./prd.md #
|
|
957
|
+
echo " # Cheap / open models (DeepSeek, GLM, MiniMax, Kimi) via OpenRouter:"
|
|
958
|
+
echo " loki provider set aider # aider + cline reach any OpenRouter model"
|
|
959
|
+
echo " export OPENROUTER_API_KEY=sk-or-..."
|
|
960
|
+
echo " loki start ./prd.md # defaults to deepseek-v3.2 (open weights)"
|
|
961
|
+
echo " # Pick a specific model:"
|
|
962
|
+
echo " export LOKI_AIDER_MODEL=openrouter/z-ai/glm-4.6"
|
|
963
|
+
echo ""
|
|
964
|
+
echo " # Anthropic-PROTOCOL gateways (LiteLLM, Bedrock proxies, self-hosted):"
|
|
965
|
+
echo " export ANTHROPIC_BASE_URL=https://your-gateway/v1 \\\\"
|
|
966
|
+
echo " LOKI_MODEL_OVERRIDE=<model-id-your-gateway-serves>"
|
|
967
|
+
echo " # NOTE: this routes Claude Code itself, so the endpoint must speak the"
|
|
968
|
+
echo " # Anthropic /v1/messages API. OpenRouter does NOT -- it serves"
|
|
969
|
+
echo " # only the OpenAI-shaped /v1/chat/completions. Use the aider or"
|
|
970
|
+
echo " # cline route above for OpenRouter."
|
|
961
971
|
echo ""
|
|
962
972
|
echo " # Cross-project context (Phase F v7.5.23+)"
|
|
963
973
|
echo " # Drop .loki/app.json with {schema_version:1,app_id:myapp,members:[ui,api]}"
|
|
@@ -3871,7 +3881,10 @@ cmd_why() {
|
|
|
3871
3881
|
fi
|
|
3872
3882
|
|
|
3873
3883
|
if [ "$as_json" = "1" ]; then
|
|
3884
|
+
local _why_json_head_sha
|
|
3885
|
+
_why_json_head_sha="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
|
3874
3886
|
_LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
|
|
3887
|
+
_LOKI_WHY_HEAD_SHA="$_why_json_head_sha" \
|
|
3875
3888
|
_LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" python3 - <<'WHYJSON'
|
|
3876
3889
|
import json, os
|
|
3877
3890
|
def load(p):
|
|
@@ -3881,7 +3894,26 @@ def load(p):
|
|
|
3881
3894
|
state = load(os.environ.get("_LOKI_WHY_STATE", ""))
|
|
3882
3895
|
comp = load(os.environ.get("_LOKI_WHY_COMPLETION", ""))
|
|
3883
3896
|
last_error = load(os.environ.get("_LOKI_WHY_LAST_ERROR", ""))
|
|
3884
|
-
|
|
3897
|
+
|
|
3898
|
+
# The human report labels a stale completion record "(from previous completed
|
|
3899
|
+
# run)". The JSON consumer got the same record with NO such marker, so a script
|
|
3900
|
+
# reading `completion.branch` or `completion.pr_url` after a crashed run would
|
|
3901
|
+
# attribute a PREVIOUS run's branch and PR to the current one -- silently, and
|
|
3902
|
+
# with no field to check. Emit the identical determination as explicit fields.
|
|
3903
|
+
# Kept byte-identical in logic to the human path below; the two must not drift.
|
|
3904
|
+
live_statuses = {"running", "exited"}
|
|
3905
|
+
head_sha = os.environ.get("_LOKI_WHY_HEAD_SHA", "")
|
|
3906
|
+
comp_head = comp.get("head_sha", "")
|
|
3907
|
+
comp_is_stale = bool(state.get("status") in live_statuses) or bool(
|
|
3908
|
+
head_sha and comp_head and comp_head != head_sha
|
|
3909
|
+
)
|
|
3910
|
+
print(json.dumps({
|
|
3911
|
+
"state": state,
|
|
3912
|
+
"completion": comp,
|
|
3913
|
+
"last_error": last_error,
|
|
3914
|
+
"completion_is_stale": comp_is_stale,
|
|
3915
|
+
"head_sha": head_sha or None,
|
|
3916
|
+
}, indent=2))
|
|
3885
3917
|
WHYJSON
|
|
3886
3918
|
return 0
|
|
3887
3919
|
fi
|
package/autonomy/run.sh
CHANGED
|
@@ -22823,6 +22823,8 @@ check_human_intervention() {
|
|
|
22823
22823
|
log_warn "PAUSE file created by budget limit - NOT auto-clearing in perpetual mode"
|
|
22824
22824
|
log_warn "Budget limit reached. Remove .loki/signals/BUDGET_EXCEEDED and .loki/PAUSE to continue."
|
|
22825
22825
|
notify_intervention_needed "Budget limit reached - execution paused" 2>/dev/null || true
|
|
22826
|
+
# Same-instant snapshot as COMPLETION.txt (see the PAUSE-file site).
|
|
22827
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22826
22828
|
# Genuinely blocking pause: write the durable intervention record
|
|
22827
22829
|
# now (state-only; the ping above already fired). This is the
|
|
22828
22830
|
# correct site for the durable file because the run actually halts
|
|
@@ -22835,6 +22837,7 @@ check_human_intervention() {
|
|
|
22835
22837
|
if [ "$pause_result" -eq 1 ]; then
|
|
22836
22838
|
# STOP requested DURING the pause: relabel the durable record
|
|
22837
22839
|
# as stopped (state-only; the user typed STOP and is aware).
|
|
22840
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22838
22841
|
build_completion_summary stopped 2>/dev/null || true
|
|
22839
22842
|
return 2
|
|
22840
22843
|
fi
|
|
@@ -22849,6 +22852,13 @@ check_human_intervention() {
|
|
|
22849
22852
|
fi
|
|
22850
22853
|
log_warn "PAUSE file detected - pausing execution"
|
|
22851
22854
|
notify_intervention_needed "Execution paused via PAUSE file"
|
|
22855
|
+
# Refresh STATUS.txt from the queue BEFORE writing the durable record, so
|
|
22856
|
+
# the two files snapshot the same instant. The status monitor refreshes
|
|
22857
|
+
# every 2s but is not running by the time we block here, so STATUS.txt
|
|
22858
|
+
# was frozen at its last tick: measured on a real paused run reporting
|
|
22859
|
+
# "Failed: 0" while COMPLETION.txt -- written now, from the same
|
|
22860
|
+
# queue/failed.json -- reported failed=1. Same source, different age.
|
|
22861
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22852
22862
|
# Genuinely blocking pause: write the durable intervention record now
|
|
22853
22863
|
# (state-only; the ping above already fired).
|
|
22854
22864
|
build_completion_summary intervention 2>/dev/null || true
|
|
@@ -22859,6 +22869,7 @@ check_human_intervention() {
|
|
|
22859
22869
|
if [ "$pause_result" -eq 1 ]; then
|
|
22860
22870
|
# STOP was requested during pause: relabel the durable record as
|
|
22861
22871
|
# stopped (state-only; the user typed STOP and is aware).
|
|
22872
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22862
22873
|
build_completion_summary stopped 2>/dev/null || true
|
|
22863
22874
|
return 2
|
|
22864
22875
|
fi
|
|
@@ -22872,6 +22883,8 @@ check_human_intervention() {
|
|
|
22872
22883
|
rm -f "$loki_dir/PAUSE_AT_CHECKPOINT"
|
|
22873
22884
|
notify_intervention_needed "Execution paused at checkpoint"
|
|
22874
22885
|
touch "$loki_dir/PAUSE"
|
|
22886
|
+
# Same-instant snapshot as COMPLETION.txt (see the PAUSE-file site).
|
|
22887
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22875
22888
|
# Genuinely blocking pause: write the durable intervention record now
|
|
22876
22889
|
# (state-only; the ping above already fired).
|
|
22877
22890
|
build_completion_summary intervention 2>/dev/null || true
|
|
@@ -22881,6 +22894,7 @@ check_human_intervention() {
|
|
|
22881
22894
|
rm -f "$loki_dir/PAUSE"
|
|
22882
22895
|
if [ "$pause_result" -eq 1 ]; then
|
|
22883
22896
|
# STOP requested during pause: relabel as stopped (state-only).
|
|
22897
|
+
(cd "${TARGET_DIR:-.}" && update_status_file) 2>/dev/null || true
|
|
22884
22898
|
build_completion_summary stopped 2>/dev/null || true
|
|
22885
22899
|
return 2
|
|
22886
22900
|
fi
|
package/dashboard/__init__.py
CHANGED
package/events/emit.sh
CHANGED
|
@@ -44,36 +44,63 @@ safe_append_event_jsonl() {
|
|
|
44
44
|
if command -v flock >/dev/null 2>&1; then
|
|
45
45
|
# flock path: bind FD 9 to the sentinel file (created if absent),
|
|
46
46
|
# take an exclusive lock, append, release on subshell exit.
|
|
47
|
+
#
|
|
48
|
+
# `-w 5` is LOAD-BEARING: a bare `flock -x 9` blocks FOREVER. A holder
|
|
49
|
+
# that is killed mid-write (a reaped CI run, a Ctrl-C'd build) leaves
|
|
50
|
+
# every subsequent emit hung, and because emit.sh is spawned per CLI
|
|
51
|
+
# invocation those hangs ACCUMULATE. Measured 2026-07-29: 59 orphaned
|
|
52
|
+
# emit.sh processes, the oldest alive 21 HOURS, on a machine at 0% idle.
|
|
53
|
+
# Observability must never outlive the thing it observes.
|
|
47
54
|
(
|
|
48
|
-
flock -x 9
|
|
55
|
+
flock -w 5 -x 9 || exit 1
|
|
49
56
|
printf '%s\n' "$line" >> "$events_path"
|
|
50
57
|
) 9>"$lock_target"
|
|
51
|
-
|
|
58
|
+
local frc=$?
|
|
59
|
+
if [ "$frc" -ne 0 ]; then
|
|
60
|
+
# Lock unavailable within the timeout: append unlocked rather than
|
|
61
|
+
# block. A rare interleaved line is strictly better than a hung CLI.
|
|
62
|
+
printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
|
|
63
|
+
fi
|
|
64
|
+
return 0
|
|
52
65
|
fi
|
|
53
66
|
|
|
54
|
-
# Fallback: mkdir-based mutex. mkdir is atomic on POSIX.
|
|
67
|
+
# Fallback: mkdir-based mutex. mkdir is atomic on POSIX. macOS ships NO
|
|
68
|
+
# flock(1), so this is the path real users take -- it must be the robust one.
|
|
55
69
|
local lock_dir="${events_path}.lockdir"
|
|
56
70
|
local attempts=0
|
|
57
|
-
local max_attempts=
|
|
71
|
+
local max_attempts=100 # ~1s at 10ms sleep
|
|
72
|
+
local stale_after=30
|
|
58
73
|
while ! mkdir "$lock_dir" 2>/dev/null; do
|
|
74
|
+
# Check staleness EVERY iteration, not only after exhausting attempts.
|
|
75
|
+
# The old code waited for all 500 attempts before its first staleness
|
|
76
|
+
# check, so a stale lock cost ~5s AND ~500 forked sleep helpers per
|
|
77
|
+
# event; under concurrent emits the locks kept going stale and the
|
|
78
|
+
# processes piled up. Measured before this fix: 10s and ~500 forks for
|
|
79
|
+
# a SINGLE append against an already-stale lock.
|
|
80
|
+
local age=0
|
|
81
|
+
local mtime
|
|
82
|
+
mtime=$(stat -f%m "$lock_dir" 2>/dev/null || stat -c%Y "$lock_dir" 2>/dev/null || echo "")
|
|
83
|
+
if [ -n "$mtime" ]; then
|
|
84
|
+
age=$(( $(date +%s) - mtime ))
|
|
85
|
+
else
|
|
86
|
+
# Lock vanished between the failed mkdir and the stat: retry at once.
|
|
87
|
+
continue
|
|
88
|
+
fi
|
|
89
|
+
if [ "$age" -gt "$stale_after" ]; then
|
|
90
|
+
rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null || true
|
|
91
|
+
continue
|
|
92
|
+
fi
|
|
59
93
|
attempts=$((attempts + 1))
|
|
60
94
|
if [ "$attempts" -ge "$max_attempts" ]; then
|
|
61
|
-
#
|
|
62
|
-
local age
|
|
63
|
-
age=$(( $(date +%s) - $(stat -f%m "$lock_dir" 2>/dev/null \
|
|
64
|
-
|| stat -c%Y "$lock_dir" 2>/dev/null \
|
|
65
|
-
|| echo 0) ))
|
|
66
|
-
if [ "$age" -gt 30 ]; then
|
|
67
|
-
rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null || true
|
|
68
|
-
attempts=0
|
|
69
|
-
continue
|
|
70
|
-
fi
|
|
71
|
-
# Give up -- best-effort write so observability never blocks.
|
|
95
|
+
# Give up fast -- best-effort write so observability never blocks.
|
|
72
96
|
printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
|
|
73
|
-
return
|
|
97
|
+
return 0
|
|
74
98
|
fi
|
|
75
|
-
# Sleep ~10ms
|
|
76
|
-
|
|
99
|
+
# Sleep ~10ms WITHOUT forking when the shell supports fractional sleep
|
|
100
|
+
# (bash's `read -t` needs no external process). perl/sleep are fallbacks.
|
|
101
|
+
read -r -t 0.01 _unused_ < /dev/null 2>/dev/null \
|
|
102
|
+
|| perl -e 'select(undef,undef,undef,0.01)' 2>/dev/null \
|
|
103
|
+
|| sleep 1
|
|
77
104
|
done
|
|
78
105
|
# Critical section.
|
|
79
106
|
printf '%s\n' "$line" >> "$events_path"
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var k_=Object.create;var{getPrototypeOf:x_,defineProperty:oK,getOwnPropertyNames:S_}=Object;var y_=Object.prototype.hasOwnProperty;function b_(Z){return this[Z]}var __,f_,h_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?__??=new WeakMap:f_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?k_(x_(Z)):{};let K=X||!Z||!Z.__esModule?oK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of S_(Z))if(!y_.call(K,$))oK(K,$,{get:b_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var VQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var v_=(Z)=>Z;function g_(Z,X){this[Z]=v_.bind(null,X)}var c0=(Z,X)=>{for(var Q in X)oK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:g_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var EO={};c0(EO,{lokiDir:()=>j0,homeLokiDir:()=>I4,findRepoRootForVersion:()=>tK,REPO_ROOT:()=>Q8});import{resolve as s7,dirname as rK}from"path";import{fileURLToPath as m_}from"url";import{existsSync as qQ}from"fs";import{homedir as u_}from"os";function p_(){let Z=DO;for(let X=0;X<6;X++){if(qQ(s7(Z,"VERSION"))&&qQ(s7(Z,"autonomy/run.sh")))return Z;let Q=rK(Z);if(Q===Z)break;Z=Q}return s7(DO,"..","..","..")}function tK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(qQ(s7(X,"VERSION"))&&qQ(s7(X,"autonomy/run.sh")))return X;let Y=rK(X);if(Y===X)break;X=Y}return s7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??s7(process.cwd(),".loki")}function I4(){return s7(u_(),".loki")}var DO,Q8;var G8=p(()=>{DO=rK(m_(import.meta.url));Q8=p_()});import{readFileSync as d_}from"fs";import{resolve as c_,dirname as l_}from"path";import{fileURLToPath as i_}from"url";function _3(){if(f5!==null)return f5;let Z="8.0
|
|
2
|
+
var k_=Object.create;var{getPrototypeOf:x_,defineProperty:oK,getOwnPropertyNames:S_}=Object;var y_=Object.prototype.hasOwnProperty;function b_(Z){return this[Z]}var __,f_,h_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?__??=new WeakMap:f_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?k_(x_(Z)):{};let K=X||!Z||!Z.__esModule?oK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of S_(Z))if(!y_.call(K,$))oK(K,$,{get:b_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var VQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var v_=(Z)=>Z;function g_(Z,X){this[Z]=v_.bind(null,X)}var c0=(Z,X)=>{for(var Q in X)oK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:g_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var EO={};c0(EO,{lokiDir:()=>j0,homeLokiDir:()=>I4,findRepoRootForVersion:()=>tK,REPO_ROOT:()=>Q8});import{resolve as s7,dirname as rK}from"path";import{fileURLToPath as m_}from"url";import{existsSync as qQ}from"fs";import{homedir as u_}from"os";function p_(){let Z=DO;for(let X=0;X<6;X++){if(qQ(s7(Z,"VERSION"))&&qQ(s7(Z,"autonomy/run.sh")))return Z;let Q=rK(Z);if(Q===Z)break;Z=Q}return s7(DO,"..","..","..")}function tK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(qQ(s7(X,"VERSION"))&&qQ(s7(X,"autonomy/run.sh")))return X;let Y=rK(X);if(Y===X)break;X=Y}return s7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??s7(process.cwd(),".loki")}function I4(){return s7(u_(),".loki")}var DO,Q8;var G8=p(()=>{DO=rK(m_(import.meta.url));Q8=p_()});import{readFileSync as d_}from"fs";import{resolve as c_,dirname as l_}from"path";import{fileURLToPath as i_}from"url";function _3(){if(f5!==null)return f5;let Z="8.1.0";if(typeof Z==="string"&&Z.length>0)return f5=Z,f5;try{let X=l_(i_(import.meta.url)),Q=tK(X);f5=d_(c_(Q,"VERSION"),"utf-8").trim()}catch{f5="unknown"}return f5}var f5=null;var GQ=p(()=>{G8()});var kO={};c0(kO,{runOrThrow:()=>Wf,run:()=>D0,readStreamCapped:()=>HQ,commandVersion:()=>qf,commandExists:()=>H9,ShellError:()=>eK,MAX_STDOUT_BYTES:()=>RO});async function HQ(Z,X=RO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let W=$.byteLength-(z-X);J+=Y.decode($.subarray(0,W),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function D0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([HQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Wf(Z,X={}){let Q=await D0(Z,X);if(Q.exitCode!==0)throw new eK(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function H9(Z){let X=Vf(Z),Q=await D0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Vf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function qf(Z,X="--version"){if(!await H9(Z))return null;let Y=await D0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var RO=16777216,eK;var k9=p(()=>{eK=class eK extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function n7(Z){return Gf?"":Z}var Gf,M0,k8,l0,gW0,a0,H8,X9,g;var S6=p(()=>{Gf=(process.env.NO_COLOR??"").length>0;M0=n7("\x1B[0;31m"),k8=n7("\x1B[0;32m"),l0=n7("\x1B[1;33m"),gW0=n7("\x1B[0;34m"),a0=n7("\x1B[0;36m"),H8=n7("\x1B[1m"),X9=n7("\x1B[2m"),g=n7("\x1B[0m")});import{existsSync as wf}from"fs";async function F7(){if(R4!==void 0)return R4;let Z="/opt/homebrew/bin/python3.12";if(wf(Z))return R4=Z,Z;let X=await H9("python3.12");if(X)return R4=X,X;let Q=await H9("python3");return R4=Q,Q}async function D7(Z,X={}){let Q=await F7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return D0([Q,"-c",Z],X)}var R4;var o7=p(()=>{k9()});var oO={};c0(oO,{runStatus:()=>of});import{existsSync as Q9,readFileSync as h3,readdirSync as pO,statSync as dO}from"fs";import{resolve as h8,basename as mf}from"path";import{homedir as uf}from"os";function cO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function lO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*BQ/X);if(J>BQ)J=BQ;let z=BQ-J,K=k8;if(Y>=80)K=M0;else if(Y>=50)K=l0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=cO(Z),V=cO(X);return` ${H8}${Q}${g} ${K}[${$}]${g} ${Y}% (${W} / ${V})`}async function df(){if(await H9("jq"))return!0;return process.stdout.write(`${M0}Error: jq is required but not installed.${g}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -1203,4 +1203,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1203
1203
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (I_(),E_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1204
1204
|
`),process.stderr.write(P_),2}}mO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var TW0=await MW0(Bun.argv.slice(2));process.exit(TW0);
|
|
1205
1205
|
|
|
1206
|
-
//# debugId=
|
|
1206
|
+
//# debugId=59A2C45B0211620364756E2164756E21
|
package/mcp/__init__.py
CHANGED
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.1.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.1.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",
|
|
@@ -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,78 @@
|
|
|
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",
|
|
75
101
|
"models": [
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
127
|
]
|
|
81
128
|
}
|
|
82
129
|
}
|