loki-mode 8.0.0 → 8.0.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/README.md +40 -4
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/proof-generator.py +55 -8
- package/autonomy/lib/proof-verify.py +18 -3
- package/autonomy/lib/workspace_diff.py +17 -0
- package/autonomy/loki +7 -0
- package/autonomy/run.sh +178 -3
- package/dashboard/__init__.py +1 -1
- 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/skills/providers.md +42 -0
package/README.md
CHANGED
|
@@ -239,6 +239,46 @@ 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. Anything
|
|
243
|
+
that speaks the Anthropic Messages API works: OpenRouter, Ollama, LiteLLM, vLLM,
|
|
244
|
+
or your own gateway. Point `ANTHROPIC_BASE_URL` at the endpoint and name the
|
|
245
|
+
model with `LOKI_MODEL_OVERRIDE`:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# OpenRouter (hundreds of models, one key)
|
|
249
|
+
export ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1
|
|
250
|
+
export ANTHROPIC_API_KEY=sk-or-...
|
|
251
|
+
export LOKI_MODEL_OVERRIDE=<model-id-from-openrouter.ai/models>
|
|
252
|
+
loki start prd.md
|
|
253
|
+
|
|
254
|
+
# Ollama, fully local (no API key, no per-token cost)
|
|
255
|
+
export ANTHROPIC_BASE_URL=http://localhost:11434/v1
|
|
256
|
+
export LOKI_MODEL_OVERRIDE=<model you have pulled, e.g. the output of `ollama list`>
|
|
257
|
+
loki start prd.md
|
|
258
|
+
|
|
259
|
+
# LiteLLM / vLLM / any self-hosted gateway
|
|
260
|
+
export ANTHROPIC_BASE_URL=https://your-gateway.internal/v1
|
|
261
|
+
export ANTHROPIC_API_KEY=...
|
|
262
|
+
export LOKI_MODEL_OVERRIDE=<whatever your gateway calls the model>
|
|
263
|
+
loki start prd.md
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Set both variables.** `LOKI_MODEL_OVERRIDE` is what makes the alt-provider
|
|
267
|
+
path work: without it Loki keeps asking for `opus` / `sonnet` / `haiku`, which
|
|
268
|
+
only Anthropic resolves, and most providers reject those names outright. A
|
|
269
|
+
proxy that maps the aliases for you (LiteLLM can) is the one exception.
|
|
270
|
+
|
|
271
|
+
Model IDs are not listed here on purpose -- OpenRouter's catalogue changes every
|
|
272
|
+
week, and a stale ID in a README is a failure you would hit at runtime. Take the
|
|
273
|
+
exact string from your provider's own model list.
|
|
274
|
+
|
|
275
|
+
Both routes honor these variables identically -- the bundled-SDK path and the
|
|
276
|
+
Claude Code CLI path -- and `loki doctor` reports the endpoint it detected plus a
|
|
277
|
+
warning if the model override is missing.
|
|
278
|
+
|
|
279
|
+
The quality gates, the completion council, and the Evidence Receipt do not care
|
|
280
|
+
which model produced the code. They check what was actually built.
|
|
281
|
+
|
|
242
282
|
Either way, run `loki doctor` any time and it tells you exactly what is present and what is missing, with a copy-pasteable install command for each gap.
|
|
243
283
|
|
|
244
284
|
```bash
|
|
@@ -715,7 +755,3 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
|
715
755
|
**[Autonomi](https://www.autonomi.dev/)** | **[Documentation](wiki/Home.md)** | **[Changelog](CHANGELOG.md)** | **[Comparisons](references/competitive-analysis.md)**
|
|
716
756
|
|
|
717
757
|
</div>
|
|
718
|
-
|
|
719
|
-
## Star History
|
|
720
|
-
|
|
721
|
-
[](https://www.star-history.com/?repos=asklokesh%2Floki-mode&type=timeline&legend=bottom-right)
|
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.0.1
|
|
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.0.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.0.
|
|
1
|
+
8.0.1
|
|
@@ -766,12 +766,58 @@ def _diff_sha256(files_changed):
|
|
|
766
766
|
|
|
767
767
|
|
|
768
768
|
def _git_diffstat(target_dir, include_diffs):
|
|
769
|
-
"""Return the final worktree diff relative to
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
769
|
+
"""Return the final worktree diff relative to the RUN's base.
|
|
770
|
+
|
|
771
|
+
The receipt is a run-level document, so its diff stat must span the WHOLE
|
|
772
|
+
run. This previously passed _LOKI_ITER_START_SHA -- the per-ITERATION
|
|
773
|
+
baseline -- so a multi-iteration run attested only to the changes since its
|
|
774
|
+
last iteration. Measured on a real 2-commit run: 850 insertions + 76
|
|
775
|
+
deletions reported where the run actually produced 1300 insertions.
|
|
776
|
+
|
|
777
|
+
That matters more than an ordinary display bug: the returned object flows
|
|
778
|
+
into _diff_sha256, the receipt's integrity hash, which is written on EVERY
|
|
779
|
+
run. A verifier recomputing it therefore attests to the understated stat,
|
|
780
|
+
and the receipt is exactly the artifact users are told to trust. (Detached
|
|
781
|
+
GPG signing is a separate opt-in layer gated on LOKI_PROOF_GPG_KEY and
|
|
782
|
+
default OFF; when enabled it signs these same bytes, but the integrity hash
|
|
783
|
+
is the always-on path.)
|
|
784
|
+
|
|
785
|
+
Order of preference:
|
|
786
|
+
1. _LOKI_RUN_START_SHA -- the run's own baseline (run.sh exports it).
|
|
787
|
+
2. The empty tree -- correct for a GREENFIELD run (a repo with no commits
|
|
788
|
+
at start), where "everything that now exists" IS the run's output and
|
|
789
|
+
there is no earlier commit to diff against.
|
|
790
|
+
3. Empty string -- let workspace_diff apply its own fallbacks.
|
|
791
|
+
"""
|
|
792
|
+
base = os.environ.get("_LOKI_RUN_START_SHA", "").strip()
|
|
793
|
+
if not base:
|
|
794
|
+
# Greenfield: no baseline commit existed when the run started.
|
|
795
|
+
base = _empty_tree_sha(target_dir)
|
|
796
|
+
files_changed, diffs = collect_workspace_diff(target_dir, base, include_diffs)
|
|
797
|
+
# Return the base too. git_facts records base_sha NEXT TO diff and
|
|
798
|
+
# diff_sha256; if they disagree the signed receipt is internally
|
|
799
|
+
# inconsistent and a verifier recomputing the diff from base_sha gets a
|
|
800
|
+
# different answer than the one that was signed.
|
|
801
|
+
return files_changed, diffs, base
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _empty_tree_sha(repo_dir):
|
|
805
|
+
"""The canonical empty-tree object, or "" when git is unavailable.
|
|
806
|
+
|
|
807
|
+
Diffing against this yields "everything that currently exists", which is the
|
|
808
|
+
truthful baseline for a run that started from a repo with no commits.
|
|
809
|
+
"""
|
|
810
|
+
try:
|
|
811
|
+
out = subprocess.run(
|
|
812
|
+
["git", "hash-object", "-t", "tree", os.devnull],
|
|
813
|
+
cwd=repo_dir,
|
|
814
|
+
capture_output=True,
|
|
815
|
+
text=True,
|
|
816
|
+
timeout=10,
|
|
817
|
+
)
|
|
818
|
+
except Exception:
|
|
819
|
+
return ""
|
|
820
|
+
return out.stdout.strip() if out.returncode == 0 else ""
|
|
775
821
|
|
|
776
822
|
|
|
777
823
|
def _collect_iterations(loki_dir):
|
|
@@ -908,7 +954,7 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
908
954
|
provider_name = args.provider or os.environ.get("PROVIDER_NAME") or "claude"
|
|
909
955
|
model = _collect_model(loki_dir, model_from_eff)
|
|
910
956
|
|
|
911
|
-
files_changed, diffs = _git_diffstat(target_dir, args.include_diffs)
|
|
957
|
+
files_changed, diffs, diff_base_sha = _git_diffstat(target_dir, args.include_diffs)
|
|
912
958
|
iterations = _collect_iterations(loki_dir)
|
|
913
959
|
spec = _collect_spec(loki_dir, target_dir)
|
|
914
960
|
council = _collect_council(loki_dir)
|
|
@@ -944,7 +990,8 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
944
990
|
# recompute the hash. True non-forgeability requires the neutral signed record
|
|
945
991
|
# (service-held key). See proof-verify.py verify() docstring.
|
|
946
992
|
git_facts = {
|
|
947
|
-
|
|
993
|
+
# Must be the SAME base the diff above was computed against.
|
|
994
|
+
"base_sha": diff_base_sha,
|
|
948
995
|
"head_sha": _git_head_sha(target_dir),
|
|
949
996
|
"diff": files_changed,
|
|
950
997
|
"diff_sha256": _diff_sha256(files_changed),
|
|
@@ -113,11 +113,26 @@ def _is_git_repo(repo_dir):
|
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
def _rev_resolvable(repo_dir, ref):
|
|
116
|
-
"""True iff `ref`
|
|
116
|
+
"""True iff `ref` names something in repo_dir that a diff can be taken from.
|
|
117
|
+
|
|
118
|
+
Accepts a COMMIT or a TREE. The tree case is load-bearing: a greenfield run
|
|
119
|
+
starts in a repo with no commits, so the generator records the EMPTY TREE
|
|
120
|
+
(4b825dc6...) as its baseline -- "everything that now exists". That object
|
|
121
|
+
is a tree, not a commit, so a commit-only check rejects it and the verifier
|
|
122
|
+
reports "base ref unresolvable" for a perfectly genuine proof.
|
|
123
|
+
|
|
124
|
+
`git diff <empty-tree>` works fine, so the diff really is re-derivable; only
|
|
125
|
+
the resolvability probe was too narrow. This does NOT loosen the forgery
|
|
126
|
+
defense -- an unknown or fabricated SHA still resolves as neither and is
|
|
127
|
+
still rejected.
|
|
128
|
+
"""
|
|
117
129
|
if not ref:
|
|
118
130
|
return False
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
for kind in ("commit", "tree"):
|
|
132
|
+
out = _git(repo_dir, ["rev-parse", "--verify", "--quiet", "%s^{%s}" % (ref, kind)])
|
|
133
|
+
if out and out.strip():
|
|
134
|
+
return True
|
|
135
|
+
return False
|
|
121
136
|
|
|
122
137
|
|
|
123
138
|
def _numstat(repo_dir, base, head):
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import os
|
|
6
7
|
import subprocess
|
|
7
8
|
|
|
8
9
|
|
|
@@ -74,6 +75,22 @@ def collect_workspace_diff(repo_dir, base, include_diffs=False):
|
|
|
74
75
|
comparison = base or "HEAD~1"
|
|
75
76
|
raw = _git(repo_dir, ["diff", "--no-renames", "--numstat", "-z", comparison, "--"])
|
|
76
77
|
if raw is None:
|
|
78
|
+
# HEAD~1 is unusable (single-commit or empty repo). Fall back to the
|
|
79
|
+
# EMPTY TREE, not to a bare "HEAD".
|
|
80
|
+
#
|
|
81
|
+
# A bare "HEAD" compares HEAD to the working tree, so it sees only
|
|
82
|
+
# UNCOMMITTED changes and silently drops everything the run committed.
|
|
83
|
+
# Measured on a real greenfield run: bare HEAD reported 5 files where
|
|
84
|
+
# the truth was 9. The empty tree yields "everything that now exists",
|
|
85
|
+
# which is the correct answer when no earlier commit exists to diff
|
|
86
|
+
# against -- and it agrees with HEAD~1 on runs where both are valid.
|
|
87
|
+
empty_tree = _git(repo_dir, ["hash-object", "-t", "tree", os.devnull])
|
|
88
|
+
if empty_tree:
|
|
89
|
+
comparison = empty_tree.strip()
|
|
90
|
+
raw = _git(repo_dir, ["diff", "--no-renames", "--numstat", "-z", comparison, "--"])
|
|
91
|
+
if raw is None:
|
|
92
|
+
# Last resort: worktree-only. Undercounts a run that committed its work,
|
|
93
|
+
# so it is reached only when even the empty-tree diff failed.
|
|
77
94
|
comparison = "HEAD"
|
|
78
95
|
raw = _git(repo_dir, ["diff", "--no-renames", "--numstat", "-z", comparison, "--"])
|
|
79
96
|
if raw is None:
|
package/autonomy/loki
CHANGED
|
@@ -11025,6 +11025,13 @@ except Exception:
|
|
|
11025
11025
|
fi
|
|
11026
11026
|
else
|
|
11027
11027
|
echo -e " ${YELLOW}WARN${NC} Bun not found -- 'loki cockpit' uses the text summary + browser dashboard (install bun for the inline-image frame)"
|
|
11028
|
+
# Report the protocol line here too. Without it doctor's Cockpit section
|
|
11029
|
+
# is asymmetric: the bun branch prints BOTH "Inline-image protocol" and
|
|
11030
|
+
# "Render path", the no-bun branch printed only "Render path". A user
|
|
11031
|
+
# reading doctor on a machine without bun could not tell whether the
|
|
11032
|
+
# inline-image path was unsupported by their terminal or simply never
|
|
11033
|
+
# probed. Naming the reason keeps the section self-explanatory.
|
|
11034
|
+
echo -e " ${DIM} -- ${NC} Inline-image protocol: not probed (needs bun to run the cockpit renderer)"
|
|
11028
11035
|
echo -e " ${DIM} -- ${NC} Render path: text + browser dashboard"
|
|
11029
11036
|
fi
|
|
11030
11037
|
echo ""
|
package/autonomy/run.sh
CHANGED
|
@@ -2830,6 +2830,30 @@ _loki_check_workspace_writable() {
|
|
|
2830
2830
|
# is missing, when LOKI_SKIP_NET_PREFLIGHT=1, when ANTHROPIC_BASE_URL is set (alt
|
|
2831
2831
|
# provider endpoint we cannot assume), or for any provider whose endpoint we do
|
|
2832
2832
|
# not know. It NEVER blocks the build.
|
|
2833
|
+
# Alt-provider guard: ANTHROPIC_BASE_URL routes Loki at a non-Anthropic endpoint
|
|
2834
|
+
# (OpenRouter, Ollama, LiteLLM, vLLM, self-hosted). The tier aliases Loki resolves
|
|
2835
|
+
# by default -- opus / sonnet / haiku -- are Anthropic-only names, and most
|
|
2836
|
+
# alt-providers reject them outright, so the run dies at the FIRST model call with
|
|
2837
|
+
# a provider-side error that is hard to trace back to the missing override.
|
|
2838
|
+
#
|
|
2839
|
+
# LOKI_MODEL_OVERRIDE is what makes that path work (providers/claude.sh:520 and
|
|
2840
|
+
# loki-ts/src/runner/providers.ts:328 both apply it only when BOTH vars are set).
|
|
2841
|
+
# `loki doctor` already warns on this, but doctor is opt-in and the run is not.
|
|
2842
|
+
#
|
|
2843
|
+
# NON-FATAL by design: a mapping proxy (LiteLLM can do this) legitimately resolves
|
|
2844
|
+
# the aliases server-side, so this is a warning and never a block. Emitted once per
|
|
2845
|
+
# run, and silent unless an alt endpoint is actually configured.
|
|
2846
|
+
_loki_warn_alt_provider_model_alias() {
|
|
2847
|
+
[ -n "${ANTHROPIC_BASE_URL:-}" ] || return 0
|
|
2848
|
+
[ -z "${LOKI_MODEL_OVERRIDE:-}" ] || return 0
|
|
2849
|
+
log_warn "ANTHROPIC_BASE_URL is set (${ANTHROPIC_BASE_URL}) but LOKI_MODEL_OVERRIDE is not."
|
|
2850
|
+
log_warn " Loki will ask for the Anthropic tier aliases (opus/sonnet/haiku), which most"
|
|
2851
|
+
log_warn " alt-providers do not serve. Set the exact model id your endpoint expects:"
|
|
2852
|
+
log_warn " export LOKI_MODEL_OVERRIDE=<model-id> # e.g. from 'ollama list' or your provider's model page"
|
|
2853
|
+
log_warn " Ignore this if your gateway maps those aliases for you (LiteLLM can)."
|
|
2854
|
+
return 0
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2833
2857
|
_loki_check_network_reachable() {
|
|
2834
2858
|
local provider="${1:-claude}"
|
|
2835
2859
|
[ "${LOKI_SKIP_NET_PREFLIGHT:-}" = "1" ] && return 0
|
|
@@ -2874,6 +2898,7 @@ validate_api_keys() {
|
|
|
2874
2898
|
if ! _loki_check_network_reachable "$provider"; then
|
|
2875
2899
|
return 1
|
|
2876
2900
|
fi
|
|
2901
|
+
_loki_warn_alt_provider_model_alias
|
|
2877
2902
|
|
|
2878
2903
|
# Zero-friction auth preflight for LOCAL runs (must run BEFORE the early
|
|
2879
2904
|
# return below, which exits for non-Docker/K8s envs). When claude is the
|
|
@@ -3889,6 +3914,33 @@ notify_rate_limit() {
|
|
|
3889
3914
|
# written even when desktop notifications are disabled. emit_completion_summary
|
|
3890
3915
|
# below is the wrapper that writes the files AND (gated) fires the desktop ping.
|
|
3891
3916
|
#===============================================================================
|
|
3917
|
+
# Read the gate escalation signal into one human line, or print nothing.
|
|
3918
|
+
# Shared by COMPLETION.txt's per-outcome guidance and PAUSED.md so the two never
|
|
3919
|
+
# disagree about why a run stopped. Best-effort: a missing or corrupt signal
|
|
3920
|
+
# yields an empty string, and the caller degrades to generic guidance.
|
|
3921
|
+
_loki_summary_gate_reason() {
|
|
3922
|
+
local loki_dir="${1:-${TARGET_DIR:-.}/.loki}"
|
|
3923
|
+
[ -s "$loki_dir/signals/GATE_ESCALATION.json" ] || return 0
|
|
3924
|
+
_LOKI_GR_FILE="$loki_dir/signals/GATE_ESCALATION.json" python3 -c '
|
|
3925
|
+
import json, os, sys
|
|
3926
|
+
try:
|
|
3927
|
+
d = json.load(open(os.environ["_LOKI_GR_FILE"]))
|
|
3928
|
+
except Exception:
|
|
3929
|
+
sys.exit(0)
|
|
3930
|
+
if not isinstance(d, dict):
|
|
3931
|
+
sys.exit(0)
|
|
3932
|
+
gate = str(d.get("gate", "") or "").strip()
|
|
3933
|
+
if not gate:
|
|
3934
|
+
sys.exit(0)
|
|
3935
|
+
count = d.get("count")
|
|
3936
|
+
thr = d.get("threshold")
|
|
3937
|
+
if isinstance(count, int) and isinstance(thr, int):
|
|
3938
|
+
print("%s (failed %d times, threshold %d)" % (gate, count, thr))
|
|
3939
|
+
else:
|
|
3940
|
+
print(gate)
|
|
3941
|
+
' 2>/dev/null || true
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3892
3944
|
build_completion_summary() {
|
|
3893
3945
|
local outcome="${1:-complete}"
|
|
3894
3946
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
@@ -3944,7 +3996,30 @@ except Exception:
|
|
|
3944
3996
|
fi
|
|
3945
3997
|
review_cmd="git diff ${start_sha}..HEAD -- . ':(exclude).loki/'"
|
|
3946
3998
|
else
|
|
3947
|
-
|
|
3999
|
+
# No baseline SHA. This is the GREENFIELD case: the run started in a repo
|
|
4000
|
+
# with no commits, so `git rev-parse --verify HEAD` correctly produced
|
|
4001
|
+
# nothing (see the start-sha capture). Everything the run built is new.
|
|
4002
|
+
#
|
|
4003
|
+
# Without this branch the counts stayed at zero and a user was told
|
|
4004
|
+
# "files_changed: 0" about a run that had produced real, committed,
|
|
4005
|
+
# working code -- observed on a PRD benchmark that built 4 files and
|
|
4006
|
+
# passed 28/28 of its own tests, yet reported 0. Diff against the empty
|
|
4007
|
+
# tree so the summary reports what was actually created.
|
|
4008
|
+
local _empty_tree
|
|
4009
|
+
_empty_tree="$( (cd "${TARGET_DIR:-.}" && git hash-object -t tree /dev/null) 2>/dev/null || true )"
|
|
4010
|
+
if [ -n "$_empty_tree" ] && (cd "${TARGET_DIR:-.}" && git rev-parse --verify HEAD >/dev/null 2>&1); then
|
|
4011
|
+
diff_stat="$( (cd "${TARGET_DIR:-.}" && git diff --stat "${_empty_tree}..HEAD" "${_summary_pathspec[@]}") 2>/dev/null || true )"
|
|
4012
|
+
local _shortstat_new
|
|
4013
|
+
_shortstat_new="$( (cd "${TARGET_DIR:-.}" && git diff --shortstat "${_empty_tree}..HEAD" "${_summary_pathspec[@]}") 2>/dev/null || true )"
|
|
4014
|
+
if [ -n "$_shortstat_new" ]; then
|
|
4015
|
+
files_changed="$(printf '%s\n' "$_shortstat_new" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' | head -1)"
|
|
4016
|
+
insertions="$(printf '%s\n' "$_shortstat_new" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' | head -1)"
|
|
4017
|
+
deletions="$(printf '%s\n' "$_shortstat_new" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' | head -1)"
|
|
4018
|
+
fi
|
|
4019
|
+
review_cmd="git diff ${_empty_tree}..HEAD -- . ':(exclude).loki/'"
|
|
4020
|
+
else
|
|
4021
|
+
review_cmd="git diff HEAD -- . ':(exclude).loki/'"
|
|
4022
|
+
fi
|
|
3948
4023
|
fi
|
|
3949
4024
|
[ -z "$files_changed" ] && files_changed=0
|
|
3950
4025
|
[ -z "$insertions" ] && insertions=0
|
|
@@ -4073,6 +4148,52 @@ except Exception:
|
|
|
4073
4148
|
esac
|
|
4074
4149
|
;;
|
|
4075
4150
|
esac
|
|
4151
|
+
|
|
4152
|
+
# Per-outcome next step. Deliberately NOT nested inside the
|
|
4153
|
+
# files_changed==0 case above: a run that stops for a gate reason has
|
|
4154
|
+
# usually built plenty, and that is exactly the run whose user most needs
|
|
4155
|
+
# to be told what to do. Keying this on an empty diff would silence it
|
|
4156
|
+
# for the case that motivated it.
|
|
4157
|
+
#
|
|
4158
|
+
# Measured on the PRD benchmark: outcome=intervention with 9 files built
|
|
4159
|
+
# and 28/28 tests passing produced NO guidance at all, because the old
|
|
4160
|
+
# block covered only complete|max_iterations.
|
|
4161
|
+
case "$outcome" in
|
|
4162
|
+
intervention)
|
|
4163
|
+
echo ""
|
|
4164
|
+
echo "Loki stopped and needs a decision from you."
|
|
4165
|
+
_cs_gate="$(_loki_summary_gate_reason "$loki_dir")"
|
|
4166
|
+
if [ -n "$_cs_gate" ]; then
|
|
4167
|
+
echo " Blocked by: $_cs_gate"
|
|
4168
|
+
echo " The findings say exactly what to change. Fix those, then resume:"
|
|
4169
|
+
else
|
|
4170
|
+
echo " See .loki/PAUSED.md for the reason and .loki/CONTINUITY.md for context."
|
|
4171
|
+
echo " Resume with:"
|
|
4172
|
+
fi
|
|
4173
|
+
echo " rm .loki/PAUSE # resume this run"
|
|
4174
|
+
echo " touch .loki/STOP # end it instead"
|
|
4175
|
+
echo " Resuming without addressing the finding will stop at the same gate again."
|
|
4176
|
+
;;
|
|
4177
|
+
failed)
|
|
4178
|
+
echo ""
|
|
4179
|
+
echo "The run failed. What went wrong:"
|
|
4180
|
+
echo " loki why # the classified cause, in one line"
|
|
4181
|
+
echo " .loki/logs/ # the full session log"
|
|
4182
|
+
echo "Anything already committed is listed above and is yours to keep or discard."
|
|
4183
|
+
;;
|
|
4184
|
+
stopped|force_stopped)
|
|
4185
|
+
echo ""
|
|
4186
|
+
echo "The run was stopped before it finished."
|
|
4187
|
+
echo "Work committed up to that point is listed above. To continue from here:"
|
|
4188
|
+
echo " loki start <your spec> # a fresh run over the current tree"
|
|
4189
|
+
;;
|
|
4190
|
+
inconclusive_spec_contradiction)
|
|
4191
|
+
echo ""
|
|
4192
|
+
echo "Loki could not reconcile parts of the spec, so it stopped rather than"
|
|
4193
|
+
echo "guess. The specific contradictions are in .loki/assumptions/ledger.md."
|
|
4194
|
+
echo "Resolve those in the spec, then re-run."
|
|
4195
|
+
;;
|
|
4196
|
+
esac
|
|
4076
4197
|
} > "$loki_dir/COMPLETION.txt" 2>/dev/null || true
|
|
4077
4198
|
|
|
4078
4199
|
# ---- Durable machine-readable file: .loki/state/completion.json -----------
|
|
@@ -20065,7 +20186,17 @@ except Exception:
|
|
|
20065
20186
|
fi
|
|
20066
20187
|
|
|
20067
20188
|
if [ "${ITERATION_COUNT:-0}" -eq 0 ] || [ ! -s "$_start_sha_file" ]; then
|
|
20068
|
-
|
|
20189
|
+
# --verify is load-bearing. In a repo with NO commits yet (every
|
|
20190
|
+
# greenfield build: git init, nothing committed), plain
|
|
20191
|
+
# `git rev-parse HEAD` exits 128 but still prints the literal string
|
|
20192
|
+
# "HEAD" ON STDOUT -- so 2>/dev/null does NOT suppress it and the
|
|
20193
|
+
# unresolved ref name gets captured as the start SHA. Every downstream
|
|
20194
|
+
# diff then becomes `git diff HEAD..HEAD`, which is ALWAYS empty: the
|
|
20195
|
+
# completion council blocks with reason "empty_diff" and can never see
|
|
20196
|
+
# the work, so it can never vote done and the run burns to the cap.
|
|
20197
|
+
# `--verify` resolves-or-fails, emitting nothing on failure, which
|
|
20198
|
+
# leaves the file empty and lets consumers apply their no-baseline path.
|
|
20199
|
+
(cd "${TARGET_DIR:-.}" && git rev-parse --verify HEAD 2>/dev/null) > "$_start_sha_file" 2>/dev/null || true
|
|
20069
20200
|
fi
|
|
20070
20201
|
_LOKI_RUN_START_SHA="$(cat "$_start_sha_file" 2>/dev/null || echo "")"
|
|
20071
20202
|
export _LOKI_RUN_START_SHA
|
|
@@ -20474,7 +20605,11 @@ except Exception as exc:
|
|
|
20474
20605
|
# v7.6.4 B-3a fix: capture iteration-start git SHA so auto_capture_episode
|
|
20475
20606
|
# can diff against this baseline (not just HEAD, which is empty after
|
|
20476
20607
|
# loki's per-iteration auto-commit makes the new files HEAD).
|
|
20477
|
-
|
|
20608
|
+
# --verify: without it, a no-commits-yet repo makes `git rev-parse HEAD`
|
|
20609
|
+
# print the literal "HEAD" on STDOUT while exiting 128, so the
|
|
20610
|
+
# `|| echo ""` fallback never fires and the unresolved ref name is
|
|
20611
|
+
# captured as a baseline. See the start-sha capture above.
|
|
20612
|
+
_LOKI_ITER_START_SHA=$(cd "${TARGET_DIR:-.}" && git rev-parse --verify HEAD 2>/dev/null || echo "")
|
|
20478
20613
|
export _LOKI_ITER_START_SHA
|
|
20479
20614
|
_LOKI_ITER_START_TREE=$(_loki_snapshot_workspace_tree "${TARGET_DIR:-.}" 2>/dev/null || echo "")
|
|
20480
20615
|
export _LOKI_ITER_START_TREE
|
|
@@ -22925,6 +23060,46 @@ Current state is saved. You can inspect:
|
|
|
22925
23060
|
- `.loki/logs/` - Session logs
|
|
22926
23061
|
EOF
|
|
22927
23062
|
|
|
23063
|
+
# Say WHY it paused. The block above is static boilerplate: a user who comes
|
|
23064
|
+
# back to a paused run learns how to resume but not what stopped it, and has
|
|
23065
|
+
# to go spelunking in .loki/signals to find out. Observed on a PRD benchmark
|
|
23066
|
+
# where a gate escalation paused a run that had produced 9 files and 28
|
|
23067
|
+
# passing tests -- the pause was correct, the report told them nothing.
|
|
23068
|
+
# Best-effort and append-only: never let a formatting failure block a pause.
|
|
23069
|
+
{
|
|
23070
|
+
_pause_reason_file="$loki_dir/signals/GATE_ESCALATION"
|
|
23071
|
+
if [ -s "$_pause_reason_file" ]; then
|
|
23072
|
+
printf '\n## Why this paused\n\n'
|
|
23073
|
+
printf 'A quality gate stayed blocked across repeated attempts, so Loki stopped\n'
|
|
23074
|
+
printf 'rather than keep spending on a loop that was not converging.\n\n'
|
|
23075
|
+
_esc_gate="$(python3 -c 'import json,sys
|
|
23076
|
+
try:
|
|
23077
|
+
d = json.load(open(sys.argv[1]))
|
|
23078
|
+
print("%s (failed %s times, threshold %s)" % (d.get("gate","?"), d.get("count","?"), d.get("threshold","?")))
|
|
23079
|
+
except Exception:
|
|
23080
|
+
print("")' "$loki_dir/signals/GATE_ESCALATION.json" 2>/dev/null || true)"
|
|
23081
|
+
[ -n "$_esc_gate" ] && printf -- '- Gate: %s\n' "$_esc_gate"
|
|
23082
|
+
_esc_art="$(python3 -c 'import json,sys
|
|
23083
|
+
try:
|
|
23084
|
+
print(json.load(open(sys.argv[1])).get("latest_artifact","") or "")
|
|
23085
|
+
except Exception:
|
|
23086
|
+
print("")' "$loki_dir/signals/GATE_ESCALATION.json" 2>/dev/null || true)"
|
|
23087
|
+
[ -n "$_esc_art" ] && printf -- '- Findings: %s\n' "$_esc_art"
|
|
23088
|
+
printf '\nRead the findings, fix what they name, then resume. Resuming without a\nchange will most likely stop at the same gate again.\n'
|
|
23089
|
+
fi
|
|
23090
|
+
# What DID get built, so a paused run never reads as "nothing happened".
|
|
23091
|
+
_pause_head="$( (cd "${TARGET_DIR:-.}" && git rev-parse --verify HEAD) 2>/dev/null || true )"
|
|
23092
|
+
if [ -n "$_pause_head" ]; then
|
|
23093
|
+
_pause_base="$( (cd "${TARGET_DIR:-.}" && git hash-object -t tree /dev/null) 2>/dev/null || true )"
|
|
23094
|
+
[ -s "$loki_dir/state/start-sha" ] && _pause_base="$(cat "$loki_dir/state/start-sha" 2>/dev/null)"
|
|
23095
|
+
_pause_stat="$( (cd "${TARGET_DIR:-.}" && git diff --shortstat "${_pause_base}..HEAD" -- . ':(exclude).loki/') 2>/dev/null || true )"
|
|
23096
|
+
if [ -n "$_pause_stat" ]; then
|
|
23097
|
+
printf '\n## What was built before the pause\n\n%s\n' "$_pause_stat"
|
|
23098
|
+
printf '\nReview it with:\n git diff %s..HEAD -- . ":(exclude).loki/"\n' "$_pause_base"
|
|
23099
|
+
fi
|
|
23100
|
+
fi
|
|
23101
|
+
} >> "$loki_dir/PAUSED.md" 2>/dev/null || true
|
|
23102
|
+
|
|
22928
23103
|
# Wait for resume signal (unified: file removal, keyboard, or STOP)
|
|
22929
23104
|
while [ "$PAUSED" = "true" ]; do
|
|
22930
23105
|
# Check for stop signal
|
package/dashboard/__init__.py
CHANGED
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.0.1";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=1CA655940A54189964756E2164756E21
|
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.0.1",
|
|
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.0.1",
|
|
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/skills/providers.md
CHANGED
|
@@ -31,6 +31,48 @@ export LOKI_PROVIDER=claude # or codex, cline, aider
|
|
|
31
31
|
loki start --provider cline ./prd.md
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Any Model, Any Provider (ANTHROPIC_BASE_URL)
|
|
35
|
+
|
|
36
|
+
Independent of the four CLI providers below. Loki speaks the Anthropic Messages
|
|
37
|
+
API, so ANY endpoint that implements it works: OpenRouter, Ollama, LiteLLM,
|
|
38
|
+
vLLM, or a self-hosted gateway. Nothing needs installing.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1 # or http://localhost:11434/v1
|
|
42
|
+
export ANTHROPIC_API_KEY=... # omit for a keyless local Ollama
|
|
43
|
+
export LOKI_MODEL_OVERRIDE=<exact model id from your provider>
|
|
44
|
+
loki start prd.md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**BOTH variables are required.** The override at `providers/claude.sh:520` fires
|
|
48
|
+
only when `ANTHROPIC_BASE_URL` AND `LOKI_MODEL_OVERRIDE` are both set. With only
|
|
49
|
+
the base URL, Loki keeps resolving the tier aliases `opus` / `sonnet` / `haiku`,
|
|
50
|
+
which only Anthropic serves -- most providers reject them and the run dies at the
|
|
51
|
+
first model call. (A proxy that maps the aliases, such as LiteLLM, is the
|
|
52
|
+
exception.) `loki doctor` warns on exactly this condition.
|
|
53
|
+
|
|
54
|
+
Implementation, byte-mirrored across both routes -- edit BOTH or the parity
|
|
55
|
+
fixtures diverge:
|
|
56
|
+
|
|
57
|
+
| Route | Site |
|
|
58
|
+
|---|---|
|
|
59
|
+
| bash | `providers/claude.sh:520`, `autonomy/run.sh:693` |
|
|
60
|
+
| Bun | `loki-ts/src/runner/providers.ts:328` |
|
|
61
|
+
| doctor | `loki-ts/src/commands/doctor.ts:652` |
|
|
62
|
+
|
|
63
|
+
`ANTHROPIC_BASE_URL` is passed through unchanged; Loki never rewrites it.
|
|
64
|
+
|
|
65
|
+
**No model IDs are listed here deliberately.** Alt-provider catalogues change
|
|
66
|
+
weekly, and a stale ID in documentation becomes a runtime failure for the user.
|
|
67
|
+
Read the exact string from the provider (`ollama list`, OpenRouter's model page,
|
|
68
|
+
your gateway's config). For the same reason `LOKI_MODEL_OVERRIDE` is NOT
|
|
69
|
+
validated against an allowlist -- any string the provider accepts is passed
|
|
70
|
+
straight through.
|
|
71
|
+
|
|
72
|
+
Quality gates, the completion council, and the Evidence Receipt are all
|
|
73
|
+
model-agnostic: they assert on the artifact that was built, not on which model
|
|
74
|
+
built it. A cheaper or local model gets the same verification as Opus.
|
|
75
|
+
|
|
34
76
|
## Claude Code (Default, Full Features)
|
|
35
77
|
|
|
36
78
|
**Best for:** All use cases. Full autonomous capability.
|