loki-mode 8.10.0 → 8.12.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/agents/hub_install.py +12 -0
- package/autonomy/run.sh +144 -19
- 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/00-index.md +16 -0
- package/skills/extending.md +131 -0
- package/skills/model-selection.md +25 -10
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.
|
|
6
|
+
# Loki Mode v8.12.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.
|
|
472
|
+
**v8.12.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
8.
|
|
1
|
+
8.12.0
|
package/agents/hub_install.py
CHANGED
|
@@ -31,6 +31,18 @@ SECURITY MODEL
|
|
|
31
31
|
ignored, never run, and their presence is reported)
|
|
32
32
|
- manifests that are not the declared kind
|
|
33
33
|
|
|
34
|
+
WHAT AN INSTALLED AGENT DOES
|
|
35
|
+
An installed agent is not inert metadata. Beyond `loki agent
|
|
36
|
+
list/info/run`, it joins the code-review reviewer pool: its `focus`
|
|
37
|
+
entries act as trigger keywords scored against the diff, and on a match
|
|
38
|
+
it is dispatched as a real reviewer whose findings feed the same
|
|
39
|
+
Critical/High = BLOCK mechanism as a built-in. Selection is strictly
|
|
40
|
+
ADDITIVE -- installed agents are appended after the built-in battery and
|
|
41
|
+
can never displace a built-in reviewer, and at most 2 fire per review.
|
|
42
|
+
Consistent with the security model above, only manifest TEXT (persona /
|
|
43
|
+
focus / capabilities) reaches the reviewer prompt; nothing is executed.
|
|
44
|
+
See skills/quality-gates.md "Adding Your Own Reviewer".
|
|
45
|
+
|
|
34
46
|
STORE LAYOUT (project-local, under .loki/)
|
|
35
47
|
.loki/agents/installed.json list of installed agent manifests
|
|
36
48
|
.loki/templates/<name>.md installed template body
|
package/autonomy/run.sh
CHANGED
|
@@ -657,16 +657,29 @@ loki_detect_scoped_change() {
|
|
|
657
657
|
|
|
658
658
|
# Greenfield is not a scoped change: no repo, or a repo with almost no
|
|
659
659
|
# history, means we are building something new.
|
|
660
|
+
#
|
|
661
|
+
# Ask git whether this is a work tree rather than testing for a .git
|
|
662
|
+
# DIRECTORY: in a git worktree (and in a submodule) .git is a FILE, so the
|
|
663
|
+
# old -d test rejected every worktree-based run -- including the parallel
|
|
664
|
+
# workflow streams this project runs by default. rev-parse is true for a
|
|
665
|
+
# plain clone, a worktree, and a submodule alike, and replaces two
|
|
666
|
+
# subprocesses' worth of checking with one.
|
|
660
667
|
local target="${TARGET_DIR:-.}"
|
|
661
|
-
|
|
668
|
+
git -C "$target" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
|
|
662
669
|
local commits
|
|
663
670
|
commits="$(git -C "$target" rev-list --count HEAD 2>/dev/null || echo 0)"
|
|
664
671
|
[ "${commits:-0}" -ge 5 ] || return 1
|
|
665
672
|
|
|
666
673
|
# An issue-sourced spec is the canonical scoped change: someone filed a
|
|
667
674
|
# discrete request against code that already exists.
|
|
675
|
+
#
|
|
676
|
+
# The spec path is passed in by the caller ($1). It used to be read only
|
|
677
|
+
# from LOKI_PRD_FILE / LOKI_ISSUE_REF, but `loki start <issue>` writes
|
|
678
|
+
# .loki/prd-issue-N.md and hands run.sh that path as a POSITIONAL argument,
|
|
679
|
+
# so neither variable was ever set and this check could not fire.
|
|
680
|
+
local spec="${1:-${LOKI_PRD_FILE:-}}"
|
|
668
681
|
[ -n "${LOKI_ISSUE_REF:-}" ] && return 0
|
|
669
|
-
case "$
|
|
682
|
+
case "$spec" in
|
|
670
683
|
*prd-issue-*) return 0 ;;
|
|
671
684
|
esac
|
|
672
685
|
|
|
@@ -674,7 +687,7 @@ loki_detect_scoped_change() {
|
|
|
674
687
|
}
|
|
675
688
|
|
|
676
689
|
loki_apply_scoped_change_profile() {
|
|
677
|
-
loki_detect_scoped_change || return 0
|
|
690
|
+
loki_detect_scoped_change "${1:-}" || return 0
|
|
678
691
|
|
|
679
692
|
# Off: cannot affect the correctness of a scoped change to existing code.
|
|
680
693
|
: "${LOKI_PHASE_WEB_RESEARCH:=false}"
|
|
@@ -10597,17 +10610,38 @@ print("REPEATED_GATE_BLOCKER (PRIORITY): action=escalate gate=%s count=%d thresh
|
|
|
10597
10610
|
# Usage: _loki_run_pytest_with_timeout <target_dir> [pytest_args...]
|
|
10598
10611
|
# Stdout: combined pytest output
|
|
10599
10612
|
# Exit: 0 on pass, non-zero on fail. Exit 124 indicates the timeout fired.
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10613
|
+
# Portable timeout-prefix probe, shared by every gate that must be wall-clock
|
|
10614
|
+
# bounded. Stock macOS ships NEITHER `timeout` NOR `gtimeout` (gtimeout arrives
|
|
10615
|
+
# with coreutils), so a bare `timeout` would resolve to "command not found"
|
|
10616
|
+
# (exit 127) and flip test_passed=false on every macOS run -- turning a rare
|
|
10617
|
+
# hang into a universal false RED. When no timeout binary exists we emit an
|
|
10618
|
+
# EMPTY prefix and run unbounded, which is the pre-existing behaviour.
|
|
10619
|
+
#
|
|
10620
|
+
# Usage: _loki_timeout_prefix <seconds> <gate-label> (writes words to stdout)
|
|
10621
|
+
# local _cmd=(); read -r -a _cmd <<< "$(_loki_timeout_prefix 300 'go test')"
|
|
10622
|
+
# "${_cmd[@]}" go test ./...
|
|
10623
|
+
_loki_timeout_prefix() {
|
|
10624
|
+
local secs="$1" label="${2:-gate}"
|
|
10604
10625
|
if command -v gtimeout >/dev/null 2>&1; then
|
|
10605
|
-
|
|
10626
|
+
printf 'gtimeout %ss' "$secs"
|
|
10606
10627
|
elif command -v timeout >/dev/null 2>&1; then
|
|
10607
|
-
|
|
10628
|
+
printf 'timeout %ss' "$secs"
|
|
10608
10629
|
else
|
|
10609
|
-
|
|
10630
|
+
# >&2 is LOAD-BEARING: this function's STDOUT becomes the command-prefix
|
|
10631
|
+
# array at every call site. log_warn writes to stdout (run.sh:1684), so
|
|
10632
|
+
# without this redirect the warning text itself would be executed as the
|
|
10633
|
+
# command -> exit 127 -> test_passed=false on every box lacking a timeout
|
|
10634
|
+
# binary (stock macOS). That would be a universal false RED, strictly
|
|
10635
|
+
# worse than the unbounded hang this helper exists to prevent.
|
|
10636
|
+
log_warn "Neither gtimeout nor timeout available; ${label} will run unbounded (install coreutils on macOS)" >&2
|
|
10610
10637
|
fi
|
|
10638
|
+
}
|
|
10639
|
+
|
|
10640
|
+
_loki_run_pytest_with_timeout() {
|
|
10641
|
+
local target_dir="$1"; shift
|
|
10642
|
+
local pytest_timeout="${LOKI_PYTEST_TIMEOUT:-${LOKI_GATE_TIMEOUT:-300}}"
|
|
10643
|
+
local _to_cmd=()
|
|
10644
|
+
read -r -a _to_cmd <<< "$(_loki_timeout_prefix "$pytest_timeout" 'pytest gate')"
|
|
10611
10645
|
(cd "$target_dir" && "${_to_cmd[@]}" pytest "$@" 2>&1)
|
|
10612
10646
|
}
|
|
10613
10647
|
|
|
@@ -11081,9 +11115,8 @@ sys.stdout.write(t.strip())
|
|
|
11081
11115
|
test_runner="unittest"
|
|
11082
11116
|
local output unittest_exit _ut_to
|
|
11083
11117
|
_ut_to="${LOKI_PYTEST_TIMEOUT:-${LOKI_GATE_TIMEOUT:-300}}"
|
|
11084
|
-
local _ut_cmd=(
|
|
11085
|
-
|
|
11086
|
-
command -v timeout &>/dev/null || command -v gtimeout &>/dev/null || _ut_cmd=()
|
|
11118
|
+
local _ut_cmd=()
|
|
11119
|
+
read -r -a _ut_cmd <<< "$(_loki_timeout_prefix "$_ut_to" 'unittest gate')"
|
|
11087
11120
|
output=$(cd "${TARGET_DIR:-.}" && "${_ut_cmd[@]}" python3 -m unittest discover -p 'test_*.py' 2>&1)
|
|
11088
11121
|
unittest_exit=$?
|
|
11089
11122
|
if [ "$unittest_exit" -eq 124 ]; then
|
|
@@ -11098,19 +11131,46 @@ sys.stdout.write(t.strip())
|
|
|
11098
11131
|
fi
|
|
11099
11132
|
|
|
11100
11133
|
# Go
|
|
11134
|
+
# Wall-clock bounded like every other runner above. `go test` self-imposes
|
|
11135
|
+
# -timeout 10m PER TEST BINARY, but `./...` runs one binary per package, so
|
|
11136
|
+
# the AGGREGATE is unbounded -- and a test blocked in a cgo call or a syscall
|
|
11137
|
+
# can outlive that panic. `cargo test` has no default timeout at all. Without
|
|
11138
|
+
# this the gate hangs the whole iteration with no verdict.
|
|
11139
|
+
# $gate_timeout is NOT in scope here (it is declared inside the package.json
|
|
11140
|
+
# block), so read LOKI_GATE_TIMEOUT directly.
|
|
11101
11141
|
if [ "$test_runner" = "none" ] && [ -f "${TARGET_DIR:-.}/go.mod" ] && command -v go &>/dev/null; then
|
|
11102
11142
|
test_runner="go-test"
|
|
11103
|
-
local output
|
|
11104
|
-
|
|
11105
|
-
|
|
11143
|
+
local output go_exit _go_to _go_cmd=()
|
|
11144
|
+
_go_to="${LOKI_GATE_TIMEOUT:-300}"
|
|
11145
|
+
read -r -a _go_cmd <<< "$(_loki_timeout_prefix "$_go_to" 'go test gate')"
|
|
11146
|
+
output=$(cd "${TARGET_DIR:-.}" && "${_go_cmd[@]}" go test ./... 2>&1)
|
|
11147
|
+
go_exit=$?
|
|
11148
|
+
if [ "$go_exit" -eq 124 ]; then
|
|
11149
|
+
test_passed=false
|
|
11150
|
+
log_warn "go test gate timed out after ${_go_to}s (exit 124)"
|
|
11151
|
+
details="go test: TIMED OUT after ${_go_to}s -- $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
11152
|
+
else
|
|
11153
|
+
[ "$go_exit" -ne 0 ] && test_passed=false
|
|
11154
|
+
details="go test: $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
11155
|
+
fi
|
|
11106
11156
|
fi
|
|
11107
11157
|
|
|
11108
11158
|
# Rust
|
|
11109
11159
|
if [ "$test_runner" = "none" ] && [ -f "${TARGET_DIR:-.}/Cargo.toml" ] && command -v cargo &>/dev/null; then
|
|
11110
11160
|
test_runner="cargo-test"
|
|
11111
|
-
local output
|
|
11112
|
-
|
|
11113
|
-
|
|
11161
|
+
local output cargo_exit _cargo_to _cargo_cmd=()
|
|
11162
|
+
_cargo_to="${LOKI_GATE_TIMEOUT:-300}"
|
|
11163
|
+
read -r -a _cargo_cmd <<< "$(_loki_timeout_prefix "$_cargo_to" 'cargo test gate')"
|
|
11164
|
+
output=$(cd "${TARGET_DIR:-.}" && "${_cargo_cmd[@]}" cargo test 2>&1)
|
|
11165
|
+
cargo_exit=$?
|
|
11166
|
+
if [ "$cargo_exit" -eq 124 ]; then
|
|
11167
|
+
test_passed=false
|
|
11168
|
+
log_warn "cargo test gate timed out after ${_cargo_to}s (exit 124)"
|
|
11169
|
+
details="cargo test: TIMED OUT after ${_cargo_to}s -- $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
11170
|
+
else
|
|
11171
|
+
[ "$cargo_exit" -ne 0 ] && test_passed=false
|
|
11172
|
+
details="cargo test: $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
11173
|
+
fi
|
|
11114
11174
|
fi
|
|
11115
11175
|
|
|
11116
11176
|
# node --test (built-in Node test runner) -- config-less fallback (task #79).
|
|
@@ -16936,8 +16996,64 @@ check_completion_promise() {
|
|
|
16936
16996
|
}
|
|
16937
16997
|
|
|
16938
16998
|
# Check if max iterations reached
|
|
16999
|
+
# EVIDENCE-AWARE ITERATION CAP.
|
|
17000
|
+
#
|
|
17001
|
+
# The cap used to be a bare counter: it consulted no gate, no council, and no
|
|
17002
|
+
# evidence. A run one step from finishing was cut off identically to a run
|
|
17003
|
+
# thrashing in circles, and both reported the same terminal.
|
|
17004
|
+
#
|
|
17005
|
+
# An iteration count is a PROXY for "is this converging". Where real evidence
|
|
17006
|
+
# exists, prefer the evidence. Two signals are already on disk at this point:
|
|
17007
|
+
#
|
|
17008
|
+
# 1. the model's own completion request (.loki/signals/COMPLETION_REQUESTED),
|
|
17009
|
+
# which the agent writes when it believes the work is done
|
|
17010
|
+
# 2. gate state (.loki/quality/gate-failures.txt), which says whether the
|
|
17011
|
+
# last verification pass actually found anything
|
|
17012
|
+
#
|
|
17013
|
+
# When the model says it is done AND no gate is failing, the run gets ONE extra
|
|
17014
|
+
# iteration to land it. That is the difference between a finished product and a
|
|
17015
|
+
# terminal failure at the buzzer.
|
|
17016
|
+
#
|
|
17017
|
+
# WHY THIS CANNOT LOOP FOREVER, which is the only thing that matters here:
|
|
17018
|
+
# the grace is granted at most once per run (a marker file, checked before it
|
|
17019
|
+
# is written), it requires POSITIVE evidence rather than the absence of a
|
|
17020
|
+
# signal, and it extends by exactly one iteration. A run that keeps claiming
|
|
17021
|
+
# done without finishing gets the cap, once, and then stops. Published
|
|
17022
|
+
# measurements put automated-verifier false-negative rates near 24%, so an
|
|
17023
|
+
# unbounded verifier-driven loop would burn real money on already-correct work.
|
|
17024
|
+
# This is deliberately a bounded nudge, not a verifier-driven terminal.
|
|
17025
|
+
#
|
|
17026
|
+
# LOKI_ITERATION_GRACE=0 restores the pure counter.
|
|
17027
|
+
_iteration_grace_available() {
|
|
17028
|
+
[ "${LOKI_ITERATION_GRACE:-1}" != "0" ] || return 1
|
|
17029
|
+
|
|
17030
|
+
local _loki_root="${TARGET_DIR:-.}/.loki"
|
|
17031
|
+
local _marker="$_loki_root/state/iteration-grace-used"
|
|
17032
|
+
[ -f "$_marker" ] && return 1
|
|
17033
|
+
|
|
17034
|
+
# POSITIVE evidence the model believes it is done. Absence is not evidence.
|
|
17035
|
+
[ -f "$_loki_root/signals/COMPLETION_REQUESTED" ] || return 1
|
|
17036
|
+
|
|
17037
|
+
# ...and nothing is currently failing. A non-empty gate-failures.txt means
|
|
17038
|
+
# the last verification pass found real problems, so a "done" claim on top
|
|
17039
|
+
# of it is exactly the case the cap should still stop.
|
|
17040
|
+
local _gf="$_loki_root/quality/gate-failures.txt"
|
|
17041
|
+
if [ -s "$_gf" ]; then
|
|
17042
|
+
return 1
|
|
17043
|
+
fi
|
|
17044
|
+
|
|
17045
|
+
mkdir -p "$_loki_root/state" 2>/dev/null || true
|
|
17046
|
+
printf 'granted at iteration %s\n' "${ITERATION_COUNT:-0}" > "$_marker" 2>/dev/null || true
|
|
17047
|
+
return 0
|
|
17048
|
+
}
|
|
17049
|
+
|
|
16939
17050
|
check_max_iterations() {
|
|
16940
17051
|
if [ $ITERATION_COUNT -ge $MAX_ITERATIONS ]; then
|
|
17052
|
+
if _iteration_grace_available; then
|
|
17053
|
+
MAX_ITERATIONS=$((MAX_ITERATIONS + 1))
|
|
17054
|
+
log_info "Iteration cap reached, but the agent reports done with no failing gate -- granting ONE final iteration to land it (once per run; LOKI_ITERATION_GRACE=0 to disable)."
|
|
17055
|
+
return 1
|
|
17056
|
+
fi
|
|
16941
17057
|
log_warn "Max iterations ($MAX_ITERATIONS) reached. Stopping."
|
|
16942
17058
|
return 0
|
|
16943
17059
|
fi
|
|
@@ -24182,6 +24298,15 @@ main() {
|
|
|
24182
24298
|
set --
|
|
24183
24299
|
fi
|
|
24184
24300
|
|
|
24301
|
+
# Re-apply the scoped-change profile now that PRD_PATH is known.
|
|
24302
|
+
#
|
|
24303
|
+
# The module-scope call runs at source time, BEFORE this argument parsing,
|
|
24304
|
+
# so the spec path was always empty there and an issue-sourced build could
|
|
24305
|
+
# never be recognised. Re-running it here is idempotent: the profile assigns
|
|
24306
|
+
# with := so anything already set (including an explicit operator override)
|
|
24307
|
+
# is left untouched, and a non-scoped run still returns immediately.
|
|
24308
|
+
loki_apply_scoped_change_profile "$PRD_PATH"
|
|
24309
|
+
|
|
24185
24310
|
# Validate PRD if provided
|
|
24186
24311
|
if [ -n "$PRD_PATH" ] && [ ! -f "$PRD_PATH" ]; then
|
|
24187
24312
|
log_error "PRD file not found: $PRD_PATH"
|
package/dashboard/__init__.py
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var h_=Object.create;var{getPrototypeOf:v_,defineProperty:rK,getOwnPropertyNames:g_}=Object;var m_=Object.prototype.hasOwnProperty;function u_(Z){return this[Z]}var p_,d_,c_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?p_??=new WeakMap:d_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?h_(v_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of g_(Z))if(!m_.call(K,$))rK(K,$,{get:u_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var qQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var l_=(Z)=>Z;function i_(Z,X){this[Z]=l_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:i_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>P4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>r0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as a_}from"url";import{existsSync as GQ}from"fs";import{homedir as s_}from"os";function n_(){let Z=RO;for(let X=0;X<6;X++){if(GQ(n7(Z,"VERSION"))&&GQ(n7(Z,"autonomy/run.sh")))return Z;let Q=tK(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function eK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(GQ(n7(X,"VERSION"))&&GQ(n7(X,"autonomy/run.sh")))return X;let Y=tK(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function P4(){return n7(s_(),".loki")}var RO,r0;var G8=p(()=>{RO=tK(a_(import.meta.url));r0=n_()});import{readFileSync as o_}from"fs";import{resolve as r_,dirname as t_}from"path";import{fileURLToPath as e_}from"url";function f3(){if(h5!==null)return h5;let Z="8.
|
|
2
|
+
var h_=Object.create;var{getPrototypeOf:v_,defineProperty:rK,getOwnPropertyNames:g_}=Object;var m_=Object.prototype.hasOwnProperty;function u_(Z){return this[Z]}var p_,d_,c_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?p_??=new WeakMap:d_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?h_(v_(Z)):{};let K=X||!Z||!Z.__esModule?rK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of g_(Z))if(!m_.call(K,$))rK(K,$,{get:u_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var qQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var l_=(Z)=>Z;function i_(Z,X){this[Z]=l_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)rK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:i_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var e0=import.meta.require;var kO={};l0(kO,{lokiDir:()=>j0,homeLokiDir:()=>P4,findRepoRootForVersion:()=>eK,REPO_ROOT:()=>r0});import{resolve as n7,dirname as tK}from"path";import{fileURLToPath as a_}from"url";import{existsSync as GQ}from"fs";import{homedir as s_}from"os";function n_(){let Z=RO;for(let X=0;X<6;X++){if(GQ(n7(Z,"VERSION"))&&GQ(n7(Z,"autonomy/run.sh")))return Z;let Q=tK(Z);if(Q===Z)break;Z=Q}return n7(RO,"..","..","..")}function eK(Z){let X=Z;for(let Q=0;Q<6;Q++){if(GQ(n7(X,"VERSION"))&&GQ(n7(X,"autonomy/run.sh")))return X;let Y=tK(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function P4(){return n7(s_(),".loki")}var RO,r0;var G8=p(()=>{RO=tK(a_(import.meta.url));r0=n_()});import{readFileSync as o_}from"fs";import{resolve as r_,dirname as t_}from"path";import{fileURLToPath as e_}from"url";function f3(){if(h5!==null)return h5;let Z="8.12.0";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=t_(e_(import.meta.url)),Q=eK(X);h5=o_(r_(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var HQ=p(()=>{G8()});var bO={};l0(bO,{runOrThrow:()=>Of,run:()=>E0,readStreamCapped:()=>UQ,commandVersion:()=>Af,commandExists:()=>X9,ShellError:()=>Z$,MAX_STDOUT_BYTES:()=>yO});async function UQ(Z,X=yO){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 E0(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([UQ(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 Of(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Z$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=Lf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Lf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Af(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var yO=16777216,Z$;var x9=p(()=>{Z$=class Z$ 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 o7(Z){return jf?"":Z}var jf,L0,k8,p0,ZV0,i0,H8,Q9,v;var S6=p(()=>{jf=(process.env.NO_COLOR??"").length>0;L0=o7("\x1B[0;31m"),k8=o7("\x1B[0;32m"),p0=o7("\x1B[1;33m"),ZV0=o7("\x1B[0;34m"),i0=o7("\x1B[0;36m"),H8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),v=o7("\x1B[0m")});import{existsSync as xf}from"fs";async function E7(){if(k4!==void 0)return k4;let Z="/opt/homebrew/bin/python3.12";if(xf(Z))return k4=Z,Z;let X=await X9("python3.12");if(X)return k4=X,X;let Q=await X9("python3");return k4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var k4;var r7=p(()=>{x9()});var ZL={};l0(ZL,{runStatus:()=>Yh});import{existsSync as Y9,readFileSync as v3,readdirSync as iO,statSync as aO}from"fs";import{resolve as h8,basename as sf}from"path";import{homedir as nf}from"os";function sO(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 nO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*NQ/X);if(J>NQ)J=NQ;let z=NQ-J,K=k8;if(Y>=80)K=L0;else if(Y>=50)K=p0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),W=sO(Z),V=sO(X);return` ${H8}${Q}${v} ${K}[${$}]${v} ${Y}% (${W} / ${V})`}async function rf(){if(await X9("jq"))return!0;return process.stdout.write(`${L0}Error: jq is required but not installed.${v}
|
|
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)
|
|
@@ -1222,4 +1222,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
1222
1222
|
`),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (b_(),y_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
|
|
1223
1223
|
`),process.stderr.write(__),2}}cO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var fW0=await _W0(Bun.argv.slice(2));process.exit(fW0);
|
|
1224
1224
|
|
|
1225
|
-
//# debugId=
|
|
1225
|
+
//# debugId=12231098D894234164756E2164756E21
|
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.
|
|
4
|
+
"version": "8.12.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.
|
|
5
|
+
"version": "8.12.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/skills/00-index.md
CHANGED
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
| Legacy healing, modernization, archaeology | `healing.md` |
|
|
36
36
|
| Plan deepening, knowledge extraction | `compound-learning.md` |
|
|
37
37
|
| Managed Agents memory, multiagent council, flag hierarchy | `memory.md` |
|
|
38
|
+
| Non-trivial change (>3 files, agent runtime, MINOR/MAJOR release) | `sdlc-fleet.md` |
|
|
39
|
+
| Adding your own gate/reviewer/agent to the loop | `extending.md` |
|
|
38
40
|
|
|
39
41
|
## Module Descriptions
|
|
40
42
|
|
|
@@ -159,6 +161,20 @@
|
|
|
159
161
|
- Language-specific guides (COBOL, legacy Java, PHP, Python 2)
|
|
160
162
|
- Full reference: `references/legacy-healing-patterns.md`
|
|
161
163
|
|
|
164
|
+
### sdlc-fleet.md
|
|
165
|
+
**When:** Any non-trivial change (feature touching >3 files, bug fix in agent
|
|
166
|
+
runtime / council / memory / auto-spawn, MINOR or MAJOR release, cross-route
|
|
167
|
+
parity change). Binding per CLAUDE.md, so this index must be able to route to it.
|
|
168
|
+
- Six roles: Architect, Product Owner, Dev Fleet, SDET, Council Reviewers, Real-User QA
|
|
169
|
+
- Unanimous 3-of-3 APPROVE required from the review council
|
|
170
|
+
- Skip rules: typo fixes, docs-only edits, reverts, emergency hotfixes
|
|
171
|
+
|
|
172
|
+
### extending.md
|
|
173
|
+
**When:** Adding a custom reviewer/agent to the loop without editing engine files
|
|
174
|
+
- The live seam: `loki agent install` -> `.loki/agents/installed.json`
|
|
175
|
+
- What is strictly additive vs what can never displace a built-in gate
|
|
176
|
+
- Honest status of `src/plugins/` (built, tested, not wired into the loop)
|
|
177
|
+
|
|
162
178
|
### providers.md (v5.0.0)
|
|
163
179
|
**When:** Using non-Claude providers (Codex, Cline, Aider), understanding degraded mode
|
|
164
180
|
- Provider comparison matrix
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Extending the Loop
|
|
2
|
+
|
|
3
|
+
**When to load:** you want to add your own reviewer, agent, or gate to the RARV
|
|
4
|
+
loop without editing engine files.
|
|
5
|
+
|
|
6
|
+
Read this before writing an extension. Every claim below is cited to source, and
|
|
7
|
+
the one live seam is exercised by `tests/test-installed-agent-reviewer.sh`
|
|
8
|
+
(10 tests, wired into `tests/run-all-tests.sh:732`).
|
|
9
|
+
|
|
10
|
+
## What is actually extensible today
|
|
11
|
+
|
|
12
|
+
| Surface | Extend without editing engine files? | Mechanism |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Code-review reviewer pool | **Yes** | `loki agent install` -> `.loki/agents/installed.json` |
|
|
15
|
+
| Numbered quality gates 1-8 | No | Compiled into `autonomy/run.sh` phases; opt-out flags only |
|
|
16
|
+
| RARV phases | No | Phase sequence lives in `run_autonomous()` (`autonomy/run.sh`) |
|
|
17
|
+
| MCP tools | Yes | Standard MCP server config (see `references/mcp-integration.md`) |
|
|
18
|
+
|
|
19
|
+
Only the reviewer pool has a supported, tested, data-only extension seam. The
|
|
20
|
+
rest are opt-out (disable) surfaces, not opt-in (add) surfaces. If you need a new
|
|
21
|
+
blocking gate, that is an engine change, not an extension.
|
|
22
|
+
|
|
23
|
+
## The reviewer seam (R10)
|
|
24
|
+
|
|
25
|
+
`autonomy/run.sh:14002-14036` loads user-installed agents into a dict named
|
|
26
|
+
`INSTALLED_SPECIALISTS` (declared at `:14013`), separate from the built-in
|
|
27
|
+
`SPECIALISTS` pool.
|
|
28
|
+
|
|
29
|
+
Line numbers drift. Re-anchor with
|
|
30
|
+
`grep -n "R10 extension seam" autonomy/run.sh` before relying on them.
|
|
31
|
+
|
|
32
|
+
### Install
|
|
33
|
+
|
|
34
|
+
Write a manifest and install it. The source may be a local path, a git repo URL,
|
|
35
|
+
or a raw manifest URL (`autonomy/loki:26774`).
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"type": "a11y-auditor",
|
|
40
|
+
"name": "Accessibility Auditor",
|
|
41
|
+
"swarm": "review",
|
|
42
|
+
"capabilities": "WCAG and screen-reader review",
|
|
43
|
+
"focus": ["aria", "accessibility", "contrast", "keyboard"],
|
|
44
|
+
"persona": "You audit for WCAG 2.2 AA compliance."
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
loki agent install ./my-agent/manifest.json
|
|
50
|
+
# Installed agent: a11y-auditor (Accessibility Auditor)
|
|
51
|
+
# Stored in .loki/agents/installed.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`focus` is load-bearing: those strings become the keywords that decide whether
|
|
55
|
+
your reviewer fires on a given diff. An agent with an empty `focus` is skipped
|
|
56
|
+
rather than made always-on (`autonomy/run.sh:14027`).
|
|
57
|
+
|
|
58
|
+
### When it fires
|
|
59
|
+
|
|
60
|
+
Your reviewer only fires when one of its `focus` keywords appears in the diff or
|
|
61
|
+
changed-file list, so a backend-only change does not pay for an a11y auditor.
|
|
62
|
+
Verified by running the real selector block extracted from `run.sh` against a
|
|
63
|
+
diff containing `aria-label`, `contrast`, `keyboard`:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
REVIEWERS: ['architecture-strategist', 'maintainer-mergeability',
|
|
67
|
+
'eng-frontend', 'security-sentinel', 'a11y-auditor']
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The silent case (a diff matching none of the agent's keywords leaves it out of
|
|
71
|
+
the pool) is covered by the `stays silent on a diff its keywords do not match`
|
|
72
|
+
case in `tests/test-installed-agent-reviewer.sh`.
|
|
73
|
+
|
|
74
|
+
## Three invariants you cannot override
|
|
75
|
+
|
|
76
|
+
These are deliberate. An extension can only ADD scrutiny.
|
|
77
|
+
|
|
78
|
+
1. **Never displaces a built-in reviewer.** Installed agents are kept in a
|
|
79
|
+
separate dict and APPENDED after the built-in `ranked[:want]` selection
|
|
80
|
+
(`autonomy/run.sh:14011` records that an earlier version displaced
|
|
81
|
+
`security-sentinel`, which is why the dicts are split). In the run above,
|
|
82
|
+
`security-sentinel` is still present alongside the installed agent.
|
|
83
|
+
2. **Never shadows a built-in type.** An installed agent whose `type` collides
|
|
84
|
+
with a built-in is skipped (`autonomy/run.sh:14022-14024`).
|
|
85
|
+
3. **Capped at 2 installed reviewers per run.** `_MAX_INSTALLED_REVIEWERS = 2`
|
|
86
|
+
(`autonomy/run.sh:14104`). Each appended agent costs one more LLM reviewer
|
|
87
|
+
call every iteration. Verified: three installed agents whose keywords all
|
|
88
|
+
match one diff yield exactly two appended reviewers
|
|
89
|
+
(`['a11y-one', 'a11y-two']`, the third dropped).
|
|
90
|
+
|
|
91
|
+
A corrupt or absent `installed.json` is swallowed and leaves the normal battery
|
|
92
|
+
intact (`autonomy/run.sh:14035-14036`) -- extensions must never be able to break
|
|
93
|
+
code review.
|
|
94
|
+
|
|
95
|
+
## Manifests are data, never code
|
|
96
|
+
|
|
97
|
+
`hub_install.py` stores manifest fields; it never executes anything from them.
|
|
98
|
+
Executable-looking fields are stripped and reported. Verified by installing a
|
|
99
|
+
manifest carrying a `postinstall` field:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
{"type": "a11y-auditor", ..., "_ignored_executable_fields": ["postinstall"]}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The command in that field did not run. Covered by the test
|
|
106
|
+
`manifest postinstall field is not stored and never runs (data-only)`.
|
|
107
|
+
|
|
108
|
+
## `src/plugins/` is NOT a seam (status: unwired)
|
|
109
|
+
|
|
110
|
+
`src/plugins/` contains `GatePlugin`, `AgentPlugin`, `MCPPlugin`,
|
|
111
|
+
`IntegrationPlugin`, a loader and a validator, with 84 passing tests in
|
|
112
|
+
`tests/plugins/`.
|
|
113
|
+
|
|
114
|
+
**Status: not wired.** No engine file calls any of it, and the loop will never
|
|
115
|
+
call it. There is no `require` of `src/plugins` in
|
|
116
|
+
`autonomy/run.sh`, `autonomy/loki`, or anywhere outside its own tests, and
|
|
117
|
+
`tests/plugins/` is absent from the `test` script in `package.json`, from
|
|
118
|
+
`tests/run-all-tests.sh`, and from `scripts/local-ci.sh` -- so those 84 tests do
|
|
119
|
+
not run in CI.
|
|
120
|
+
|
|
121
|
+
Do not register a gate through `GatePlugin` expecting the loop to execute it.
|
|
122
|
+
Registration succeeds and nothing ever calls the gate. It is documented here so
|
|
123
|
+
the next reader does not mistake a green test suite for a working feature.
|
|
124
|
+
|
|
125
|
+
## Adding a real gate
|
|
126
|
+
|
|
127
|
+
If you need a new blocking gate, it is an engine change: add the phase to
|
|
128
|
+
`autonomy/run.sh`, give it an opt-out flag matching the `LOKI_GATE_*` convention
|
|
129
|
+
in `skills/quality-gates.md`, and mirror it on the Bun route. Follow
|
|
130
|
+
`skills/sdlc-fleet.md` -- a new gate is a non-trivial change and needs the
|
|
131
|
+
council.
|
|
@@ -32,7 +32,7 @@ Claude Fable 5 (alias `fable`, full id `claude-fable-5`) is Anthropic's most cap
|
|
|
32
32
|
|
|
33
33
|
### Mid-flight model switching
|
|
34
34
|
|
|
35
|
-
You can change the model a **live run** uses, from the dashboard or by writing a state file. The switch applies at the **next iteration boundary** (each iteration spawns a fresh `claude -p`, which fixes the model per invocation, so it never changes mid-invocation). The override applies to the **current run only**: the runner clears a leftover override at the start of a fresh run, so a switch never silently carries into future runs. The override is also clamped by `LOKI_MAX_TIER`: if the operator set a cost ceiling, an override above it is clamped down with one honest log line, and the dashboard reports the clamped effective model. The clamp is scoped, not blanket: on the override path a `sonnet` ceiling downgrades only `fable` (to `PROVIDER_MODEL_DEVELOPMENT`,
|
|
35
|
+
You can change the model a **live run** uses, from the dashboard or by writing a state file. The switch applies at the **next iteration boundary** (each iteration spawns a fresh `claude -p`, which fixes the model per invocation, so it never changes mid-invocation). The override applies to the **current run only**: the runner clears a leftover override at the start of a fresh run, so a switch never silently carries into future runs. The override is also clamped by `LOKI_MAX_TIER`: if the operator set a cost ceiling, an override above it is clamped down with one honest log line, and the dashboard reports the clamped effective model. The clamp is scoped, not blanket: on the override path a `sonnet` ceiling downgrades only `fable` (to `PROVIDER_MODEL_DEVELOPMENT`, sonnet by default per `providers/claude.sh:61`), while a plain `opus` override stays opus; a `haiku` ceiling pins to `PROVIDER_MODEL_FAST`; an `opus` ceiling caps only `fable` back to opus. (The clamp is enforced on the bash runner, which is the live `start` route; the experimental Bun runner does not yet port it.)
|
|
36
36
|
|
|
37
37
|
- Dashboard: the Model selector in the session-control panel. The Fable option shows its 2x-Opus cost; an inline notice discloses the iteration-boundary timing. It calls `POST /api/session/model`.
|
|
38
38
|
- File / CLI: write an allowlisted alias (`haiku`, `sonnet`, `opus`, `fable`) to `.loki/state/model-override`. Empty or absent file reverts to the tier mapping. Invalid content is ignored (the runtime warns once). The value is allowlist-validated because it is fed straight into `claude --model`.
|
|
@@ -58,7 +58,7 @@ For diffs that move money or mutate infrastructure (payments, billing, spend aut
|
|
|
58
58
|
|
|
59
59
|
### Cost estimate honesty
|
|
60
60
|
|
|
61
|
-
`loki plan` quotes the model the runner actually dispatches, on **every** path, for the levers the runner honors. This includes the stock no-lever default: the `sonnet` session pin resolves through the development tier to
|
|
61
|
+
`loki plan` quotes the model the runner actually dispatches, on **every** path, for the levers the runner honors. This includes the stock no-lever default: the `sonnet` session pin resolves through the development tier to sonnet, so the quote names Sonnet on the default path (`providers/claude.sh:61` sets `CLAUDE_DEFAULT_DEVELOPMENT="sonnet"`, and the estimator mirrors it at `autonomy/loki:17378-17382`; a stock `loki plan` prints `Model distribution: Sonnet x1`). Set `LOKI_SESSION_MODEL=fable`, or have a pending `.loki/state/model-override` of `fable`, and the estimate uses Fable's $10/$50 pricing with a 2x-Opus note. The quote, the dashboard `effective` model, and the actual `claude --model` argument always agree on both routes: the session-pin tier route (no override) and the override-path clamp (override present) are each mirrored exactly in the estimator and the dashboard, so the same lever that changes the quote changes the run. `LOKI_MAX_TIER` is applied to the quote too, so the estimate never quotes a model above the operator's cost ceiling. (`LOKI_MODEL` is not a session lever and does not affect the run; use `LOKI_SESSION_MODEL`.)
|
|
62
62
|
|
|
63
63
|
**Where the dispatched model is disclosed (precisely):**
|
|
64
64
|
- Override path (a `.loki/state/model-override` alias): the runner logs the override and the clamp line (`model override: <model>`, plus the one honest clamp-down log line when `LOKI_MAX_TIER` reduces it); the dashboard `GET /api/session/model` `effective` field reports the clamped model.
|
|
@@ -68,14 +68,29 @@ For diffs that move money or mutate infrastructure (payments, billing, spend aut
|
|
|
68
68
|
|
|
69
69
|
## Multi-Provider Support (v5.0.0)
|
|
70
70
|
|
|
71
|
-
Loki Mode supports five AI providers.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
71
|
+
Loki Mode supports five AI providers. The authoritative list is
|
|
72
|
+
`SUPPORTED_PROVIDERS` in `providers/loader.sh:8`; each provider's capability
|
|
73
|
+
flags live in its own `providers/<name>.sh`. Claude has full features. Every
|
|
74
|
+
other provider runs sequentially (no Task tool, no parallel agents), but only
|
|
75
|
+
those with `PROVIDER_DEGRADED=true` are flagged degraded.
|
|
76
|
+
|
|
77
|
+
| Provider | Full Features | `PROVIDER_DEGRADED` | CLI Flag |
|
|
78
|
+
|----------|---------------|---------------------|----------|
|
|
79
|
+
| **Claude Code** | Yes | false (`providers/claude.sh:104`) | `--provider claude` (default) |
|
|
80
|
+
| **OpenAI Codex CLI** | No | true (`providers/codex.sh:180`) | `--provider codex` |
|
|
81
|
+
| **Cline CLI** | No | false (`providers/cline.sh:97`) | `--provider cline` |
|
|
82
|
+
| **Aider** | No | true (`providers/aider.sh:95`) | `--provider aider` |
|
|
83
|
+
| **opencode** | No | false (`providers/opencode.sh:66`) | `--provider opencode` |
|
|
84
|
+
|
|
85
|
+
Only **codex** and **aider** are flagged degraded. Cline and opencode are
|
|
86
|
+
sequential but not degraded, so do not describe them as degraded-mode providers.
|
|
87
|
+
|
|
88
|
+
**opencode** is the model-agnostic route: it reaches 75+ model providers, so it
|
|
89
|
+
is the path to cheap/open models. It is sequential like the others
|
|
90
|
+
(`PROVIDER_HAS_SUBAGENTS=false`, `PROVIDER_HAS_PARALLEL=false`,
|
|
91
|
+
`PROVIDER_HAS_TASK_TOOL=false` at `providers/opencode.sh:61-63`) but it does
|
|
92
|
+
support MCP (`PROVIDER_HAS_MCP=true`, `:64`), which is why it is not marked
|
|
93
|
+
degraded.
|
|
79
94
|
|
|
80
95
|
**Degraded mode limitations:**
|
|
81
96
|
- No Task tool (cannot spawn subagents)
|