omnilane 0.8.3 → 0.9.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/CHANGELOG.md +65 -1
- package/README.ja.md +147 -87
- package/README.ko.md +145 -86
- package/README.md +163 -90
- package/README.zh-CN.md +126 -78
- package/README.zh-TW.md +136 -74
- package/VERSION +1 -1
- package/bin/omnilane +4 -3
- package/bin/omnilane-mcp +291 -0
- package/completions/omnilane.fish +50 -0
- package/package.json +1 -1
- package/scripts/check.sh +125 -0
- package/scripts/configure.sh +151 -1
- package/scripts/dispatch.sh +4 -4
- package/scripts/doctor.sh +38 -0
- package/scripts/jobs.sh +132 -10
- package/scripts/lib/common.sh +57 -3
- package/scripts/runners/run-cerebras.sh +7 -0
- package/scripts/runners/run-deepseek.sh +7 -0
- package/scripts/runners/run-groq.sh +7 -0
- package/scripts/runners/run-mistral.sh +7 -0
- package/scripts/runners/run-openai-compat.sh +102 -0
- package/scripts/runners/run-openrouter.sh +5 -84
- package/scripts/runners/run-zai.sh +7 -0
package/scripts/configure.sh
CHANGED
|
@@ -9,6 +9,144 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib/i18n.sh"
|
|
|
9
9
|
|
|
10
10
|
LOCAL_FILE="$OMNILANE_HOME/routing.local.yaml"
|
|
11
11
|
|
|
12
|
+
# --- Non-interactive subcommands -------------------------------------------
|
|
13
|
+
# `configure set|get|unset|list` script the same routing.local.yaml the menu
|
|
14
|
+
# writes, so automation never needs a tty. No subcommand => interactive menu.
|
|
15
|
+
|
|
16
|
+
cfg_usage() {
|
|
17
|
+
cat >&2 <<'EOF'
|
|
18
|
+
usage: configure interactive lane menu
|
|
19
|
+
configure set LANE SPEC set/override one lane. SPEC = "vendor model effort [| ...]" or "off".
|
|
20
|
+
Quote the whole SPEC (and the model) when a model name has spaces.
|
|
21
|
+
configure get LANE show the effective routing line for LANE
|
|
22
|
+
configure unset LANE remove LANE's local override
|
|
23
|
+
configure list show current local overrides
|
|
24
|
+
configure diff show how local overrides change the effective table vs the defaults
|
|
25
|
+
EOF
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Reject shell-dangerous bytes while allowing the routing RHS grammar
|
|
29
|
+
# (chains with |, quoted "models with spaces", slashes, effort tokens).
|
|
30
|
+
cfg_spec_is_safe() {
|
|
31
|
+
case "$1" in
|
|
32
|
+
'') return 1 ;;
|
|
33
|
+
*'$'*|*'`'*|*'\'*|*'#'*|*';'*|*'&'*|*'<'*|*'>'*|*$'\r'*|*$'\n'*) return 1 ;;
|
|
34
|
+
*) return 0 ;;
|
|
35
|
+
esac
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cfg_lane_is_known() {
|
|
39
|
+
local want="$1" line
|
|
40
|
+
while IFS= read -r line; do
|
|
41
|
+
case "$line" in "$want":*) return 0 ;; esac
|
|
42
|
+
done < "$OMNILANE_REPO/routing.yaml"
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cfg_valid_lane() { [[ "$1" =~ ^[a-z][a-z0-9-]*$ ]]; }
|
|
47
|
+
|
|
48
|
+
cfg_set() {
|
|
49
|
+
local lane="${1:-}"; shift || true
|
|
50
|
+
local spec="$*"
|
|
51
|
+
cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
|
|
52
|
+
cfg_lane_is_known "$lane" || { echo "omnilane: unknown lane '$lane' (see: omnilane list)" >&2; exit 2; }
|
|
53
|
+
[[ -n "$spec" ]] || { echo "omnilane: missing routing spec for '$lane'" >&2; cfg_usage; exit 2; }
|
|
54
|
+
cfg_spec_is_safe "$spec" || { echo 'omnilane: unsafe routing spec (not allowed: $ ` \ # ; & < > newlines)' >&2; exit 2; }
|
|
55
|
+
|
|
56
|
+
mkdir -p "$OMNILANE_HOME"
|
|
57
|
+
local had_file=0
|
|
58
|
+
if [[ -f "$LOCAL_FILE" ]]; then had_file=1; cp "$LOCAL_FILE" "$LOCAL_FILE.bak"; fi
|
|
59
|
+
local tmp="$LOCAL_FILE.tmp.$$"
|
|
60
|
+
{
|
|
61
|
+
echo "# updated by 'configure set' on $(date +%F) — first match per lane wins"
|
|
62
|
+
echo "$lane: $spec"
|
|
63
|
+
# Drop only our own stamp line and the lane being replaced; the user's own
|
|
64
|
+
# comments in routing.local.yaml survive a set.
|
|
65
|
+
[[ "$had_file" -eq 1 ]] && grep -v "^# updated by 'configure set'" "$LOCAL_FILE.bak" | grep -v "^$lane:" || true
|
|
66
|
+
} > "$tmp"
|
|
67
|
+
mv "$tmp" "$LOCAL_FILE"
|
|
68
|
+
|
|
69
|
+
# Semantic guard: reject only a structural FAIL on THIS lane. Availability
|
|
70
|
+
# WARN (validate exit 4) is fine — the vendor CLI may be legitimately absent.
|
|
71
|
+
local validate_out
|
|
72
|
+
validate_out="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --validate 2>&1)" || true
|
|
73
|
+
if printf '%s\n' "$validate_out" | grep -q "^FAIL $lane "; then
|
|
74
|
+
if [[ "$had_file" -eq 1 ]]; then mv "$LOCAL_FILE.bak" "$LOCAL_FILE"; else /bin/rm -f "$LOCAL_FILE"; fi
|
|
75
|
+
echo "omnilane: rejected — $(printf '%s\n' "$validate_out" | grep "^FAIL $lane " | head -1)" >&2
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
[[ "$had_file" -eq 1 ]] && /bin/rm -f "$LOCAL_FILE.bak"
|
|
79
|
+
echo "set $lane -> $spec"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
cfg_get() {
|
|
83
|
+
local lane="${1:-}"
|
|
84
|
+
cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
|
|
85
|
+
local line
|
|
86
|
+
line="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null | grep "^$lane:" | head -1 || true)"
|
|
87
|
+
[[ -n "$line" ]] || { echo "omnilane: unknown lane '$lane' (see: omnilane list)" >&2; exit 2; }
|
|
88
|
+
printf '%s\n' "$line"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cfg_unset() {
|
|
92
|
+
local lane="${1:-}"
|
|
93
|
+
cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
|
|
94
|
+
if [[ ! -f "$LOCAL_FILE" ]] || ! grep -q "^$lane:" "$LOCAL_FILE"; then
|
|
95
|
+
echo "no local override for '$lane'"; return 0
|
|
96
|
+
fi
|
|
97
|
+
cp "$LOCAL_FILE" "$LOCAL_FILE.bak"
|
|
98
|
+
grep -v "^$lane:" "$LOCAL_FILE.bak" > "$LOCAL_FILE" || true
|
|
99
|
+
/bin/rm -f "$LOCAL_FILE.bak"
|
|
100
|
+
echo "unset $lane (local override removed)"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cfg_list() {
|
|
104
|
+
if [[ -f "$LOCAL_FILE" ]] && grep -qE '^[a-z]' "$LOCAL_FILE"; then
|
|
105
|
+
cat "$LOCAL_FILE"
|
|
106
|
+
else
|
|
107
|
+
echo "no local overrides in $LOCAL_FILE"
|
|
108
|
+
fi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Diff the effective table (local wins) against a defaults-only resolution, so
|
|
112
|
+
# the user sees exactly which lanes their overrides change. Reuses dispatch.sh
|
|
113
|
+
# --list for both, so availability annotation and formatting stay consistent; a
|
|
114
|
+
# throwaway empty OMNILANE_HOME yields the defaults-only table.
|
|
115
|
+
cfg_diff() {
|
|
116
|
+
if [[ ! -f "$LOCAL_FILE" ]] || ! grep -qE '^[a-z]' "$LOCAL_FILE"; then
|
|
117
|
+
echo "no local overrides ($LOCAL_FILE); effective table equals the defaults"
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
local empty eff def changed=0 lane eff_line def_line
|
|
121
|
+
empty="$(mktemp -d "${TMPDIR:-/tmp}/omnilane-diff.XXXXXX")"
|
|
122
|
+
eff="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null || true)"
|
|
123
|
+
def="$(OMNILANE_HOME="$empty" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null || true)"
|
|
124
|
+
/bin/rm -rf "$empty"
|
|
125
|
+
while IFS= read -r eff_line; do
|
|
126
|
+
[[ "$eff_line" =~ ^([a-z][a-z0-9-]*): ]] || continue
|
|
127
|
+
lane="${BASH_REMATCH[1]}"
|
|
128
|
+
def_line="$(printf '%s\n' "$def" | grep "^$lane:" | head -1 || true)"
|
|
129
|
+
if [[ "$eff_line" != "$def_line" ]]; then
|
|
130
|
+
changed=1
|
|
131
|
+
printf 'default> %s\n' "${def_line:-($lane not in defaults)}"
|
|
132
|
+
printf 'local > %s\n' "$eff_line"
|
|
133
|
+
echo
|
|
134
|
+
fi
|
|
135
|
+
done <<DIFF_EFF
|
|
136
|
+
$eff
|
|
137
|
+
DIFF_EFF
|
|
138
|
+
[[ "$changed" -eq 1 ]] || echo "local overrides present, but the effective table matches the defaults"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case "${1:-}" in
|
|
142
|
+
set) shift; cfg_set "$@"; exit $? ;;
|
|
143
|
+
get) shift; cfg_get "$@"; exit $? ;;
|
|
144
|
+
unset) shift; cfg_unset "$@"; exit $? ;;
|
|
145
|
+
list) shift; cfg_list "$@"; exit $? ;;
|
|
146
|
+
diff) cfg_diff; exit $? ;;
|
|
147
|
+
-h|--help|help) cfg_usage; exit 0 ;;
|
|
148
|
+
esac
|
|
149
|
+
|
|
12
150
|
# Curated suggestions only — "c" always allows free text so new models work.
|
|
13
151
|
CODEX_MODELS=("gpt-5.6-sol" "gpt-5.6-terra" "gpt-5.6-luna")
|
|
14
152
|
CODEX_EFFORTS=("xhigh" "max" "ultra" "high" "medium" "low" "minimal" "none")
|
|
@@ -21,6 +159,13 @@ QWEN_MODELS=("qwen3-coder-plus" "qwen3-coder-flash")
|
|
|
21
159
|
# OpenCode models use provider/model form; OpenRouter models are catalog slugs.
|
|
22
160
|
OPENCODE_MODELS=("openrouter/anthropic/claude-sonnet-5" "openrouter/openai/gpt-5.6-sol" "opencode/default (leave model to opencode)")
|
|
23
161
|
OPENROUTER_MODELS=("anthropic/claude-sonnet-5" "openai/gpt-5.6-sol" "moonshotai/kimi-k3" "qwen/qwen3-coder-plus")
|
|
162
|
+
# Direct-API OpenAI-compatible vendors (curl + <VENDOR>_API_KEY); slugs are
|
|
163
|
+
# suggestions — "c" free text covers anything each provider's /models lists.
|
|
164
|
+
DEEPSEEK_MODELS=("deepseek-chat" "deepseek-reasoner" "deepseek-v4-flash")
|
|
165
|
+
ZAI_MODELS=("glm-4.6")
|
|
166
|
+
MISTRAL_MODELS=("devstral-latest" "codestral-latest" "mistral-medium-latest")
|
|
167
|
+
GROQ_MODELS=("openai/gpt-oss-120b" "qwen/qwen3.6-27b" "openai/gpt-oss-20b")
|
|
168
|
+
CEREBRAS_MODELS=("gpt-oss-120b" "qwen-3-32b" "llama-3.3-70b")
|
|
24
169
|
|
|
25
170
|
custom_value_is_safe() {
|
|
26
171
|
case "$1" in
|
|
@@ -79,7 +224,7 @@ while true; do
|
|
|
79
224
|
echo "$(msgf cfg_pick_range "${#LANES[@]}")"; continue
|
|
80
225
|
fi
|
|
81
226
|
lane="${LANES[$((n - 1))]}"
|
|
82
|
-
vendor="$(pick "$(msgf cfg_vendor_for "$lane")" codex claude grok gemini kimi qwen opencode openrouter "vote (multi-model panel)" "exec (your own script/gate)" off)"
|
|
227
|
+
vendor="$(pick "$(msgf cfg_vendor_for "$lane")" codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras "vote (multi-model panel)" "exec (your own script/gate)" off)"
|
|
83
228
|
[[ "$vendor" == exec* ]] && vendor="exec"
|
|
84
229
|
[[ "$vendor" == vote* ]] && vendor="vote"
|
|
85
230
|
if [[ "$vendor" == "off" ]]; then
|
|
@@ -96,6 +241,11 @@ while true; do
|
|
|
96
241
|
opencode) model="$(pick "$(msg cfg_model)" "${OPENCODE_MODELS[@]}")"; effort="-"
|
|
97
242
|
[[ "$model" == "opencode/default"* ]] && model="-" ;;
|
|
98
243
|
openrouter) model="$(pick "$(msg cfg_model)" "${OPENROUTER_MODELS[@]}")"; effort="-" ;;
|
|
244
|
+
deepseek) model="$(pick "$(msg cfg_model)" "${DEEPSEEK_MODELS[@]}")"; effort="-" ;;
|
|
245
|
+
zai) model="$(pick "$(msg cfg_model)" "${ZAI_MODELS[@]}")"; effort="-" ;;
|
|
246
|
+
mistral) model="$(pick "$(msg cfg_model)" "${MISTRAL_MODELS[@]}")"; effort="-" ;;
|
|
247
|
+
groq) model="$(pick "$(msg cfg_model)" "${GROQ_MODELS[@]}")"; effort="-" ;;
|
|
248
|
+
cerebras) model="$(pick "$(msg cfg_model)" "${CEREBRAS_MODELS[@]}")"; effort="-" ;;
|
|
99
249
|
vote) while true; do
|
|
100
250
|
count="$(pick "$(msg cfg_voters_count)" "1" "2" "3" "4")"
|
|
101
251
|
[[ "$count" =~ ^[1-4]$ ]] && break
|
package/scripts/dispatch.sh
CHANGED
|
@@ -52,7 +52,7 @@ flags:
|
|
|
52
52
|
before any provider call or job state
|
|
53
53
|
--mode advise|work advise (read-only, default) or work (may edit files)
|
|
54
54
|
--workdir DIR working directory handed to the vendor CLI
|
|
55
|
-
--vendor V pin one configured vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter)
|
|
55
|
+
--vendor V pin one configured vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|deepseek|zai|mistral|groq|cerebras)
|
|
56
56
|
--model M override the routed model
|
|
57
57
|
--effort E override the routed effort
|
|
58
58
|
--timeout SECONDS cap each CLI call (default 600)
|
|
@@ -363,7 +363,7 @@ validate_routing() {
|
|
|
363
363
|
lane_invalid=1
|
|
364
364
|
break
|
|
365
365
|
fi
|
|
366
|
-
if ! [[ "$vendor" =~ ^(
|
|
366
|
+
if ! [[ "$vendor" =~ ^(${OMNILANE_VENDOR_ALT}|off)$ ]]; then
|
|
367
367
|
printf 'FAIL %s unknown-vendor=%s\n' "$lane" "$vendor"
|
|
368
368
|
lane_invalid=1
|
|
369
369
|
break
|
|
@@ -489,8 +489,8 @@ TASK="$2"
|
|
|
489
489
|
# A typo like --mode advice must not fall through to the write-enabled branch.
|
|
490
490
|
[[ "$MODE" == "advise" || "$MODE" == "work" ]] || { echo "omnilane: invalid --mode (advise|work)" >&2; exit 2; }
|
|
491
491
|
if [[ -n "$OVERRIDE_VENDOR" ]] &&
|
|
492
|
-
! [[ "$OVERRIDE_VENDOR" =~ ^(
|
|
493
|
-
echo "omnilane: invalid vendor (
|
|
492
|
+
! [[ "$OVERRIDE_VENDOR" =~ ^(${OMNILANE_OVERRIDE_VENDOR_ALT})$ ]]; then
|
|
493
|
+
echo "omnilane: invalid vendor (${OMNILANE_OVERRIDE_VENDOR_ALT})" >&2
|
|
494
494
|
exit 2
|
|
495
495
|
fi
|
|
496
496
|
|
package/scripts/doctor.sh
CHANGED
|
@@ -140,6 +140,44 @@ else
|
|
|
140
140
|
report FAIL watchdog "timeout, gtimeout, and perl are all unavailable"
|
|
141
141
|
fi
|
|
142
142
|
|
|
143
|
+
# Vendor CLI availability. Runners resolve each vendor's binary through a *_BIN
|
|
144
|
+
# override (default name otherwise) and source local.sh, so probe the same way
|
|
145
|
+
# in a subshell — set +u because a machine-local local.sh may reference its own
|
|
146
|
+
# unset vars, and the subshell keeps any side effects out of this report.
|
|
147
|
+
vendor_line="$(
|
|
148
|
+
set +u
|
|
149
|
+
[[ -f "$OMNILANE_HOME/local.sh" ]] && . "$OMNILANE_HOME/local.sh" 2>/dev/null
|
|
150
|
+
present=""; absent=""
|
|
151
|
+
for spec in "codex:${CODEX_BIN:-codex}" "claude:${CLAUDE_BIN:-claude}" \
|
|
152
|
+
"grok:${GROK_BIN:-grok}" "gemini:${AGY_BIN:-agy}" \
|
|
153
|
+
"kimi:${KIMI_BIN:-kimi}" "qwen:${QWEN_BIN:-qwen}" \
|
|
154
|
+
"opencode:${OPENCODE_BIN:-opencode}"; do
|
|
155
|
+
name="${spec%%:*}"; bin="${spec#*:}"
|
|
156
|
+
if command -v "$bin" >/dev/null 2>&1; then present="$present $name"; else absent="$absent $name"; fi
|
|
157
|
+
done
|
|
158
|
+
# Direct-API vendors (OpenAI-compatible, no CLI): reachable iff curl exists
|
|
159
|
+
# and the matching API key is set. Keep aligned with
|
|
160
|
+
# OMNILANE_DIRECT_API_VENDORS in lib/common.sh.
|
|
161
|
+
if command -v curl >/dev/null 2>&1; then
|
|
162
|
+
for spec in "openrouter:${OPENROUTER_API_KEY:-}" "deepseek:${DEEPSEEK_API_KEY:-}" \
|
|
163
|
+
"zai:${ZAI_API_KEY:-}" "mistral:${MISTRAL_API_KEY:-}" \
|
|
164
|
+
"groq:${GROQ_API_KEY:-}" "cerebras:${CEREBRAS_API_KEY:-}"; do
|
|
165
|
+
name="${spec%%:*}"; key="${spec#*:}"
|
|
166
|
+
if [[ -n "$key" ]]; then present="$present $name"; else absent="$absent $name"; fi
|
|
167
|
+
done
|
|
168
|
+
else
|
|
169
|
+
absent="$absent openrouter deepseek zai mistral groq cerebras"
|
|
170
|
+
fi
|
|
171
|
+
printf '%s|%s' "${present# }" "${absent# }"
|
|
172
|
+
)"
|
|
173
|
+
vendor_present="${vendor_line%%|*}"
|
|
174
|
+
vendor_absent="${vendor_line#*|}"
|
|
175
|
+
if [[ -n "$vendor_present" ]]; then
|
|
176
|
+
report PASS vendors "present: $vendor_present${vendor_absent:+; missing: $vendor_absent}"
|
|
177
|
+
else
|
|
178
|
+
report WARN vendors "no vendor CLI reachable${vendor_absent:+ (missing: $vendor_absent)}; every lane degrades to off"
|
|
179
|
+
fi
|
|
180
|
+
|
|
143
181
|
if command -v python3 >/dev/null 2>&1; then
|
|
144
182
|
if python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' \
|
|
145
183
|
>/dev/null 2>&1; then
|
package/scripts/jobs.sh
CHANGED
|
@@ -48,7 +48,7 @@ die() {
|
|
|
48
48
|
exit "$rc"
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
USAGE_TEXT="usage: jobs.sh [--json] list|status ID|result ID|tail ID [--lines N]|retry ID [--background]|stats [--last N]|wait ID [--timeout N]|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
51
|
+
USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done]|status ID|result ID|tail ID [--lines N]|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|wait ID [--timeout N]|cancel ID|rm ID|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
52
52
|
|
|
53
53
|
usage() {
|
|
54
54
|
die 2 "$USAGE_TEXT"
|
|
@@ -132,7 +132,7 @@ read_job_pid() {
|
|
|
132
132
|
|
|
133
133
|
parse_stats_metadata() {
|
|
134
134
|
local value="$1"
|
|
135
|
-
local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"(
|
|
135
|
+
local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"('"$OMNILANE_VENDOR_ALT"')"(,|})'
|
|
136
136
|
STATS_LANE=""
|
|
137
137
|
STATS_VENDOR=""
|
|
138
138
|
[[ "$value" =~ $metadata_re ]] || return 1
|
|
@@ -143,7 +143,7 @@ parse_stats_metadata() {
|
|
|
143
143
|
parse_audit_metadata() {
|
|
144
144
|
local value="$1" metadata_re
|
|
145
145
|
local json_string='([^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*'
|
|
146
|
-
metadata_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(
|
|
146
|
+
metadata_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work)\",\"workdir\":\"${json_string}\",\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"\\}$"
|
|
147
147
|
[[ "$value" =~ $metadata_re ]]
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -252,7 +252,7 @@ case "${1:-}" in
|
|
|
252
252
|
read_public_metadata "$JOB_DIR/meta.json" || {
|
|
253
253
|
die 1 "cannot retry: unreadable job metadata"
|
|
254
254
|
}
|
|
255
|
-
retry_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"(
|
|
255
|
+
retry_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"('"$OMNILANE_VENDOR_ALT"')","model":"([^"\\]*)","effort":"([^"\\]*)","timeout":([1-9][0-9]*),"job_timeout":([1-9][0-9]*|null),"mode":"(advise|work)","workdir":"([^"\\]*)",'
|
|
256
256
|
[[ "$PUBLIC_METADATA" =~ $retry_re ]] || {
|
|
257
257
|
die 1 "cannot retry: job metadata is not safely parseable"
|
|
258
258
|
}
|
|
@@ -420,18 +420,27 @@ case "${1:-}" in
|
|
|
420
420
|
[[ "$findings" -eq 0 ]] || exit 1 ;;
|
|
421
421
|
stats)
|
|
422
422
|
limit=100
|
|
423
|
+
filter_lane=""; filter_vendor=""
|
|
423
424
|
shift
|
|
424
425
|
while [[ $# -gt 0 ]]; do
|
|
425
426
|
case "$1" in
|
|
426
427
|
--last)
|
|
427
428
|
[[ $# -ge 2 ]] || usage
|
|
428
429
|
limit="$2"; shift 2 ;;
|
|
430
|
+
--lane)
|
|
431
|
+
[[ $# -ge 2 ]] || usage
|
|
432
|
+
filter_lane="$2"; shift 2 ;;
|
|
433
|
+
--vendor)
|
|
434
|
+
[[ $# -ge 2 ]] || usage
|
|
435
|
+
filter_vendor="$2"; shift 2 ;;
|
|
429
436
|
*) usage ;;
|
|
430
437
|
esac
|
|
431
438
|
done
|
|
432
439
|
[[ "$limit" =~ ^[1-9][0-9]{0,3}$ && "$limit" -le 10000 ]] || {
|
|
433
440
|
die 2 "invalid --last value (want 1..10000)"
|
|
434
441
|
}
|
|
442
|
+
[[ -z "$filter_lane" || "$filter_lane" =~ ^[a-z][a-z0-9-]*$ ]] || die 2 "invalid --lane value"
|
|
443
|
+
[[ -z "$filter_vendor" ]] || omnilane_known_vendor "$filter_vendor" || die 2 "invalid --vendor value"
|
|
435
444
|
sampled=0
|
|
436
445
|
succeeded=0
|
|
437
446
|
failed=0
|
|
@@ -451,8 +460,21 @@ case "${1:-}" in
|
|
|
451
460
|
fi
|
|
452
461
|
if [[ ${#ids[@]} -gt 0 ]]; then
|
|
453
462
|
while IFS= read -r id; do
|
|
454
|
-
sampled=$((sampled + 1))
|
|
455
463
|
job_dir="$JOBS/$id"
|
|
464
|
+
# Resolve lane/vendor first so a --lane/--vendor filter can exclude a job
|
|
465
|
+
# before it counts toward any aggregate.
|
|
466
|
+
meta_ok=0
|
|
467
|
+
metadata_path="$job_dir/meta.json"
|
|
468
|
+
if read_public_metadata "$metadata_path" &&
|
|
469
|
+
parse_stats_metadata "$PUBLIC_METADATA"; then
|
|
470
|
+
meta_ok=1
|
|
471
|
+
fi
|
|
472
|
+
if [[ -n "$filter_lane" || -n "$filter_vendor" ]]; then
|
|
473
|
+
[[ "$meta_ok" -eq 1 ]] || continue
|
|
474
|
+
[[ -z "$filter_lane" || "$filter_lane" == "$STATS_LANE" ]] || continue
|
|
475
|
+
[[ -z "$filter_vendor" || "$filter_vendor" == "$STATS_VENDOR" ]] || continue
|
|
476
|
+
fi
|
|
477
|
+
sampled=$((sampled + 1))
|
|
456
478
|
exit_path="$job_dir/exit"
|
|
457
479
|
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
458
480
|
if read_exit_code "$exit_path"; then
|
|
@@ -467,9 +489,7 @@ case "${1:-}" in
|
|
|
467
489
|
else
|
|
468
490
|
running=$((running + 1))
|
|
469
491
|
fi
|
|
470
|
-
|
|
471
|
-
if read_public_metadata "$metadata_path" &&
|
|
472
|
-
parse_stats_metadata "$PUBLIC_METADATA"; then
|
|
492
|
+
if [[ "$meta_ok" -eq 1 ]]; then
|
|
473
493
|
lanes+=("$STATS_LANE")
|
|
474
494
|
vendors+=("$STATS_VENDOR")
|
|
475
495
|
else
|
|
@@ -501,7 +521,22 @@ case "${1:-}" in
|
|
|
501
521
|
fi
|
|
502
522
|
fi ;;
|
|
503
523
|
list)
|
|
504
|
-
|
|
524
|
+
filter_lane=""; filter_vendor=""; filter_status=""
|
|
525
|
+
shift
|
|
526
|
+
while [[ $# -gt 0 ]]; do
|
|
527
|
+
case "$1" in
|
|
528
|
+
--lane) [[ $# -ge 2 ]] || usage; filter_lane="$2"; shift 2 ;;
|
|
529
|
+
--vendor) [[ $# -ge 2 ]] || usage; filter_vendor="$2"; shift 2 ;;
|
|
530
|
+
--status) [[ $# -ge 2 ]] || usage; filter_status="$2"; shift 2 ;;
|
|
531
|
+
*) usage ;;
|
|
532
|
+
esac
|
|
533
|
+
done
|
|
534
|
+
[[ -z "$filter_lane" || "$filter_lane" =~ ^[a-z][a-z0-9-]*$ ]] || die 2 "invalid --lane value"
|
|
535
|
+
[[ -z "$filter_vendor" ]] || omnilane_known_vendor "$filter_vendor" || die 2 "invalid --vendor value"
|
|
536
|
+
case "$filter_status" in
|
|
537
|
+
""|running|done) ;;
|
|
538
|
+
*) die 2 "invalid --status value (want running or done)" ;;
|
|
539
|
+
esac
|
|
505
540
|
if [[ ! -d "$JOBS" ]]; then
|
|
506
541
|
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":[]}\n'
|
|
507
542
|
exit 0
|
|
@@ -519,6 +554,7 @@ case "${1:-}" in
|
|
|
519
554
|
fi
|
|
520
555
|
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":['
|
|
521
556
|
json_first=1
|
|
557
|
+
shown=0
|
|
522
558
|
while IFS= read -r id; do
|
|
523
559
|
state="running"
|
|
524
560
|
exit_json="null"
|
|
@@ -555,6 +591,21 @@ case "${1:-}" in
|
|
|
555
591
|
elif [[ -e "$metadata_path" || -L "$metadata_path" ]]; then
|
|
556
592
|
metadata_status="invalid"
|
|
557
593
|
fi
|
|
594
|
+
if [[ -n "$filter_status" ]]; then
|
|
595
|
+
case "$json_state" in
|
|
596
|
+
done) [[ "$filter_status" == "done" ]] || continue ;;
|
|
597
|
+
running) [[ "$filter_status" == "running" ]] || continue ;;
|
|
598
|
+
*) continue ;;
|
|
599
|
+
esac
|
|
600
|
+
fi
|
|
601
|
+
if [[ -n "$filter_lane" || -n "$filter_vendor" ]]; then
|
|
602
|
+
if [[ "$metadata_status" == "valid" ]] && parse_stats_metadata "$metadata"; then
|
|
603
|
+
[[ -z "$filter_lane" || "$filter_lane" == "$STATS_LANE" ]] || continue
|
|
604
|
+
[[ -z "$filter_vendor" || "$filter_vendor" == "$STATS_VENDOR" ]] || continue
|
|
605
|
+
else
|
|
606
|
+
continue
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
558
609
|
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
559
610
|
[[ "$json_first" -eq 1 ]] || printf ','
|
|
560
611
|
json_first=0
|
|
@@ -563,7 +614,9 @@ case "${1:-}" in
|
|
|
563
614
|
else
|
|
564
615
|
printf '%s %s %s\n' "$id" "$state" "$metadata"
|
|
565
616
|
fi
|
|
566
|
-
|
|
617
|
+
shown=$((shown + 1))
|
|
618
|
+
if [[ "$shown" -ge 20 ]]; then break; fi
|
|
619
|
+
done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r)
|
|
567
620
|
[[ "$JSON_MODE" -eq 0 ]] || printf ']}\n' ;;
|
|
568
621
|
status)
|
|
569
622
|
[[ $# -eq 2 ]] || usage
|
|
@@ -672,6 +725,75 @@ case "${1:-}" in
|
|
|
672
725
|
fi
|
|
673
726
|
sleep 1
|
|
674
727
|
done ;;
|
|
728
|
+
cancel)
|
|
729
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
|
|
730
|
+
select_job "$2"
|
|
731
|
+
exit_path="$JOB_DIR/exit"
|
|
732
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
733
|
+
if read_exit_code "$exit_path"; then
|
|
734
|
+
echo "already finished (exit $RECORDED_EXIT)"
|
|
735
|
+
else
|
|
736
|
+
echo "already finished (invalid exit metadata)"
|
|
737
|
+
fi
|
|
738
|
+
exit 0
|
|
739
|
+
fi
|
|
740
|
+
pid=""; pid_state="missing"
|
|
741
|
+
if [[ -e "$JOB_DIR/pid" || -L "$JOB_DIR/pid" ]]; then
|
|
742
|
+
if read_job_pid "$JOB_DIR/pid"; then pid="$RECORDED_PID"; pid_state="valid"; else pid_state="invalid"; fi
|
|
743
|
+
fi
|
|
744
|
+
if [[ "$pid_state" != "valid" ]] || ! kill -0 "$pid" 2>/dev/null; then
|
|
745
|
+
echo "not running (no live worker to cancel)"
|
|
746
|
+
exit 0
|
|
747
|
+
fi
|
|
748
|
+
# Background workers run as their own process-group leader (dispatch `set -m`),
|
|
749
|
+
# and that leader traps SIGTERM to record a best-effort exit code. Signal the
|
|
750
|
+
# whole group so the vendor CLI child dies too and the worker writes its exit.
|
|
751
|
+
cancel_grp="$pid"
|
|
752
|
+
cancel_pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d '[:space:]' || true)"
|
|
753
|
+
[[ "$cancel_pgid" =~ ^[1-9][0-9]*$ ]] && cancel_grp="$cancel_pgid"
|
|
754
|
+
kill -TERM "-$cancel_grp" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
755
|
+
cancel_waited=0
|
|
756
|
+
while [[ "$cancel_waited" -lt 5 ]]; do
|
|
757
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then break; fi
|
|
758
|
+
if ! kill -0 "$pid" 2>/dev/null; then break; fi
|
|
759
|
+
sleep 1; cancel_waited=$((cancel_waited + 1))
|
|
760
|
+
done
|
|
761
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
762
|
+
# The TERM trap did not fire in time; SIGKILL cannot be trapped, so no exit
|
|
763
|
+
# gets recorded — force the group down and record the terminal state below.
|
|
764
|
+
kill -KILL "-$cancel_grp" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
|
765
|
+
sleep 1
|
|
766
|
+
fi
|
|
767
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
768
|
+
if read_exit_code "$exit_path"; then
|
|
769
|
+
echo "cancelled (exit $RECORDED_EXIT)"
|
|
770
|
+
else
|
|
771
|
+
echo "cancelled (invalid exit metadata)"
|
|
772
|
+
fi
|
|
773
|
+
elif kill -0 "$pid" 2>/dev/null; then
|
|
774
|
+
die 1 "cancel could not terminate worker pid $pid"
|
|
775
|
+
else
|
|
776
|
+
(umask 077; printf '137\n' > "$exit_path")
|
|
777
|
+
echo "cancelled (exit 137)"
|
|
778
|
+
fi
|
|
779
|
+
exit 0 ;;
|
|
780
|
+
rm)
|
|
781
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
|
|
782
|
+
select_job "$2"
|
|
783
|
+
exit_path="$JOB_DIR/exit"
|
|
784
|
+
# Refuse to delete a job whose worker is still alive: removing the directory
|
|
785
|
+
# out from under a running worker would strand it writing into a deleted path.
|
|
786
|
+
# A recorded exit (finished) or a dead pid (crashed worker) is safe to remove.
|
|
787
|
+
if [[ ! -e "$exit_path" && ! -L "$exit_path" ]]; then
|
|
788
|
+
if [[ -e "$JOB_DIR/pid" || -L "$JOB_DIR/pid" ]] &&
|
|
789
|
+
read_job_pid "$JOB_DIR/pid" && kill -0 "$RECORDED_PID" 2>/dev/null; then
|
|
790
|
+
die 1 "job is running (pid $RECORDED_PID); cancel it first"
|
|
791
|
+
fi
|
|
792
|
+
fi
|
|
793
|
+
# select_job already proved $JOB_DIR is a real child dir, never a symlink.
|
|
794
|
+
/bin/rm -rf "$JOB_DIR"
|
|
795
|
+
echo "removed $2"
|
|
796
|
+
exit 0 ;;
|
|
675
797
|
result)
|
|
676
798
|
[[ $# -eq 2 ]] || usage
|
|
677
799
|
select_job "$2"
|
package/scripts/lib/common.sh
CHANGED
|
@@ -64,6 +64,57 @@ write_current_pid_file() {
|
|
|
64
64
|
mv "$tmp" "$path"
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
# ---- Vendor registry (single source of truth) ---------------------------
|
|
68
|
+
# Two vendor kinds. CLI-agent vendors run a local binary (see vendor_bin).
|
|
69
|
+
# Direct-API vendors are OpenAI-compatible /chat/completions endpoints driven
|
|
70
|
+
# by run-openai-compat.sh (curl + an API key, no CLI). To add an
|
|
71
|
+
# OpenAI-compatible vendor: add its name to OMNILANE_DIRECT_API_VENDORS, add a
|
|
72
|
+
# vendor_api_spec row, add a runners/run-<name>.sh wrapper, and a model row in
|
|
73
|
+
# configure.sh. Every validation site derives from these lists.
|
|
74
|
+
OMNILANE_CLI_VENDORS="codex claude grok gemini kimi qwen opencode"
|
|
75
|
+
OMNILANE_DIRECT_API_VENDORS="openrouter deepseek zai mistral groq cerebras"
|
|
76
|
+
|
|
77
|
+
# Pipe-joined vendor alternations for =~ checks and JSON metadata regexes.
|
|
78
|
+
# _OVERRIDE covers what --vendor may pin (no exec/off); _ALT adds the "exec"
|
|
79
|
+
# bring-your-own gate. Built from the lists above — the single source of truth.
|
|
80
|
+
OMNILANE_OVERRIDE_VENDOR_ALT=""
|
|
81
|
+
for _omnilane_v in $OMNILANE_CLI_VENDORS $OMNILANE_DIRECT_API_VENDORS; do
|
|
82
|
+
OMNILANE_OVERRIDE_VENDOR_ALT="${OMNILANE_OVERRIDE_VENDOR_ALT:+$OMNILANE_OVERRIDE_VENDOR_ALT|}$_omnilane_v"
|
|
83
|
+
done
|
|
84
|
+
unset _omnilane_v
|
|
85
|
+
# shellcheck disable=SC2034 # consumed by jobs.sh/dispatch.sh which source this file
|
|
86
|
+
OMNILANE_VENDOR_ALT="$OMNILANE_OVERRIDE_VENDOR_ALT|exec"
|
|
87
|
+
|
|
88
|
+
# Direct-API vendor -> "BASE_URL_DEFAULT|KEY_ENV|MODEL_HINT". Base URL is
|
|
89
|
+
# overridable per vendor via <VENDOR>_BASE_URL (e.g. DEEPSEEK_BASE_URL).
|
|
90
|
+
vendor_api_spec() {
|
|
91
|
+
case "$1" in
|
|
92
|
+
openrouter) echo "https://openrouter.ai/api/v1|OPENROUTER_API_KEY|anthropic/claude-sonnet-4" ;;
|
|
93
|
+
deepseek) echo "https://api.deepseek.com|DEEPSEEK_API_KEY|deepseek-chat" ;;
|
|
94
|
+
zai) echo "https://api.z.ai/api/openai/v1|ZAI_API_KEY|glm-4.6" ;;
|
|
95
|
+
mistral) echo "https://api.mistral.ai/v1|MISTRAL_API_KEY|devstral-latest" ;;
|
|
96
|
+
groq) echo "https://api.groq.com/openai/v1|GROQ_API_KEY|qwen/qwen3.6-27b" ;;
|
|
97
|
+
cerebras) echo "https://api.cerebras.ai/v1|CEREBRAS_API_KEY|qwen-3-32b" ;;
|
|
98
|
+
*) echo "" ;;
|
|
99
|
+
esac
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
vendor_is_direct_api() { # 0 if vendor is an OpenAI-compatible direct-API vendor
|
|
103
|
+
local _v
|
|
104
|
+
# Exact token match — a substring `case` would accept adjacent-pair strings
|
|
105
|
+
# like "openrouter deepseek".
|
|
106
|
+
for _v in $OMNILANE_DIRECT_API_VENDORS; do [[ "$1" == "$_v" ]] && return 0; done
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
omnilane_known_vendor() { # 0 if vendor is dispatchable (CLI, direct-API, or exec)
|
|
111
|
+
local _v
|
|
112
|
+
for _v in $OMNILANE_CLI_VENDORS $OMNILANE_DIRECT_API_VENDORS exec; do
|
|
113
|
+
[[ "$1" == "$_v" ]] && return 0
|
|
114
|
+
done
|
|
115
|
+
return 1
|
|
116
|
+
}
|
|
117
|
+
|
|
67
118
|
vendor_bin() { # vendor -> binary (honors local.sh overrides)
|
|
68
119
|
case "$1" in
|
|
69
120
|
codex) echo "${CODEX_BIN:-codex}" ;;
|
|
@@ -82,9 +133,12 @@ vendor_available() {
|
|
|
82
133
|
# checked at dispatch time (the chain resolver cannot see it here).
|
|
83
134
|
# "vote" is the built-in multi-model panel: needs >=2 voters, checked at dispatch.
|
|
84
135
|
[[ "$1" == "exec" || "$1" == "vote" ]] && return 0
|
|
85
|
-
#
|
|
86
|
-
if
|
|
87
|
-
|
|
136
|
+
# Direct-API vendors have no CLI: available iff curl and their API key exist.
|
|
137
|
+
if vendor_is_direct_api "$1"; then
|
|
138
|
+
local spec key_env
|
|
139
|
+
spec="$(vendor_api_spec "$1")"
|
|
140
|
+
key_env="${spec#*|}"; key_env="${key_env%%|*}"
|
|
141
|
+
command -v curl >/dev/null 2>&1 && [[ -n "${!key_env:-}" ]]
|
|
88
142
|
return
|
|
89
143
|
fi
|
|
90
144
|
local b; b="$(vendor_bin "$1")"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: Cerebras (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with CEREBRAS_BASE_URL; needs CEREBRAS_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=cerebras
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: DeepSeek (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with DEEPSEEK_BASE_URL; needs DEEPSEEK_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=deepseek
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: Groq (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with GROQ_BASE_URL; needs GROQ_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=groq
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: Mistral (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with MISTRAL_BASE_URL; needs MISTRAL_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=mistral
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|