omnilane 0.8.2 → 0.9.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.
@@ -0,0 +1,50 @@
1
+ # Fish completion for omnilane. Static command/option completion only; it never
2
+ # invokes dispatch, provider CLIs, or the executable machine-local overlay.
3
+ # Dynamic lane-name and job-id completion remain Bash/Zsh-only for now.
4
+
5
+ # Top-level subcommands (offered only before a subcommand is chosen).
6
+ complete -c omnilane -f -n __fish_use_subcommand -a version -d 'installed release version'
7
+ complete -c omnilane -f -n __fish_use_subcommand -a list -d 'effective routing table'
8
+ complete -c omnilane -f -n __fish_use_subcommand -a route -d 'dispatch or consult a model'
9
+ complete -c omnilane -f -n __fish_use_subcommand -a dispatch -d 'dispatch or consult a model'
10
+ complete -c omnilane -f -n __fish_use_subcommand -a jobs -d 'inspect background jobs'
11
+ complete -c omnilane -f -n __fish_use_subcommand -a mcp -d 'MCP stdio server'
12
+ complete -c omnilane -f -n __fish_use_subcommand -a doctor -d 'read-only health report'
13
+ complete -c omnilane -f -n __fish_use_subcommand -a release-audit -d 'offline release gate'
14
+ complete -c omnilane -f -n __fish_use_subcommand -a ui -d 'Live Board server'
15
+ complete -c omnilane -f -n __fish_use_subcommand -a configure -d 'lane routing (menu or set/get/unset/list/diff)'
16
+ complete -c omnilane -f -n __fish_use_subcommand -a completion -d 'print a shell completion script'
17
+ complete -c omnilane -f -n __fish_use_subcommand -a help -d 'usage'
18
+
19
+ # route / dispatch options.
20
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l mode -x -a 'advise work' -d 'read-only advise or write-enabled work'
21
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l vendor -x -a 'codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras exec vote' -d 'pin one vendor, no fallback'
22
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l effort -x -a 'low medium high xhigh max' -d 'reasoning effort'
23
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l workdir -r -d 'working directory'
24
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l model -x -d 'routed model override'
25
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l timeout -x -d 'per-call timeout in seconds'
26
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l job-timeout -x -d 'whole-job timeout in seconds'
27
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l background -d 'run as a background job'
28
+ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l dry-run -d 'resolve the plan and stop'
29
+
30
+ # jobs subcommands (only before a jobs subcommand is chosen).
31
+ complete -c omnilane -f \
32
+ -n '__fish_seen_subcommand_from jobs; and not __fish_seen_subcommand_from list status result tail retry stats wait audit prune cancel rm help' \
33
+ -a 'list status result tail retry stats wait audit prune cancel rm help' -d 'jobs subcommand'
34
+ complete -c omnilane -n '__fish_seen_subcommand_from jobs' -l json -d 'JSON output'
35
+ # jobs list / stats filters.
36
+ complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from list stats' -l lane -x -d 'filter by lane'
37
+ complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from list stats' -l vendor -x -a 'codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras exec' -d 'filter by vendor'
38
+ complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from list' -l status -x -a 'running done' -d 'filter by status'
39
+
40
+ # configure subcommands (non-interactive).
41
+ complete -c omnilane -f -n '__fish_seen_subcommand_from configure; and not __fish_seen_subcommand_from set get unset list diff' -a 'set get unset list diff' -d 'configure action'
42
+
43
+ # doctor / release-audit / ui / completion.
44
+ complete -c omnilane -n '__fish_seen_subcommand_from doctor' -l json -d 'JSON output'
45
+ complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l target -x -d 'target version'
46
+ complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l allow-dirty -d 'permit a dirty tree'
47
+ complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l require-tag -d 'require an annotated tag'
48
+ complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l json -d 'JSON output'
49
+ complete -c omnilane -f -n '__fish_seen_subcommand_from ui' -a 'start status url stop' -d 'Live Board action'
50
+ complete -c omnilane -f -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'target shell'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "One routing table, every harness — classify subtasks into lanes and dispatch each lane to the best vendor's agentic CLI (Codex, Claude, Gemini, Grok) using your existing subscription logins.",
5
5
  "bin": {
6
6
  "omnilane": "bin/omnilane"
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # omnilane self-check: run the CONTRIBUTING required checks in one command.
4
+ # Usage: check.sh [--quick] [REPO_DIR]
5
+ # --quick skip the two slow suite runs (python unittest, tests/run.sh)
6
+ # A SKIP (tool unavailable or target absent) is not a failure; the script exits
7
+ # non-zero only when a check actually FAILs. No -e: checks may fail and are
8
+ # tallied rather than aborting the run.
9
+
10
+ QUICK=0
11
+ REPO=""
12
+ while [[ $# -gt 0 ]]; do
13
+ case "$1" in
14
+ --quick) QUICK=1; shift ;;
15
+ -h|--help) echo "usage: check.sh [--quick] [REPO_DIR]"; exit 0 ;;
16
+ --) shift; break ;;
17
+ -*) echo "check.sh: unknown option: $1" >&2; exit 2 ;;
18
+ *) if [[ -n "$REPO" ]]; then echo "check.sh: too many arguments" >&2; exit 2; fi
19
+ REPO="$1"; shift ;;
20
+ esac
21
+ done
22
+ if [[ $# -gt 0 ]]; then
23
+ if [[ -n "$REPO" ]]; then echo "check.sh: too many arguments" >&2; exit 2; fi
24
+ REPO="$1"
25
+ fi
26
+ [[ -n "$REPO" ]] || REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
27
+
28
+ PASS_N=0; FAIL_N=0; SKIP_N=0
29
+ _pass() { PASS_N=$((PASS_N + 1)); printf 'PASS %s\n' "$1"; }
30
+ _skip() { SKIP_N=$((SKIP_N + 1)); printf 'SKIP %s (%s)\n' "$1" "$2"; }
31
+ _fail() { FAIL_N=$((FAIL_N + 1)); printf 'FAIL %s\n' "$1"; [[ -z "${2:-}" ]] || printf ' %s\n' "$2"; }
32
+
33
+ # Shell files present in the repo (CONTRIBUTING's bash -n / shellcheck set).
34
+ shell_files=()
35
+ for pattern in 'bin/omnilane' 'scripts/*.sh' 'scripts/lib/*.sh' 'scripts/runners/*.sh' 'install.sh'; do
36
+ # shellcheck disable=SC2086 # $pattern is an intentional glob against $REPO
37
+ for f in $REPO/$pattern; do
38
+ [[ -f "$f" ]] && shell_files+=("$f")
39
+ done
40
+ done
41
+
42
+ # 1) bash -n on every tracked shell file
43
+ if [[ ${#shell_files[@]} -eq 0 ]]; then
44
+ _skip bash-syntax "no shell files found"
45
+ else
46
+ bad=""
47
+ for f in "${shell_files[@]}"; do
48
+ bash -n "$f" 2>/dev/null || bad="$bad ${f##*/}"
49
+ done
50
+ if [[ -z "$bad" ]]; then _pass bash-syntax; else _fail bash-syntax "invalid:$bad"; fi
51
+ fi
52
+
53
+ # 2) shellcheck -S warning (skip if unavailable)
54
+ if ! command -v shellcheck >/dev/null 2>&1; then
55
+ _skip shellcheck "not installed"
56
+ elif [[ ${#shell_files[@]} -eq 0 ]]; then
57
+ _skip shellcheck "no shell files"
58
+ elif shellcheck -S warning "${shell_files[@]}" >/dev/null 2>&1; then
59
+ _pass shellcheck
60
+ else
61
+ _fail shellcheck "shellcheck -S warning reported findings"
62
+ fi
63
+
64
+ # 3) perl -c on the job-timeout supervisor
65
+ perl_file="$REPO/scripts/lib/job-timeout.pl"
66
+ if ! command -v perl >/dev/null 2>&1; then
67
+ _skip perl-syntax "perl not installed"
68
+ elif [[ ! -f "$perl_file" ]]; then
69
+ _skip perl-syntax "job-timeout.pl absent"
70
+ elif perl -c "$perl_file" >/dev/null 2>&1; then
71
+ _pass perl-syntax
72
+ else
73
+ _fail perl-syntax "perl -c failed"
74
+ fi
75
+
76
+ # 4) python compile of the UI and test modules
77
+ py_files=()
78
+ for rel in scripts/ui.py tests/test_ui.py tests/test_ci_policy.py tests/ui_browser_harness.py; do
79
+ [[ -f "$REPO/$rel" ]] && py_files+=("$REPO/$rel")
80
+ done
81
+ if ! command -v python3 >/dev/null 2>&1; then
82
+ _skip py-compile "python3 not installed"
83
+ elif [[ ${#py_files[@]} -eq 0 ]]; then
84
+ _skip py-compile "no python files"
85
+ elif python3 -m py_compile "${py_files[@]}" >/dev/null 2>&1; then
86
+ _pass py-compile
87
+ else
88
+ _fail py-compile "py_compile failed"
89
+ fi
90
+
91
+ # 5) python unit tests (slow; --quick skips)
92
+ if [[ "$QUICK" -eq 1 ]]; then
93
+ _skip unittest "--quick"
94
+ elif ! command -v python3 >/dev/null 2>&1; then
95
+ _skip unittest "python3 not installed"
96
+ elif [[ ! -d "$REPO/tests" ]]; then
97
+ _skip unittest "no tests dir"
98
+ elif ( cd "$REPO" && python3 -m unittest discover -s tests -p 'test_*.py' >/dev/null 2>&1 ); then
99
+ _pass unittest
100
+ else
101
+ _fail unittest "python unittest suite failed"
102
+ fi
103
+
104
+ # 6) shell test suite (slow; --quick skips)
105
+ if [[ "$QUICK" -eq 1 ]]; then
106
+ _skip shell-suite "--quick"
107
+ elif [[ ! -f "$REPO/tests/run.sh" ]]; then
108
+ _skip shell-suite "tests/run.sh absent"
109
+ elif bash "$REPO/tests/run.sh" >/dev/null 2>&1; then
110
+ _pass shell-suite
111
+ else
112
+ _fail shell-suite "bash tests/run.sh failed"
113
+ fi
114
+
115
+ # 7) effective routing table resolves
116
+ if [[ ! -f "$REPO/scripts/dispatch.sh" ]]; then
117
+ _skip dispatch-list "dispatch.sh absent"
118
+ elif bash "$REPO/scripts/dispatch.sh" --list >/dev/null 2>&1; then
119
+ _pass dispatch-list
120
+ else
121
+ _fail dispatch-list "dispatch.sh --list failed"
122
+ fi
123
+
124
+ printf '\nsummary: %d passed, %d skipped, %d failed\n' "$PASS_N" "$SKIP_N" "$FAIL_N"
125
+ [[ "$FAIL_N" -eq 0 ]]
@@ -9,6 +9,142 @@ 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
+ [[ "$had_file" -eq 1 ]] && grep -v '^#' "$LOCAL_FILE.bak" | grep -v "^$lane:" || true
64
+ } > "$tmp"
65
+ mv "$tmp" "$LOCAL_FILE"
66
+
67
+ # Semantic guard: reject only a structural FAIL on THIS lane. Availability
68
+ # WARN (validate exit 4) is fine — the vendor CLI may be legitimately absent.
69
+ local validate_out
70
+ validate_out="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --validate 2>&1)" || true
71
+ if printf '%s\n' "$validate_out" | grep -q "^FAIL $lane "; then
72
+ if [[ "$had_file" -eq 1 ]]; then mv "$LOCAL_FILE.bak" "$LOCAL_FILE"; else /bin/rm -f "$LOCAL_FILE"; fi
73
+ echo "omnilane: rejected — $(printf '%s\n' "$validate_out" | grep "^FAIL $lane " | head -1)" >&2
74
+ exit 2
75
+ fi
76
+ [[ "$had_file" -eq 1 ]] && /bin/rm -f "$LOCAL_FILE.bak"
77
+ echo "set $lane -> $spec"
78
+ }
79
+
80
+ cfg_get() {
81
+ local lane="${1:-}"
82
+ cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
83
+ local line
84
+ line="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null | grep "^$lane:" | head -1 || true)"
85
+ [[ -n "$line" ]] || { echo "omnilane: unknown lane '$lane' (see: omnilane list)" >&2; exit 2; }
86
+ printf '%s\n' "$line"
87
+ }
88
+
89
+ cfg_unset() {
90
+ local lane="${1:-}"
91
+ cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
92
+ if [[ ! -f "$LOCAL_FILE" ]] || ! grep -q "^$lane:" "$LOCAL_FILE"; then
93
+ echo "no local override for '$lane'"; return 0
94
+ fi
95
+ cp "$LOCAL_FILE" "$LOCAL_FILE.bak"
96
+ grep -v "^$lane:" "$LOCAL_FILE.bak" > "$LOCAL_FILE" || true
97
+ /bin/rm -f "$LOCAL_FILE.bak"
98
+ echo "unset $lane (local override removed)"
99
+ }
100
+
101
+ cfg_list() {
102
+ if [[ -f "$LOCAL_FILE" ]] && grep -qE '^[a-z]' "$LOCAL_FILE"; then
103
+ cat "$LOCAL_FILE"
104
+ else
105
+ echo "no local overrides in $LOCAL_FILE"
106
+ fi
107
+ }
108
+
109
+ # Diff the effective table (local wins) against a defaults-only resolution, so
110
+ # the user sees exactly which lanes their overrides change. Reuses dispatch.sh
111
+ # --list for both, so availability annotation and formatting stay consistent; a
112
+ # throwaway empty OMNILANE_HOME yields the defaults-only table.
113
+ cfg_diff() {
114
+ if [[ ! -f "$LOCAL_FILE" ]] || ! grep -qE '^[a-z]' "$LOCAL_FILE"; then
115
+ echo "no local overrides ($LOCAL_FILE); effective table equals the defaults"
116
+ return 0
117
+ fi
118
+ local empty eff def changed=0 lane eff_line def_line
119
+ empty="$(mktemp -d "${TMPDIR:-/tmp}/omnilane-diff.XXXXXX")"
120
+ eff="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null || true)"
121
+ def="$(OMNILANE_HOME="$empty" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null || true)"
122
+ /bin/rm -rf "$empty"
123
+ while IFS= read -r eff_line; do
124
+ [[ "$eff_line" =~ ^([a-z][a-z0-9-]*): ]] || continue
125
+ lane="${BASH_REMATCH[1]}"
126
+ def_line="$(printf '%s\n' "$def" | grep "^$lane:" | head -1 || true)"
127
+ if [[ "$eff_line" != "$def_line" ]]; then
128
+ changed=1
129
+ printf 'default> %s\n' "${def_line:-($lane not in defaults)}"
130
+ printf 'local > %s\n' "$eff_line"
131
+ echo
132
+ fi
133
+ done <<DIFF_EFF
134
+ $eff
135
+ DIFF_EFF
136
+ [[ "$changed" -eq 1 ]] || echo "local overrides present, but the effective table matches the defaults"
137
+ }
138
+
139
+ case "${1:-}" in
140
+ set) shift; cfg_set "$@"; exit $? ;;
141
+ get) shift; cfg_get "$@"; exit $? ;;
142
+ unset) shift; cfg_unset "$@"; exit $? ;;
143
+ list) shift; cfg_list "$@"; exit $? ;;
144
+ diff) cfg_diff; exit $? ;;
145
+ -h|--help|help) cfg_usage; exit 0 ;;
146
+ esac
147
+
12
148
  # Curated suggestions only — "c" always allows free text so new models work.
13
149
  CODEX_MODELS=("gpt-5.6-sol" "gpt-5.6-terra" "gpt-5.6-luna")
14
150
  CODEX_EFFORTS=("xhigh" "max" "ultra" "high" "medium" "low" "minimal" "none")
@@ -21,6 +157,13 @@ QWEN_MODELS=("qwen3-coder-plus" "qwen3-coder-flash")
21
157
  # OpenCode models use provider/model form; OpenRouter models are catalog slugs.
22
158
  OPENCODE_MODELS=("openrouter/anthropic/claude-sonnet-5" "openrouter/openai/gpt-5.6-sol" "opencode/default (leave model to opencode)")
23
159
  OPENROUTER_MODELS=("anthropic/claude-sonnet-5" "openai/gpt-5.6-sol" "moonshotai/kimi-k3" "qwen/qwen3-coder-plus")
160
+ # Direct-API OpenAI-compatible vendors (curl + <VENDOR>_API_KEY); slugs are
161
+ # suggestions — "c" free text covers anything each provider's /models lists.
162
+ DEEPSEEK_MODELS=("deepseek-chat" "deepseek-reasoner" "deepseek-v4-flash")
163
+ ZAI_MODELS=("glm-4.6")
164
+ MISTRAL_MODELS=("devstral-latest" "codestral-latest" "mistral-medium-latest")
165
+ GROQ_MODELS=("openai/gpt-oss-120b" "qwen/qwen3.6-27b" "openai/gpt-oss-20b")
166
+ CEREBRAS_MODELS=("gpt-oss-120b" "qwen-3-32b" "llama-3.3-70b")
24
167
 
25
168
  custom_value_is_safe() {
26
169
  case "$1" in
@@ -79,7 +222,7 @@ while true; do
79
222
  echo "$(msgf cfg_pick_range "${#LANES[@]}")"; continue
80
223
  fi
81
224
  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)"
225
+ 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
226
  [[ "$vendor" == exec* ]] && vendor="exec"
84
227
  [[ "$vendor" == vote* ]] && vendor="vote"
85
228
  if [[ "$vendor" == "off" ]]; then
@@ -96,6 +239,11 @@ while true; do
96
239
  opencode) model="$(pick "$(msg cfg_model)" "${OPENCODE_MODELS[@]}")"; effort="-"
97
240
  [[ "$model" == "opencode/default"* ]] && model="-" ;;
98
241
  openrouter) model="$(pick "$(msg cfg_model)" "${OPENROUTER_MODELS[@]}")"; effort="-" ;;
242
+ deepseek) model="$(pick "$(msg cfg_model)" "${DEEPSEEK_MODELS[@]}")"; effort="-" ;;
243
+ zai) model="$(pick "$(msg cfg_model)" "${ZAI_MODELS[@]}")"; effort="-" ;;
244
+ mistral) model="$(pick "$(msg cfg_model)" "${MISTRAL_MODELS[@]}")"; effort="-" ;;
245
+ groq) model="$(pick "$(msg cfg_model)" "${GROQ_MODELS[@]}")"; effort="-" ;;
246
+ cerebras) model="$(pick "$(msg cfg_model)" "${CEREBRAS_MODELS[@]}")"; effort="-" ;;
99
247
  vote) while true; do
100
248
  count="$(pick "$(msg cfg_voters_count)" "1" "2" "3" "4")"
101
249
  [[ "$count" =~ ^[1-4]$ ]] && break
@@ -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" =~ ^(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec|off)$ ]]; then
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" =~ ^(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter)$ ]]; then
493
- echo "omnilane: invalid vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter)" >&2
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