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.
@@ -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
@@ -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
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":"(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec)"(,|})'
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\":\"(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec)\",\"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\"\\}$"
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":"(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec)","model":"([^"\\]*)","effort":"([^"\\]*)","timeout":([1-9][0-9]*),"job_timeout":([1-9][0-9]*|null),"mode":"(advise|work)","workdir":"([^"\\]*)",'
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
- metadata_path="$job_dir/meta.json"
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
- [[ $# -eq 1 ]] || usage
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
- done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r | sed -n '1,20p')
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"
@@ -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
- # "openrouter" is direct-API (no CLI): available iff curl and a key exist.
86
- if [[ "$1" == "openrouter" ]]; then
87
- command -v curl >/dev/null 2>&1 && [[ -n "${OPENROUTER_API_KEY:-}" ]]
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" "$@"