loki-mode 9.22.12 → 9.24.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 +3 -3
- package/VERSION +1 -1
- package/autonomy/lib/claude-flags.sh +69 -5
- package/autonomy/lib/contract-scaffold/scaffold.sh +1 -1
- package/autonomy/loki +324 -74
- package/autonomy/run.sh +176 -40
- package/autonomy/verify.sh +16 -3
- package/bin/loki +25 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +88 -4
- package/dashboard/server.py +50 -48
- package/docs/INSTALLATION.md +2 -2
- package/docs/RARV-C-100X-PLAN.md +9 -1
- package/docs/enterprise/performance.md +51 -9
- package/docs/exit-codes.md +4 -0
- package/loki-ts/dist/cockpit.js +1 -1
- package/loki-ts/dist/loki.js +634 -549
- package/loki-ts/package.json +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +6 -5
- package/plugins/loki-mode/.claude-plugin/plugin.json +2 -3
- package/providers/loader.sh +2 -2
- package/providers/model_catalog.json +26 -8
- package/providers/models.sh +1 -1
- package/providers/opencode.sh +1 -1
- package/skills/providers.md +42 -14
- package/tools/normalize-package-permissions.mjs +34 -0
- package/web-app/dist/assets/{AdminPage-DLhWX6si.js → AdminPage-PyyVXg4b.js} +1 -1
- package/web-app/dist/assets/{Avatar-CqyDcUxl.js → Avatar-CTUlr_Xf.js} +1 -1
- package/web-app/dist/assets/{Badge-BKGSt1n2.js → Badge-BQeXvmg8.js} +1 -1
- package/web-app/dist/assets/{Button-DuuKHXre.js → Button-Dc6-P6pz.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DdClYERC.js → CockpitPage-BspOJ3Fv.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CUWlqBxv.js → ComparePage-CFXIM--Z.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-RwfGKyRJ.js → ErrorBoundary-C7vQYDW9.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CzIgk9sg.js → EvidenceReceiptPanel-CqjsjTOs.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-CsXVKuyJ.js → GitHubIssuesPanel-DyuNDMgD.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CYRp1eKS.js → GitHubPRsPanel-BtAq20xF.js} +1 -1
- package/web-app/dist/assets/{HomePage-CVJcI5pQ.js → HomePage-BuVFOrO8.js} +1 -1
- package/web-app/dist/assets/{LoginPage-BBJNV9Qb.js → LoginPage-CuhhrxGb.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CIi9P8jK.js → MagicPage-D2KZPX3F.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-Bm34ipiy.js → MetricsPage-DFEYJl9z.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-DpTbcC2P.js → NotFoundPage-CTnnaWlL.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CY2SMGx9.js → ProjectPage-uJFaoM8-.js} +55 -55
- package/web-app/dist/assets/{ProjectsPage-DSrXUUKJ.js → ProjectsPage-8lKAOBIW.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-jvDyOqYi.js → SettingsPage-D1BBd5qy.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-C8tFWKau.js → ShowcasePage-CmQ0n8iO.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DH4FTH3K.js → SystemSettingsPage-CSHaVyuD.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-C1fQYauF.js → TeamsPage-BS0TtVUa.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-CSTFt-4P.js → TemplatesPage-C2zPnnf2.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cu4NsyN6.js → TerminalOutput-Dle1klqq.js} +1 -1
- package/web-app/dist/assets/{activity-CtjsDzmh.js → activity-DQp5U9f9.js} +1 -1
- package/web-app/dist/assets/{bell-C5es1xAs.js → bell-BqkTmgL7.js} +1 -1
- package/web-app/dist/assets/{bot-DE04U5Lc.js → bot-DILhaIGH.js} +1 -1
- package/web-app/dist/assets/{check-DSKKA9EU.js → check-BC9x7oPm.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DJ13W5AR.js → chevron-left--yqvAfjx.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CS0BfqAd.js → circle-alert-DOH6PcgZ.js} +1 -1
- package/web-app/dist/assets/{clock-D1Y5nruk.js → clock-DKSDFsE6.js} +1 -1
- package/web-app/dist/assets/{cloud-nU6LBoZL.js → cloud-DRBU3JYs.js} +1 -1
- package/web-app/dist/assets/{code-xml-BZfvIdFo.js → code-xml-D2LqszR9.js} +1 -1
- package/web-app/dist/assets/{copy-CK_2O1Xv.js → copy-BKW4U4Qr.js} +1 -1
- package/web-app/dist/assets/{database-DJiwznAm.js → database-D7ceedlh.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-pO20A3QY.js → dollar-sign-D0A47egY.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Besy6vek.js → file-code-corner-CH_Qbtjo.js} +1 -1
- package/web-app/dist/assets/{file-plus-DeaswxYY.js → file-plus-CUNMjHk9.js} +1 -1
- package/web-app/dist/assets/{folder-open-cr1UvQLI.js → folder-open-CQ_SVq94.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-BLxnfbSn.js → git-commit-horizontal-DQUBe0oD.js} +1 -1
- package/web-app/dist/assets/{globe-DrhG1NfX.js → globe-gZnaGg8N.js} +1 -1
- package/web-app/dist/assets/{hammer-CeZtPBfE.js → hammer-BhJC5DMK.js} +1 -1
- package/web-app/dist/assets/{index-CLvjO22Z.js → index-BMmYYws0.js} +2 -2
- package/web-app/dist/assets/{layers-fldADzyE.js → layers-glXeoUbJ.js} +1 -1
- package/web-app/dist/assets/{lightbulb-CznOfsPb.js → lightbulb-gXn8H0Lf.js} +1 -1
- package/web-app/dist/assets/{loader-circle---lPB1Fs.js → loader-circle-Do2ke8xm.js} +1 -1
- package/web-app/dist/assets/{lock-D5jVF-tO.js → lock-Bgplb1_n.js} +1 -1
- package/web-app/dist/assets/{mail-D7HkLnlW.js → mail-j4Lo8hfB.js} +1 -1
- package/web-app/dist/assets/{minus-C-dT19Ct.js → minus-dhrzKSK5.js} +1 -1
- package/web-app/dist/assets/{package-Cm9B-cKE.js → package-CInWQQ97.js} +1 -1
- package/web-app/dist/assets/{plus-CQwOffLD.js → plus-BpYvoLlZ.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CvWJZLbN.js → refresh-cw-F6koqEgF.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-C2Q1LYgM.js → rotate-ccw-CQ2W5S8f.js} +1 -1
- package/web-app/dist/assets/{save-BP0MvpoQ.js → save-DMCYGm06.js} +1 -1
- package/web-app/dist/assets/{server-4i4VZXix.js → server-Bj8EX4Z1.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D2_bgb7j.js → shield-alert-CiYuulCU.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-vACbzeXj.js → thumbs-up-BqEh1puP.js} +1 -1
- package/web-app/dist/assets/{trash-2-EYjdESLs.js → trash-2-CCwouZJJ.js} +1 -1
- package/web-app/dist/assets/{trending-down-BWBdcvE3.js → trending-down-Cx1vhE4o.js} +1 -1
- package/web-app/dist/assets/{trending-up-Bix6V_-Y.js → trending-up-BUhVYJ2T.js} +1 -1
- package/web-app/dist/assets/{upload-B7hh6ERF.js → upload-D5IsZeFM.js} +1 -1
- package/web-app/dist/assets/{usePolling-b_e-k-IN.js → usePolling-CJbfhFcH.js} +1 -1
- package/web-app/dist/assets/{user-pmjZKWEg.js → user-BRLhezsD.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/web-app/requirements.txt +1 -0
- package/web-app/server.py +160 -31
package/autonomy/loki
CHANGED
|
@@ -1456,7 +1456,7 @@ show_help() {
|
|
|
1456
1456
|
echo "list with 'loki help aliases'."
|
|
1457
1457
|
echo ""
|
|
1458
1458
|
echo "Options for 'start':"
|
|
1459
|
-
echo " --provider NAME AI provider: claude (default), codex, cline, aider"
|
|
1459
|
+
echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
|
|
1460
1460
|
echo " --parallel Enable parallel mode with git worktrees"
|
|
1461
1461
|
echo " --bg, --background Run in background mode"
|
|
1462
1462
|
echo " --simple Force simple complexity tier (3 phases)"
|
|
@@ -1502,7 +1502,7 @@ show_help() {
|
|
|
1502
1502
|
echo " loki cleanup # Kill orphaned processes"
|
|
1503
1503
|
echo ""
|
|
1504
1504
|
echo " # Providers + model routing"
|
|
1505
|
-
echo " loki provider list # Show
|
|
1505
|
+
echo " loki provider list # Show available providers, including opencode"
|
|
1506
1506
|
echo " loki provider set codex # Switch active provider"
|
|
1507
1507
|
echo " # Cheap / open models (DeepSeek, GLM, MiniMax, Kimi) via OpenRouter:"
|
|
1508
1508
|
echo " loki provider set aider # aider + cline reach any OpenRouter model"
|
|
@@ -2067,7 +2067,7 @@ cmd_start() {
|
|
|
2067
2067
|
echo " building locally; polls until the job finishes."
|
|
2068
2068
|
echo " Token from LOKI_REMOTE_TOKEN / LOKI_REMOTE_TOKEN_FILE"
|
|
2069
2069
|
echo " (never an argument). Env: LOKI_REMOTE_URL."
|
|
2070
|
-
echo " --provider NAME AI provider: claude (default), codex, cline, aider"
|
|
2070
|
+
echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
|
|
2071
2071
|
echo " --parallel Enable parallel mode with git worktrees"
|
|
2072
2072
|
echo " --allow-haiku Enable Haiku model for the fast tier (default: disabled)"
|
|
2073
2073
|
echo " --bg, --background Run in background mode"
|
|
@@ -2161,7 +2161,7 @@ cmd_start() {
|
|
|
2161
2161
|
args+=("--provider" "$provider")
|
|
2162
2162
|
shift 2
|
|
2163
2163
|
else
|
|
2164
|
-
echo -e "${RED}--provider requires a value (claude, codex, cline, aider)${NC}"
|
|
2164
|
+
echo -e "${RED}--provider requires a value (claude, codex, cline, aider, opencode)${NC}"
|
|
2165
2165
|
exit 1
|
|
2166
2166
|
fi
|
|
2167
2167
|
;;
|
|
@@ -3367,7 +3367,7 @@ cmd_start() {
|
|
|
3367
3367
|
# can drop the marker when the originating process is no longer alive
|
|
3368
3368
|
# (SIGKILL / crash paths that skip our cleanup hooks).
|
|
3369
3369
|
case "$provider" in
|
|
3370
|
-
claude|codex|cline|aider)
|
|
3370
|
+
claude|codex|cline|aider|opencode)
|
|
3371
3371
|
mkdir -p "$LOKI_DIR/state" 2>/dev/null || true
|
|
3372
3372
|
printf '%s:%s:%s\n' "$provider" "$(date +%s)" "$$" \
|
|
3373
3373
|
> "$LOKI_DIR/state/.cli-provider.tmp" 2>/dev/null \
|
|
@@ -3409,6 +3409,7 @@ cmd_start() {
|
|
|
3409
3409
|
codex) echo " npm install -g @openai/codex" ;;
|
|
3410
3410
|
cline) echo " npm install -g cline" ;;
|
|
3411
3411
|
aider) echo " pip install aider-chat" ;;
|
|
3412
|
+
opencode) echo " npm install -g opencode-ai" ;;
|
|
3412
3413
|
*) echo " Check the provider documentation for installation." ;;
|
|
3413
3414
|
esac
|
|
3414
3415
|
echo ""
|
|
@@ -4064,6 +4065,172 @@ PYDASH
|
|
|
4064
4065
|
fi
|
|
4065
4066
|
}
|
|
4066
4067
|
|
|
4068
|
+
# Classify one current-project PID registry entry without signalling anything.
|
|
4069
|
+
#
|
|
4070
|
+
# A numeric PID alone is not process identity because the OS reuses PIDs. The registry's
|
|
4071
|
+
# UTC `started` field is the available launch token: the live process must have started
|
|
4072
|
+
# within a bounded window AROUND the recorded registration, and that registration must
|
|
4073
|
+
# itself be corroborated by the entry's st_ctime, which unprivileged userspace cannot
|
|
4074
|
+
# backdate. The file and its parent must also remain current-UID-owned, non-linked,
|
|
4075
|
+
# canonical local state. Any missing validation fails closed.
|
|
4076
|
+
#
|
|
4077
|
+
# Prints exactly one of: OWNED, STALE, REFUSED, UNAVAILABLE.
|
|
4078
|
+
_cleanup_registry_entry_state() {
|
|
4079
|
+
local entry_file="$1" expected_pid="$2"
|
|
4080
|
+
command -v python3 >/dev/null 2>&1 || {
|
|
4081
|
+
printf '%s\n' "UNAVAILABLE"
|
|
4082
|
+
return 0
|
|
4083
|
+
}
|
|
4084
|
+
python3 - "$entry_file" "$expected_pid" <<'PY' 2>/dev/null || printf '%s\n' "REFUSED"
|
|
4085
|
+
import calendar
|
|
4086
|
+
import json
|
|
4087
|
+
import os
|
|
4088
|
+
from pathlib import Path
|
|
4089
|
+
import stat
|
|
4090
|
+
import subprocess
|
|
4091
|
+
import sys
|
|
4092
|
+
import time
|
|
4093
|
+
|
|
4094
|
+
|
|
4095
|
+
def verdict(value: str) -> None:
|
|
4096
|
+
print(value)
|
|
4097
|
+
raise SystemExit(0)
|
|
4098
|
+
|
|
4099
|
+
|
|
4100
|
+
try:
|
|
4101
|
+
entry = Path(sys.argv[1])
|
|
4102
|
+
expected_pid = int(sys.argv[2])
|
|
4103
|
+
parent = entry.parent
|
|
4104
|
+
parent_info = os.lstat(parent)
|
|
4105
|
+
entry_info = os.lstat(entry)
|
|
4106
|
+
if (
|
|
4107
|
+
expected_pid <= 1
|
|
4108
|
+
or entry.name != f"{expected_pid}.json"
|
|
4109
|
+
or not stat.S_ISDIR(parent_info.st_mode)
|
|
4110
|
+
or stat.S_ISLNK(parent_info.st_mode)
|
|
4111
|
+
or parent_info.st_uid != os.getuid()
|
|
4112
|
+
or stat.S_IMODE(parent_info.st_mode) & 0o022
|
|
4113
|
+
or os.path.realpath(parent) != os.path.abspath(parent)
|
|
4114
|
+
or not stat.S_ISREG(entry_info.st_mode)
|
|
4115
|
+
or stat.S_ISLNK(entry_info.st_mode)
|
|
4116
|
+
or entry_info.st_uid != os.getuid()
|
|
4117
|
+
or entry_info.st_nlink != 1
|
|
4118
|
+
or stat.S_IMODE(entry_info.st_mode) & 0o022
|
|
4119
|
+
):
|
|
4120
|
+
verdict("REFUSED")
|
|
4121
|
+
|
|
4122
|
+
record = json.loads(entry.read_text(encoding="utf-8"))
|
|
4123
|
+
pid = record.get("pid")
|
|
4124
|
+
ppid = record.get("ppid")
|
|
4125
|
+
label = record.get("label")
|
|
4126
|
+
started = record.get("started")
|
|
4127
|
+
if (
|
|
4128
|
+
isinstance(pid, bool)
|
|
4129
|
+
or not isinstance(pid, int)
|
|
4130
|
+
or pid != expected_pid
|
|
4131
|
+
or isinstance(ppid, bool)
|
|
4132
|
+
or not isinstance(ppid, int)
|
|
4133
|
+
or ppid <= 0
|
|
4134
|
+
or not isinstance(label, str)
|
|
4135
|
+
or not label
|
|
4136
|
+
or not isinstance(started, str)
|
|
4137
|
+
):
|
|
4138
|
+
verdict("REFUSED")
|
|
4139
|
+
|
|
4140
|
+
try:
|
|
4141
|
+
os.kill(pid, 0)
|
|
4142
|
+
except ProcessLookupError:
|
|
4143
|
+
verdict("STALE")
|
|
4144
|
+
except (PermissionError, OSError):
|
|
4145
|
+
verdict("REFUSED")
|
|
4146
|
+
|
|
4147
|
+
# register_pid() (autonomy/run.sh) writes `started` on the line right after the
|
|
4148
|
+
# `&` that spawned the pid, so for a genuine child entry the process launch and
|
|
4149
|
+
# the recorded registration are seconds apart in BOTH directions.
|
|
4150
|
+
#
|
|
4151
|
+
# `kind":"wrapper"` entries are NOT this shape: register_self_wrapper() records
|
|
4152
|
+
# the wrapper's OWN pid deep in main() startup, so the honest gap is unbounded
|
|
4153
|
+
# and no tight window fits. They are also not cmd_cleanup's to reap -- run.sh's
|
|
4154
|
+
# cleanup_orphan_pids owns them behind a parent-dead AND idle AND no-live-child
|
|
4155
|
+
# predicate, because cmd_cleanup's parent-death-only rule is wrong for a
|
|
4156
|
+
# wrapper. Refuse them here rather than widen the window for every entry: a
|
|
4157
|
+
# window wide enough for a wrapper is wide enough to forge a child.
|
|
4158
|
+
if record.get("kind") is not None:
|
|
4159
|
+
verdict("REFUSED")
|
|
4160
|
+
|
|
4161
|
+
SLACK = 10
|
|
4162
|
+
registered_at = calendar.timegm(time.strptime(started, "%Y-%m-%dT%H:%M:%SZ"))
|
|
4163
|
+
if registered_at > time.time() + 5:
|
|
4164
|
+
verdict("REFUSED")
|
|
4165
|
+
|
|
4166
|
+
# `started` is attacker-writable, so it cannot vouch for itself. st_ctime is
|
|
4167
|
+
# the one field in this registry that unprivileged userspace cannot backdate
|
|
4168
|
+
# (utimes moves atime/mtime only), so it pins WHEN the entry was really made.
|
|
4169
|
+
# Without this an attacker just backdates `started` to an old victim's real
|
|
4170
|
+
# launch time and satisfies the window below.
|
|
4171
|
+
if abs(entry_info.st_ctime - registered_at) > SLACK:
|
|
4172
|
+
verdict("REFUSED")
|
|
4173
|
+
|
|
4174
|
+
observed = subprocess.run(
|
|
4175
|
+
["ps", "-o", "uid=", "-o", "ppid=", "-o", "lstart=", "-p", str(pid)],
|
|
4176
|
+
check=False,
|
|
4177
|
+
capture_output=True,
|
|
4178
|
+
text=True,
|
|
4179
|
+
timeout=2,
|
|
4180
|
+
)
|
|
4181
|
+
# lstart contains spaces, so split off uid and ppid only; the remainder is lstart.
|
|
4182
|
+
fields = observed.stdout.strip().split(maxsplit=2)
|
|
4183
|
+
if observed.returncode != 0 or len(fields) != 3 or int(fields[0]) != os.getuid():
|
|
4184
|
+
verdict("REFUSED")
|
|
4185
|
+
|
|
4186
|
+
# The recorded `ppid` is not decoration: register_pid() (autonomy/run.sh) writes
|
|
4187
|
+
# `"ppid":$$`, so for a genuine entry the registering shell IS the live process's
|
|
4188
|
+
# real parent. Nothing above binds the entry to that parentage, which is the whole
|
|
4189
|
+
# forgery: a fresh, same-uid, correctly-ctime'd entry naming an UNRELATED live pid
|
|
4190
|
+
# with an arbitrary dead `ppid` satisfies every other check and gets TERM then KILL.
|
|
4191
|
+
#
|
|
4192
|
+
# Two honest shapes, and only two:
|
|
4193
|
+
# recorded parent alive -> it must still BE the live parent.
|
|
4194
|
+
# recorded parent dead -> the child was reparented to init (pid 1), which is the
|
|
4195
|
+
# genuine orphan case cmd_cleanup exists to reap.
|
|
4196
|
+
# A forged entry aimed at an unrelated process fails both: that process's real
|
|
4197
|
+
# parent is the attacker's own live shell, which is neither the recorded ppid nor 1.
|
|
4198
|
+
# Both ceilings below fail CLOSED (a genuine entry is skipped and left for
|
|
4199
|
+
# inspection, never an unrelated process signalled), so neither can turn into
|
|
4200
|
+
# the forgery this guard exists to stop:
|
|
4201
|
+
# - if the recorded ppid died and the OS reused that number for an unrelated
|
|
4202
|
+
# live process, this reads "parent alive" and refuses a real orphan;
|
|
4203
|
+
# - on Linux under a PR_SET_CHILD_SUBREAPER ancestor (systemd user scope) an
|
|
4204
|
+
# orphan reparents to the subreaper rather than to 1 and is refused.
|
|
4205
|
+
# Widening either one means accepting an arbitrary live pid as the parent, which
|
|
4206
|
+
# is exactly the hole above, so the strict form stays until a portable way to
|
|
4207
|
+
# identify the real reaper exists. Reparent-to-init matches what the rest of the
|
|
4208
|
+
# runtime already assumes (see run.sh's detached getppid()==1 note).
|
|
4209
|
+
observed_ppid = int(fields[1])
|
|
4210
|
+
recorded_parent_alive = True
|
|
4211
|
+
try:
|
|
4212
|
+
os.kill(ppid, 0)
|
|
4213
|
+
except ProcessLookupError:
|
|
4214
|
+
recorded_parent_alive = False
|
|
4215
|
+
except (PermissionError, OSError):
|
|
4216
|
+
verdict("REFUSED")
|
|
4217
|
+
if observed_ppid != (ppid if recorded_parent_alive else 1):
|
|
4218
|
+
verdict("REFUSED")
|
|
4219
|
+
|
|
4220
|
+
process_started_at = int(time.mktime(time.strptime(fields[2], "%a %b %d %H:%M:%S %Y")))
|
|
4221
|
+
# Upper bound rejects a pid reused AFTER registration. Lower bound rejects a
|
|
4222
|
+
# forged entry aimed at an unrelated process that was already running when
|
|
4223
|
+
# the entry was written. Both halves are required: with only the upper bound
|
|
4224
|
+
# any live same-uid pid older than `started` is signalable.
|
|
4225
|
+
if not (registered_at - SLACK <= process_started_at <= registered_at + 1):
|
|
4226
|
+
verdict("REFUSED")
|
|
4227
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError, subprocess.SubprocessError):
|
|
4228
|
+
verdict("REFUSED")
|
|
4229
|
+
|
|
4230
|
+
verdict("OWNED")
|
|
4231
|
+
PY
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4067
4234
|
# Kill orphaned processes from crashed sessions
|
|
4068
4235
|
cmd_cleanup() {
|
|
4069
4236
|
# v7.6.2 B-13 fix: --help must print help, not run cleanup as a side effect.
|
|
@@ -4086,6 +4253,7 @@ cmd_cleanup() {
|
|
|
4086
4253
|
local pids_dir="$LOKI_DIR/pids"
|
|
4087
4254
|
local killed=0
|
|
4088
4255
|
local stale=0
|
|
4256
|
+
local refused=0
|
|
4089
4257
|
|
|
4090
4258
|
if [ ! -d "$pids_dir" ]; then
|
|
4091
4259
|
echo "No PID registry found. Nothing to clean up."
|
|
@@ -4102,6 +4270,27 @@ cmd_cleanup() {
|
|
|
4102
4270
|
''|*[!0-9]*) continue ;;
|
|
4103
4271
|
esac
|
|
4104
4272
|
|
|
4273
|
+
local registry_state
|
|
4274
|
+
registry_state=$(_cleanup_registry_entry_state "$entry_file" "$pid")
|
|
4275
|
+
case "$registry_state" in
|
|
4276
|
+
STALE)
|
|
4277
|
+
rm -f "$entry_file"
|
|
4278
|
+
stale=$((stale + 1))
|
|
4279
|
+
continue
|
|
4280
|
+
;;
|
|
4281
|
+
OWNED) ;;
|
|
4282
|
+
UNAVAILABLE)
|
|
4283
|
+
echo -e " ${YELLOW}Refusing${NC} PID=$pid (python3 unavailable: ownership cannot be verified)"
|
|
4284
|
+
refused=$((refused + 1))
|
|
4285
|
+
continue
|
|
4286
|
+
;;
|
|
4287
|
+
*)
|
|
4288
|
+
echo -e " ${YELLOW}Refusing${NC} PID=$pid (registry ownership or launch identity is not valid)"
|
|
4289
|
+
refused=$((refused + 1))
|
|
4290
|
+
continue
|
|
4291
|
+
;;
|
|
4292
|
+
esac
|
|
4293
|
+
|
|
4105
4294
|
local label=""
|
|
4106
4295
|
local ppid_val=""
|
|
4107
4296
|
# Parse JSON fields (python3 with shell fallback)
|
|
@@ -4113,50 +4302,61 @@ cmd_cleanup() {
|
|
|
4113
4302
|
ppid_val=$(sed 's/.*"ppid"://' "$entry_file" 2>/dev/null | sed 's/[,}].*//' | head -1) || true
|
|
4114
4303
|
fi
|
|
4115
4304
|
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4305
|
+
# The ownership gate above proved this exact live process matches the
|
|
4306
|
+
# current-project registry launch token. Check only whether its exact
|
|
4307
|
+
# recorded parent is gone before normal orphan cleanup.
|
|
4308
|
+
local is_orphan=false
|
|
4309
|
+
case "$ppid_val" in ''|*[!0-9]*) ppid_val="" ;; esac
|
|
4310
|
+
if [ -n "$ppid_val" ] && ! kill -0 "$ppid_val" 2>/dev/null; then
|
|
4311
|
+
is_orphan=true
|
|
4312
|
+
fi
|
|
4313
|
+
|
|
4314
|
+
if [ "$is_orphan" = true ] || [ "${1:-}" = "--force" ]; then
|
|
4315
|
+
# Close the validation-to-signal window as far as this portable PID
|
|
4316
|
+
# registry permits. Re-check again after TERM before any KILL so a
|
|
4317
|
+
# rapidly reused PID is never escalated under the old identity.
|
|
4318
|
+
registry_state=$(_cleanup_registry_entry_state "$entry_file" "$pid")
|
|
4319
|
+
if [ "$registry_state" != "OWNED" ]; then
|
|
4320
|
+
echo -e " ${YELLOW}Refusing${NC} PID=$pid (launch identity changed before signalling)"
|
|
4321
|
+
refused=$((refused + 1))
|
|
4322
|
+
continue
|
|
4323
|
+
fi
|
|
4324
|
+
echo -e " ${RED}Killing${NC} PID=$pid label=$label (parent $ppid_val dead)"
|
|
4325
|
+
if ! kill "$pid" 2>/dev/null; then
|
|
4326
|
+
echo -e " ${YELLOW}Refusing${NC} PID=$pid (TERM could not be delivered)"
|
|
4327
|
+
refused=$((refused + 1))
|
|
4328
|
+
continue
|
|
4136
4329
|
fi
|
|
4330
|
+
sleep 0.5
|
|
4331
|
+
registry_state=$(_cleanup_registry_entry_state "$entry_file" "$pid")
|
|
4332
|
+
case "$registry_state" in
|
|
4333
|
+
STALE)
|
|
4334
|
+
rm -f "$entry_file"
|
|
4335
|
+
killed=$((killed + 1))
|
|
4336
|
+
;;
|
|
4337
|
+
OWNED)
|
|
4338
|
+
if kill -9 "$pid" 2>/dev/null; then
|
|
4339
|
+
rm -f "$entry_file"
|
|
4340
|
+
killed=$((killed + 1))
|
|
4341
|
+
else
|
|
4342
|
+
echo -e " ${YELLOW}Refusing${NC} PID=$pid (KILL could not be delivered)"
|
|
4343
|
+
refused=$((refused + 1))
|
|
4344
|
+
fi
|
|
4345
|
+
;;
|
|
4346
|
+
*)
|
|
4347
|
+
echo -e " ${YELLOW}Refusing${NC} PID=$pid (launch identity changed before escalation)"
|
|
4348
|
+
refused=$((refused + 1))
|
|
4349
|
+
;;
|
|
4350
|
+
esac
|
|
4137
4351
|
else
|
|
4138
|
-
|
|
4139
|
-
rm -f "$entry_file"
|
|
4140
|
-
stale=$((stale + 1))
|
|
4352
|
+
echo -e " ${GREEN}Alive${NC} PID=$pid label=$label (parent $ppid_val alive)"
|
|
4141
4353
|
fi
|
|
4142
4354
|
done
|
|
4143
4355
|
|
|
4144
4356
|
echo ""
|
|
4145
|
-
echo "Results: $killed orphan(s) killed, $stale stale entries cleaned"
|
|
4146
|
-
|
|
4147
|
-
# Also kill orphaned loki-run temp scripts
|
|
4148
|
-
local temp_killed=0
|
|
4149
|
-
if pgrep -f "loki-run-" >/dev/null 2>&1; then
|
|
4150
|
-
if ! is_session_running; then
|
|
4151
|
-
echo "Killing orphaned loki-run temp scripts..."
|
|
4152
|
-
pkill -f "loki-run-" 2>/dev/null || true
|
|
4153
|
-
sleep 0.5
|
|
4154
|
-
pkill -9 -f "loki-run-" 2>/dev/null || true
|
|
4155
|
-
temp_killed=1
|
|
4156
|
-
fi
|
|
4157
|
-
fi
|
|
4357
|
+
echo "Results: $killed orphan(s) killed, $stale stale entries cleaned, $refused entry(s) refused"
|
|
4158
4358
|
|
|
4159
|
-
if [ $killed -eq 0 ] && [ $stale -eq 0 ] && [ $
|
|
4359
|
+
if [ $killed -eq 0 ] && [ $stale -eq 0 ] && [ $refused -eq 0 ]; then
|
|
4160
4360
|
echo -e "${GREEN}System is clean. No orphans found.${NC}"
|
|
4161
4361
|
fi
|
|
4162
4362
|
}
|
|
@@ -4840,7 +5040,14 @@ cmd_ship() {
|
|
|
4840
5040
|
echo -e "${BOLD}Running quality gates on the diff (loki review)...${NC}"
|
|
4841
5041
|
echo ""
|
|
4842
5042
|
local rc=0
|
|
4843
|
-
|
|
5043
|
+
# Bash 3.2 treats an empty-array expansion as an unbound variable under
|
|
5044
|
+
# `set -u`. macOS still ships that shell, so the documented zero-option
|
|
5045
|
+
# `loki ship` path must call review without expanding an empty array.
|
|
5046
|
+
if [ "${#review_args[@]}" -gt 0 ]; then
|
|
5047
|
+
cmd_review "${review_args[@]}" || rc=$?
|
|
5048
|
+
else
|
|
5049
|
+
cmd_review || rc=$?
|
|
5050
|
+
fi
|
|
4844
5051
|
|
|
4845
5052
|
if [ "$rc" -ge 1 ]; then
|
|
4846
5053
|
echo ""
|
|
@@ -5476,9 +5683,12 @@ cmd_status() {
|
|
|
5476
5683
|
cline)
|
|
5477
5684
|
capability="near-full mode"
|
|
5478
5685
|
;;
|
|
5686
|
+
opencode)
|
|
5687
|
+
capability="model-agnostic mode"
|
|
5688
|
+
;;
|
|
5479
5689
|
esac
|
|
5480
5690
|
echo -e "${CYAN}Provider:${NC} $current_provider ($capability)"
|
|
5481
|
-
echo -e "${DIM} Switch with: loki provider set <claude|codex|cline|aider>${NC}"
|
|
5691
|
+
echo -e "${DIM} Switch with: loki provider set <claude|codex|cline|aider|opencode>${NC}"
|
|
5482
5692
|
echo ""
|
|
5483
5693
|
|
|
5484
5694
|
# Show running sessions (v6.4.0 - concurrent session support)
|
|
@@ -5679,7 +5889,7 @@ def _read_cli_provider(loki_dir):
|
|
|
5679
5889
|
return None, False
|
|
5680
5890
|
prov = parts[0]
|
|
5681
5891
|
ts = int(parts[1])
|
|
5682
|
-
if prov not in ('claude', 'codex', 'cline', 'aider'):
|
|
5892
|
+
if prov not in ('claude', 'codex', 'cline', 'aider', 'opencode'):
|
|
5683
5893
|
return None, False
|
|
5684
5894
|
if time.time() - ts > 86400:
|
|
5685
5895
|
return None, False
|
|
@@ -6338,6 +6548,9 @@ cmd_provider_show() {
|
|
|
6338
6548
|
codex|aider)
|
|
6339
6549
|
echo -e "${YELLOW}Status:${NC} Degraded mode (sequential only)"
|
|
6340
6550
|
;;
|
|
6551
|
+
opencode)
|
|
6552
|
+
echo -e "${GREEN}Status:${NC} Model-agnostic mode (75+ providers, MCP)"
|
|
6553
|
+
;;
|
|
6341
6554
|
esac
|
|
6342
6555
|
|
|
6343
6556
|
if [ -n "$saved_provider" ]; then
|
|
@@ -6356,7 +6569,7 @@ cmd_provider_set() {
|
|
|
6356
6569
|
|
|
6357
6570
|
if [ -z "$new_provider" ]; then
|
|
6358
6571
|
echo -e "${RED}Error: Provider name required${NC}"
|
|
6359
|
-
echo "Usage: loki provider set <claude|codex|cline|aider>"
|
|
6572
|
+
echo "Usage: loki provider set <claude|codex|cline|aider|opencode>"
|
|
6360
6573
|
exit 2
|
|
6361
6574
|
fi
|
|
6362
6575
|
|
|
@@ -6364,14 +6577,14 @@ cmd_provider_set() {
|
|
|
6364
6577
|
case "$new_provider" in
|
|
6365
6578
|
gemini)
|
|
6366
6579
|
echo -e "${RED}Error: Provider 'gemini' is deprecated as of v7.5.18 and has been removed.${NC}"
|
|
6367
|
-
echo "Active providers: claude, codex, cline, aider"
|
|
6580
|
+
echo "Active providers: claude, codex, cline, aider, opencode"
|
|
6368
6581
|
exit 1
|
|
6369
6582
|
;;
|
|
6370
|
-
claude|codex|cline|aider)
|
|
6583
|
+
claude|codex|cline|aider|opencode)
|
|
6371
6584
|
;;
|
|
6372
6585
|
*)
|
|
6373
6586
|
echo -e "${RED}Error: Invalid provider '$new_provider'${NC}"
|
|
6374
|
-
echo "Valid providers: claude, codex, cline, aider"
|
|
6587
|
+
echo "Valid providers: claude, codex, cline, aider, opencode"
|
|
6375
6588
|
exit 1
|
|
6376
6589
|
;;
|
|
6377
6590
|
esac
|
|
@@ -6393,6 +6606,9 @@ cmd_provider_set() {
|
|
|
6393
6606
|
aider)
|
|
6394
6607
|
echo "Install: pip install aider-chat"
|
|
6395
6608
|
;;
|
|
6609
|
+
opencode)
|
|
6610
|
+
echo "Install: npm install -g opencode-ai"
|
|
6611
|
+
;;
|
|
6396
6612
|
esac
|
|
6397
6613
|
echo ""
|
|
6398
6614
|
echo -e "${DIM}Setting provider anyway...${NC}"
|
|
@@ -6531,6 +6747,20 @@ cmd_provider_info() {
|
|
|
6531
6747
|
echo ""
|
|
6532
6748
|
echo "Status: Degraded mode (Tier 3)"
|
|
6533
6749
|
;;
|
|
6750
|
+
opencode)
|
|
6751
|
+
echo "Name: opencode (75+ Providers)"
|
|
6752
|
+
echo "Vendor: opencode (model-agnostic registry)"
|
|
6753
|
+
echo "CLI: opencode"
|
|
6754
|
+
echo "Flag: run --auto"
|
|
6755
|
+
echo ""
|
|
6756
|
+
echo "Features:"
|
|
6757
|
+
echo " - Autonomous mode"
|
|
6758
|
+
echo " - 75+ model providers and custom endpoints"
|
|
6759
|
+
echo " - Local model support"
|
|
6760
|
+
echo " - MCP server support"
|
|
6761
|
+
echo ""
|
|
6762
|
+
echo "Status: Model-agnostic mode"
|
|
6763
|
+
;;
|
|
6534
6764
|
*)
|
|
6535
6765
|
echo -e "${RED}Unknown provider: $provider${NC}"
|
|
6536
6766
|
exit 1
|
|
@@ -9871,7 +10101,7 @@ cmd_run() {
|
|
|
9871
10101
|
echo " --dry-run Preview generated PRD without starting"
|
|
9872
10102
|
echo " --no-start Generate PRD but don't start execution"
|
|
9873
10103
|
echo " --output FILE Save PRD to custom path"
|
|
9874
|
-
echo " --provider NAME AI provider: claude (default), codex, cline, aider"
|
|
10104
|
+
echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
|
|
9875
10105
|
echo " --parallel Enable parallel mode with git worktrees"
|
|
9876
10106
|
echo " --bg, --background Run in background mode"
|
|
9877
10107
|
echo " --simple Force simple complexity tier"
|
|
@@ -9946,7 +10176,7 @@ cmd_run() {
|
|
|
9946
10176
|
# v7.7.11 (council fix): validate against canonical set; include
|
|
9947
10177
|
# PID so reader can drop the marker when originating process is gone.
|
|
9948
10178
|
case "$2" in
|
|
9949
|
-
claude|codex|cline|aider)
|
|
10179
|
+
claude|codex|cline|aider|opencode)
|
|
9950
10180
|
mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
|
|
9951
10181
|
printf '%s:%s:%s\n' "$2" "$(date +%s)" "$$" \
|
|
9952
10182
|
> "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
|
|
@@ -9966,7 +10196,7 @@ cmd_run() {
|
|
|
9966
10196
|
# UT2-13: Record CLI provider at parse time (atomic write).
|
|
9967
10197
|
# v7.7.11 (council fix): validate + include PID.
|
|
9968
10198
|
case "$provider_override" in
|
|
9969
|
-
claude|codex|cline|aider)
|
|
10199
|
+
claude|codex|cline|aider|opencode)
|
|
9970
10200
|
mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
|
|
9971
10201
|
printf '%s:%s:%s\n' "$provider_override" "$(date +%s)" "$$" \
|
|
9972
10202
|
> "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
|
|
@@ -10597,7 +10827,7 @@ cmd_issue() {
|
|
|
10597
10827
|
echo " --output FILE Save PRD to custom path (default: .loki/prd-issue-N.md)"
|
|
10598
10828
|
echo ""
|
|
10599
10829
|
echo "Options passed to 'start' (when --start is used):"
|
|
10600
|
-
echo " --provider NAME AI provider: claude (default), codex, cline, aider"
|
|
10830
|
+
echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
|
|
10601
10831
|
echo " --parallel Enable parallel mode with git worktrees"
|
|
10602
10832
|
echo " --bg, --background Run in background mode"
|
|
10603
10833
|
echo ""
|
|
@@ -11994,7 +12224,7 @@ cmd_config() {
|
|
|
11994
12224
|
echo " model.planning Model for planning tier"
|
|
11995
12225
|
echo " model.development Model for development tier"
|
|
11996
12226
|
echo " model.fast Model for fast tier"
|
|
11997
|
-
echo " provider Default AI provider: claude, codex, cline, aider"
|
|
12227
|
+
echo " provider Default AI provider: claude, codex, cline, aider, opencode"
|
|
11998
12228
|
echo " issue.provider Default issue provider: github, gitlab, jira, azure_devops"
|
|
11999
12229
|
echo " blind_validation Blind validation mode: true, false (default: true)"
|
|
12000
12230
|
echo " adversarial_testing Adversarial testing: true, false (default: true)"
|
|
@@ -12055,11 +12285,11 @@ cmd_config_set() {
|
|
|
12055
12285
|
provider)
|
|
12056
12286
|
case "$value" in
|
|
12057
12287
|
gemini)
|
|
12058
|
-
echo -e "${RED}Invalid provider: 'gemini' is deprecated as of v7.5.18. Active providers: claude, codex, cline, aider${NC}"
|
|
12288
|
+
echo -e "${RED}Invalid provider: 'gemini' is deprecated as of v7.5.18. Active providers: claude, codex, cline, aider, opencode${NC}"
|
|
12059
12289
|
return 1
|
|
12060
12290
|
;;
|
|
12061
|
-
claude|codex|cline|aider) ;;
|
|
12062
|
-
*) echo -e "${RED}Invalid provider: $value (expected: claude, codex, cline, aider)${NC}"; return 1 ;;
|
|
12291
|
+
claude|codex|cline|aider|opencode) ;;
|
|
12292
|
+
*) echo -e "${RED}Invalid provider: $value (expected: claude, codex, cline, aider, opencode)${NC}"; return 1 ;;
|
|
12063
12293
|
esac
|
|
12064
12294
|
;;
|
|
12065
12295
|
issue.provider)
|
|
@@ -12476,11 +12706,20 @@ cmd_setup_skill() {
|
|
|
12476
12706
|
continue
|
|
12477
12707
|
fi
|
|
12478
12708
|
|
|
12479
|
-
#
|
|
12480
|
-
|
|
12709
|
+
# A dangling link is safe to repair. Any live path without SKILL.md is
|
|
12710
|
+
# operator-owned: `ln -sf SOURCE DIRECTORY` would otherwise create a
|
|
12711
|
+
# nested link and report success, while a regular file could be
|
|
12712
|
+
# replaced outright. Refuse both cases without changing the path.
|
|
12713
|
+
if [ -L "$target_dir" ] && [ ! -e "$target_dir" ]; then
|
|
12481
12714
|
rm -f "$target_dir"
|
|
12482
12715
|
fi
|
|
12483
12716
|
|
|
12717
|
+
if [ -e "$target_dir" ] || [ -L "$target_dir" ]; then
|
|
12718
|
+
echo -e " ${RED}FAIL${NC} $target_name ${DIM}($short_path exists without SKILL.md; move it aside and re-run)${NC}"
|
|
12719
|
+
failed=$((failed + 1))
|
|
12720
|
+
continue
|
|
12721
|
+
fi
|
|
12722
|
+
|
|
12484
12723
|
# Create parent directory
|
|
12485
12724
|
if [ ! -d "$parent_dir" ]; then
|
|
12486
12725
|
mkdir -p "$parent_dir" 2>/dev/null || true
|
|
@@ -12870,7 +13109,7 @@ cmd_doctor() {
|
|
|
12870
13109
|
echo " required egress remains."
|
|
12871
13110
|
echo ""
|
|
12872
13111
|
echo "Checks: node, python3, jq, git, curl, bash version,"
|
|
12873
|
-
echo "
|
|
13112
|
+
echo " provider CLIs, disk space, and cockpit render capability."
|
|
12874
13113
|
return 0
|
|
12875
13114
|
;;
|
|
12876
13115
|
*)
|
|
@@ -12970,6 +13209,9 @@ cmd_doctor() {
|
|
|
12970
13209
|
aider)
|
|
12971
13210
|
version=$(aider --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
12972
13211
|
;;
|
|
13212
|
+
opencode)
|
|
13213
|
+
version=$(opencode --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
13214
|
+
;;
|
|
12973
13215
|
esac
|
|
12974
13216
|
|
|
12975
13217
|
local version_display=""
|
|
@@ -13025,6 +13267,7 @@ cmd_doctor() {
|
|
|
13025
13267
|
codex) echo "npm install -g @openai/codex" ;;
|
|
13026
13268
|
cline) echo "npm install -g cline" ;;
|
|
13027
13269
|
aider) echo "pip install aider-chat" ;;
|
|
13270
|
+
opencode) echo "npm install -g opencode-ai" ;;
|
|
13028
13271
|
*) echo "" ;;
|
|
13029
13272
|
esac
|
|
13030
13273
|
}
|
|
@@ -13046,6 +13289,7 @@ cmd_doctor() {
|
|
|
13046
13289
|
doctor_check_provider "Codex CLI" codex || true
|
|
13047
13290
|
doctor_check_provider "Cline CLI" cline || true
|
|
13048
13291
|
doctor_check_provider "Aider CLI" aider || true
|
|
13292
|
+
doctor_check_provider "opencode CLI" opencode || true
|
|
13049
13293
|
|
|
13050
13294
|
# Check if at least one provider is installed (detect_any_provider is the
|
|
13051
13295
|
# shared helper from provider-offer.sh, extracted from this exact loop).
|
|
@@ -13681,6 +13925,7 @@ checks.append(check_tool('Claude CLI', 'claude', 'optional'))
|
|
|
13681
13925
|
checks.append(check_tool('Codex CLI', 'codex', 'optional'))
|
|
13682
13926
|
checks.append(check_tool('Cline CLI', 'cline', 'optional'))
|
|
13683
13927
|
checks.append(check_tool('Aider CLI', 'aider', 'optional'))
|
|
13928
|
+
checks.append(check_tool('opencode CLI', 'opencode', 'optional'))
|
|
13684
13929
|
|
|
13685
13930
|
# Disk space
|
|
13686
13931
|
disk_gb = None
|
|
@@ -13788,22 +14033,20 @@ _SKILL_ENTRIES = (
|
|
|
13788
14033
|
skills = []
|
|
13789
14034
|
for _sk_name, _sk_rel in _SKILL_ENTRIES:
|
|
13790
14035
|
_sk_dir = os.path.join(os.path.expanduser('~'), _sk_rel)
|
|
14036
|
+
# JSON is commonly archived or forwarded to automation. Keep the useful
|
|
14037
|
+
# user-relative location without exposing the machine-specific home path.
|
|
14038
|
+
_sk_json_path = '~/' + _sk_rel
|
|
13791
14039
|
if os.path.isfile(os.path.join(_sk_dir, 'SKILL.md')):
|
|
13792
|
-
skills.append({'name': _sk_name, 'path':
|
|
14040
|
+
skills.append({'name': _sk_name, 'path': _sk_json_path, 'status': 'pass',
|
|
13793
14041
|
'detail': None, 'required': 'required'})
|
|
13794
14042
|
elif os.path.islink(_sk_dir):
|
|
13795
14043
|
# islink is true for a DANGLING link (lstat-based), which is exactly
|
|
13796
14044
|
# the broken case the text path reports as FAIL.
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
except OSError:
|
|
13800
|
-
_sk_target = 'unknown'
|
|
13801
|
-
skills.append({'name': _sk_name, 'path': _sk_dir, 'status': 'fail',
|
|
13802
|
-
'detail': 'broken symlink -> ' + _sk_target
|
|
13803
|
-
+ '. Fix: loki setup-skill',
|
|
14045
|
+
skills.append({'name': _sk_name, 'path': _sk_json_path, 'status': 'fail',
|
|
14046
|
+
'detail': 'broken symlink. Fix: loki setup-skill',
|
|
13804
14047
|
'required': 'required'})
|
|
13805
14048
|
else:
|
|
13806
|
-
skills.append({'name': _sk_name, 'path':
|
|
14049
|
+
skills.append({'name': _sk_name, 'path': _sk_json_path, 'status': 'warn',
|
|
13807
14050
|
'detail': 'not found - run loki setup-skill',
|
|
13808
14051
|
'required': 'required'})
|
|
13809
14052
|
|
|
@@ -13824,7 +14067,7 @@ elif disk_status == 'fail': fail_count += 1
|
|
|
13824
14067
|
elif disk_status == 'warn': warn_count += 1
|
|
13825
14068
|
|
|
13826
14069
|
# AGGREGATE PROVIDER CHECK. Each provider CLI is individually optional --
|
|
13827
|
-
# you need Claude
|
|
14070
|
+
# you need one of Claude, Codex, Cline, Aider, or opencode -- so no single
|
|
13828
14071
|
# one can be marked required. But having NONE is a blocker, and the text
|
|
13829
14072
|
# path reports it as one (the No-AI-provider branch near autonomy/loki:11000).
|
|
13830
14073
|
#
|
|
@@ -13915,6 +14158,7 @@ result = {
|
|
|
13915
14158
|
}
|
|
13916
14159
|
|
|
13917
14160
|
print(json.dumps(result, indent=2))
|
|
14161
|
+
sys.exit(0 if result['summary']['ok'] else 1)
|
|
13918
14162
|
"
|
|
13919
14163
|
}
|
|
13920
14164
|
|
|
@@ -17268,8 +17512,11 @@ except Exception: pass
|
|
|
17268
17512
|
# shellcheck disable=SC2086
|
|
17269
17513
|
(cd "$codebase_path" && aider --message "$phase_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || phase_exit=$?
|
|
17270
17514
|
;;
|
|
17515
|
+
opencode)
|
|
17516
|
+
(cd "$codebase_path" && source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$phase_prompt" 2>&1) || phase_exit=$?
|
|
17517
|
+
;;
|
|
17271
17518
|
*)
|
|
17272
|
-
echo -e "${RED}Error: Unknown provider '${provider_name}'. Supported: claude, codex, cline, aider${NC}"
|
|
17519
|
+
echo -e "${RED}Error: Unknown provider '${provider_name}'. Supported: claude, codex, cline, aider, opencode${NC}"
|
|
17273
17520
|
phase_exit=1
|
|
17274
17521
|
;;
|
|
17275
17522
|
esac
|
|
@@ -18477,10 +18724,13 @@ except Exception: pass
|
|
|
18477
18724
|
# shellcheck disable=SC2086
|
|
18478
18725
|
(cd "$codebase_path" && aider --message "$heal_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || heal_exit=$?
|
|
18479
18726
|
;;
|
|
18727
|
+
opencode)
|
|
18728
|
+
(cd "$codebase_path" && source "$_LOKI_SCRIPT_DIR/../providers/opencode.sh" && provider_invoke "$heal_prompt" 2>&1) || heal_exit=$?
|
|
18729
|
+
;;
|
|
18480
18730
|
# BUG-HEAL-003: Unknown provider should error, not silently succeed
|
|
18481
18731
|
*)
|
|
18482
18732
|
echo -e "${RED}Error: Unknown provider: $provider${NC}"
|
|
18483
|
-
echo "Supported providers: claude, codex, cline, aider"
|
|
18733
|
+
echo "Supported providers: claude, codex, cline, aider, opencode"
|
|
18484
18734
|
return 1
|
|
18485
18735
|
;;
|
|
18486
18736
|
esac
|
|
@@ -19318,7 +19568,7 @@ with open(failover_file, 'w') as f: json.dump(d, f, indent=2)
|
|
|
19318
19568
|
local IFS=','
|
|
19319
19569
|
for p in $new_chain; do
|
|
19320
19570
|
case "$p" in
|
|
19321
|
-
claude|codex|cline|aider) ;;
|
|
19571
|
+
claude|codex|cline|aider|opencode) ;;
|
|
19322
19572
|
*) echo -e "${RED}Error: invalid provider '$p' in chain${NC}"; return 1 ;;
|
|
19323
19573
|
esac
|
|
19324
19574
|
done
|
|
@@ -20517,7 +20767,7 @@ main() {
|
|
|
20517
20767
|
# v7.5.18: early guard -- LOKI_PROVIDER=gemini is no longer supported.
|
|
20518
20768
|
if [ "${LOKI_PROVIDER:-}" = "gemini" ]; then
|
|
20519
20769
|
echo -e "${RED}Error: Provider 'gemini' is deprecated as of v7.5.18 and has been removed.${NC}" >&2
|
|
20520
|
-
echo "Active providers: claude, codex, cline, aider" >&2
|
|
20770
|
+
echo "Active providers: claude, codex, cline, aider, opencode" >&2
|
|
20521
20771
|
echo "Unset LOKI_PROVIDER or use: LOKI_PROVIDER=claude" >&2
|
|
20522
20772
|
exit 1
|
|
20523
20773
|
fi
|