omnilane 0.13.0 → 0.15.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.
@@ -10,6 +10,7 @@ complete -c omnilane -f -n __fish_use_subcommand -a dispatch -d 'dispatch o
10
10
  complete -c omnilane -f -n __fish_use_subcommand -a jobs -d 'inspect background jobs'
11
11
  complete -c omnilane -f -n __fish_use_subcommand -a mcp -d 'MCP stdio server'
12
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 benchmark -d 'fixed quality/cost comparison'
13
14
  complete -c omnilane -f -n __fish_use_subcommand -a release-audit -d 'offline release gate'
14
15
  complete -c omnilane -f -n __fish_use_subcommand -a ui -d 'Live Board server'
15
16
  complete -c omnilane -f -n __fish_use_subcommand -a configure -d 'lane routing (menu or set/get/unset/list/diff)'
@@ -29,19 +30,30 @@ complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l dry-run
29
30
 
30
31
  # jobs subcommands (only before a jobs subcommand is chosen).
31
32
  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'
33
+ -n '__fish_seen_subcommand_from jobs; and not __fish_seen_subcommand_from list status result tail retry stats recommend wait audit prune cancel rm help' \
34
+ -a 'list status result tail retry stats recommend wait audit prune cancel rm help' -d 'jobs subcommand'
34
35
  complete -c omnilane -n '__fish_seen_subcommand_from jobs' -l json -d 'JSON output'
35
36
  # 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 recommend' -l lane -x -d 'filter by lane'
37
38
  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
39
  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'
40
+ complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from stats recommend audit' -l last -x -d 'maximum recent jobs'
41
+ complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from recommend' -l min-samples -x -d 'minimum completed samples'
39
42
 
40
43
  # configure subcommands (non-interactive).
41
44
  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
45
 
43
46
  # doctor / release-audit / ui / completion.
44
47
  complete -c omnilane -n '__fish_seen_subcommand_from doctor' -l json -d 'JSON output'
48
+ complete -c omnilane -n '__fish_seen_subcommand_from doctor' -l strict -d 'fail when any warning is reported'
49
+ complete -c omnilane -n '__fish_seen_subcommand_from doctor' -l probe -x -a 'codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras' -d 'run one opt-in live probe'
50
+ complete -c omnilane -n '__fish_seen_subcommand_from doctor' -l probe-timeout -x -d 'probe timeout in seconds'
51
+ complete -c omnilane -n '__fish_seen_subcommand_from benchmark' -l json -d 'JSON output'
52
+ complete -c omnilane -n '__fish_seen_subcommand_from benchmark' -l run -d 'invoke providers in advise mode'
53
+ complete -c omnilane -n '__fish_seen_subcommand_from benchmark' -l vendor -x -a 'codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras' -d 'vendor to compare; repeatable'
54
+ complete -c omnilane -n '__fish_seen_subcommand_from benchmark' -l timeout -x -d 'per-workload timeout'
55
+ complete -c omnilane -n '__fish_seen_subcommand_from benchmark' -l workloads -r -d 'TSV workload file'
56
+ complete -c omnilane -n '__fish_seen_subcommand_from benchmark' -l cost-per-call -x -d 'user-supplied VENDOR=USD estimate'
45
57
  complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l target -x -d 'target version'
46
58
  complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l allow-dirty -d 'permit a dirty tree'
47
59
  complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l require-tag -d 'require an annotated tag'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.13.0",
3
+ "version": "0.15.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"
@@ -11,6 +11,7 @@
11
11
  "!scripts/__pycache__",
12
12
  "!**/*.pyc",
13
13
  "completions/",
14
+ "benchmarks/",
14
15
  "ui/",
15
16
  "routing.yaml",
16
17
  "routing.local.yaml.example",
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env python3
2
+ """Fixed, body-free routing quality/cost benchmark."""
3
+
4
+ import argparse
5
+ from decimal import Decimal, InvalidOperation
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import re
10
+ import shlex
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import time
15
+
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+ DEFAULT_WORKLOADS = ROOT / "benchmarks" / "workloads.tsv"
19
+ NAME_RE = re.compile(r"^[a-z][a-z0-9-]*$")
20
+ ID_RE = re.compile(r"^[a-z][a-z0-9_-]*$")
21
+
22
+
23
+ class BenchmarkError(Exception):
24
+ pass
25
+
26
+
27
+ def parse_args(argv=None):
28
+ parser = argparse.ArgumentParser(
29
+ prog="omnilane benchmark",
30
+ description=(
31
+ "Compare fixed-workload routing quality and user-supplied per-call "
32
+ "costs. Provider calls require --run."
33
+ ),
34
+ )
35
+ parser.add_argument("--json", action="store_true", help="emit JSON")
36
+ parser.add_argument(
37
+ "--run",
38
+ action="store_true",
39
+ help="invoke providers in advise mode; default is routing-only dry-run",
40
+ )
41
+ parser.add_argument(
42
+ "--vendor",
43
+ action="append",
44
+ default=[],
45
+ metavar="VENDOR",
46
+ help="vendor to compare; repeatable (default: configured vendors)",
47
+ )
48
+ parser.add_argument(
49
+ "--timeout",
50
+ type=int,
51
+ default=60,
52
+ metavar="SECONDS",
53
+ help="per-workload timeout, 1..600 (default: 60)",
54
+ )
55
+ parser.add_argument(
56
+ "--workloads",
57
+ type=Path,
58
+ default=DEFAULT_WORKLOADS,
59
+ metavar="FILE",
60
+ help="fixed TSV workload file",
61
+ )
62
+ parser.add_argument(
63
+ "--cost-per-call",
64
+ action="append",
65
+ default=[],
66
+ metavar="VENDOR=USD",
67
+ help="optional user-supplied estimated USD per call; repeatable",
68
+ )
69
+ args = parser.parse_args(argv)
70
+ if not 1 <= args.timeout <= 600:
71
+ parser.error("--timeout must be between 1 and 600 seconds")
72
+ return args
73
+
74
+
75
+ def unique(values):
76
+ return list(dict.fromkeys(values))
77
+
78
+
79
+ def decode_plan_value(value):
80
+ try:
81
+ parsed = shlex.split(value, posix=True)
82
+ except ValueError:
83
+ return value
84
+ return parsed[0] if len(parsed) == 1 else value
85
+
86
+
87
+ def parse_plan(text):
88
+ plan = {}
89
+ for line in text.splitlines():
90
+ if "=" not in line:
91
+ continue
92
+ key, value = line.split("=", 1)
93
+ if re.fullmatch(r"[a-z_]+", key):
94
+ plan[key] = decode_plan_value(value)
95
+ return plan
96
+
97
+
98
+ def load_workloads(path):
99
+ try:
100
+ lines = path.read_text(encoding="utf-8").splitlines()
101
+ except OSError as exc:
102
+ raise BenchmarkError(f"cannot read workloads: {path}: {exc}") from exc
103
+
104
+ workloads = []
105
+ seen = set()
106
+ for line_number, line in enumerate(lines, 1):
107
+ if not line.strip() or line.lstrip().startswith("#"):
108
+ continue
109
+ fields = line.split("\t", 4)
110
+ if len(fields) != 5:
111
+ raise BenchmarkError(
112
+ f"{path}:{line_number}: expected 5 tab-separated fields"
113
+ )
114
+ workload_id, lane, weight_text, pattern, prompt = fields
115
+ if not ID_RE.fullmatch(workload_id) or workload_id in seen:
116
+ raise BenchmarkError(f"{path}:{line_number}: invalid or duplicate id")
117
+ if not NAME_RE.fullmatch(lane):
118
+ raise BenchmarkError(f"{path}:{line_number}: invalid lane")
119
+ try:
120
+ weight = int(weight_text)
121
+ except ValueError as exc:
122
+ raise BenchmarkError(f"{path}:{line_number}: weight must be an integer") from exc
123
+ if not 1 <= weight <= 100:
124
+ raise BenchmarkError(f"{path}:{line_number}: weight must be 1..100")
125
+ try:
126
+ re.compile(pattern)
127
+ except re.error as exc:
128
+ raise BenchmarkError(f"{path}:{line_number}: invalid regex: {exc}") from exc
129
+ if not prompt.strip():
130
+ raise BenchmarkError(f"{path}:{line_number}: prompt must not be empty")
131
+ seen.add(workload_id)
132
+ workloads.append(
133
+ {
134
+ "id": workload_id,
135
+ "lane": lane,
136
+ "weight": weight,
137
+ "pattern": pattern,
138
+ "prompt": prompt,
139
+ }
140
+ )
141
+ if not workloads:
142
+ raise BenchmarkError(f"no workloads found: {path}")
143
+ return workloads
144
+
145
+
146
+ def parse_costs(items):
147
+ costs = {}
148
+ for item in items:
149
+ if "=" not in item:
150
+ raise BenchmarkError("--cost-per-call must use VENDOR=USD")
151
+ vendor, raw = item.split("=", 1)
152
+ if not NAME_RE.fullmatch(vendor):
153
+ raise BenchmarkError(f"invalid cost vendor: {vendor}")
154
+ try:
155
+ value = Decimal(raw)
156
+ except InvalidOperation as exc:
157
+ raise BenchmarkError(f"invalid cost for {vendor}: {raw}") from exc
158
+ if not value.is_finite() or value < 0:
159
+ raise BenchmarkError(f"cost for {vendor} must be non-negative")
160
+ costs[vendor] = value
161
+ return costs
162
+
163
+
164
+ def list_routes(dispatch, env):
165
+ result = subprocess.run(
166
+ [str(dispatch), "--list"],
167
+ text=True,
168
+ stdout=subprocess.PIPE,
169
+ stderr=subprocess.PIPE,
170
+ env=env,
171
+ timeout=15,
172
+ check=False,
173
+ )
174
+ if result.returncode != 0:
175
+ detail = result.stderr.strip().splitlines()[-1:] or ["unknown error"]
176
+ raise BenchmarkError(f"cannot list routing: {detail[0]}")
177
+ lanes = []
178
+ vendors = []
179
+ for line in result.stdout.splitlines():
180
+ if ":" not in line:
181
+ continue
182
+ lane, raw = line.split(":", 1)
183
+ lane = lane.strip()
184
+ if not NAME_RE.fullmatch(lane):
185
+ continue
186
+ lanes.append(lane)
187
+ try:
188
+ fields = shlex.split(raw.strip())
189
+ except ValueError:
190
+ fields = raw.split()
191
+ if fields and NAME_RE.fullmatch(fields[0]) and fields[0] not in {
192
+ "off",
193
+ "exec",
194
+ "vote",
195
+ }:
196
+ vendors.append(fields[0])
197
+ return unique(lanes), unique(vendors)
198
+
199
+
200
+ def resolve_route(dispatch, vendor, preferred_lane, lanes, prompt, env):
201
+ last_error = "vendor is not configured in an available lane"
202
+ for lane in unique([preferred_lane] + list(lanes)):
203
+ result = subprocess.run(
204
+ [
205
+ str(dispatch),
206
+ "--dry-run",
207
+ "--mode",
208
+ "advise",
209
+ "--vendor",
210
+ vendor,
211
+ lane,
212
+ prompt,
213
+ ],
214
+ text=True,
215
+ stdout=subprocess.PIPE,
216
+ stderr=subprocess.PIPE,
217
+ env=env,
218
+ timeout=15,
219
+ check=False,
220
+ )
221
+ if result.returncode == 0:
222
+ plan = parse_plan(result.stdout)
223
+ if plan.get("provider_invoked") != "no":
224
+ return None, "dry-run contract did not confirm provider_invoked=no"
225
+ if plan.get("vendor") != vendor:
226
+ return None, "dry-run resolved a different vendor"
227
+ plan.setdefault("lane", lane)
228
+ return plan, ""
229
+ detail = result.stderr.strip().splitlines()
230
+ if detail:
231
+ last_error = detail[-1]
232
+ return None, last_error
233
+
234
+
235
+ def money(value):
236
+ return format(value.quantize(Decimal("0.01")), "f")
237
+
238
+
239
+ def cost_report(vendor, calls, costs):
240
+ if vendor not in costs:
241
+ return None
242
+ per_call = costs[vendor]
243
+ return {
244
+ "basis": "user-supplied-per-call",
245
+ "currency": "USD",
246
+ "per_call_usd": money(per_call),
247
+ "estimated_total_usd": money(per_call * calls),
248
+ }
249
+
250
+
251
+ def run_one(repo, vendor, plan, workload, timeout, env):
252
+ runner = repo / "scripts" / "runners" / f"run-{vendor}.sh"
253
+ base = {
254
+ "id": workload["id"],
255
+ "requested_lane": workload["lane"],
256
+ "resolved_lane": plan.get("lane", workload["lane"]),
257
+ "model": plan.get("model", "-"),
258
+ "effort": plan.get("effort", "-"),
259
+ "weight": workload["weight"],
260
+ }
261
+ if not runner.is_file() or not os.access(runner, os.X_OK):
262
+ return dict(base, status="runner_error", duration_seconds=0.0, response_bytes=0), True
263
+
264
+ started = time.monotonic()
265
+ with tempfile.TemporaryDirectory(prefix="omnilane-benchmark-") as temporary:
266
+ temporary_path = Path(temporary)
267
+ prompt_file = temporary_path / "prompt.txt"
268
+ output_file = temporary_path / "response.txt"
269
+ prompt_file.write_text(str(workload["prompt"]) + "\n", encoding="utf-8")
270
+ run_env = env.copy()
271
+ run_env["OMNILANE_TIMEOUT"] = str(timeout)
272
+ try:
273
+ result = subprocess.run(
274
+ [
275
+ str(runner),
276
+ "advise",
277
+ str(repo),
278
+ plan.get("model", "-"),
279
+ plan.get("effort", "-"),
280
+ str(prompt_file),
281
+ str(output_file),
282
+ ],
283
+ stdout=subprocess.DEVNULL,
284
+ stderr=subprocess.DEVNULL,
285
+ env=run_env,
286
+ timeout=timeout + 5,
287
+ check=False,
288
+ )
289
+ except subprocess.TimeoutExpired:
290
+ elapsed = round(time.monotonic() - started, 3)
291
+ return dict(base, status="runner_error", duration_seconds=elapsed, response_bytes=0), True
292
+ elapsed = round(time.monotonic() - started, 3)
293
+ try:
294
+ body = output_file.read_text(encoding="utf-8")
295
+ response_bytes = output_file.stat().st_size
296
+ except OSError:
297
+ body = ""
298
+ response_bytes = 0
299
+ if result.returncode != 0:
300
+ status = "runner_error"
301
+ runner_error = True
302
+ else:
303
+ status = "passed" if re.search(str(workload["pattern"]), body) else "failed"
304
+ runner_error = False
305
+ return dict(
306
+ base,
307
+ status=status,
308
+ duration_seconds=elapsed,
309
+ response_bytes=response_bytes,
310
+ ), runner_error
311
+
312
+
313
+ def execute(args):
314
+ repo = Path(os.environ.get("OMNILANE_BENCHMARK_REPO", str(ROOT))).resolve()
315
+ dispatch = repo / "scripts" / "dispatch.sh"
316
+ if not dispatch.is_file() or not os.access(dispatch, os.X_OK):
317
+ raise BenchmarkError(f"dispatch is not executable: {dispatch}")
318
+ env = os.environ.copy()
319
+ workloads = load_workloads(args.workloads)
320
+ costs = parse_costs(args.cost_per_call)
321
+ lanes, configured_vendors = list_routes(dispatch, env)
322
+ vendors = unique(args.vendor or configured_vendors)
323
+ if not vendors:
324
+ raise BenchmarkError("no configured benchmark vendors found")
325
+ for vendor in vendors:
326
+ if not NAME_RE.fullmatch(vendor) or vendor in {"off", "exec", "vote"}:
327
+ raise BenchmarkError(f"unsupported benchmark vendor: {vendor}")
328
+
329
+ report = {
330
+ "schema_version": 1,
331
+ "command": "benchmark",
332
+ "mode": "run" if args.run else "dry-run",
333
+ "provider_invoked": False,
334
+ "workload_count": len(workloads),
335
+ "workloads_file": str(args.workloads.resolve()),
336
+ "vendors": [],
337
+ }
338
+ had_error = False
339
+ provider_invoked = False
340
+ for vendor in vendors:
341
+ vendor_started = time.monotonic()
342
+ items = []
343
+ passed = 0
344
+ score_possible = 0
345
+ score_earned = 0
346
+ for workload in workloads:
347
+ score_possible += int(workload["weight"])
348
+ plan, error = resolve_route(
349
+ dispatch,
350
+ vendor,
351
+ workload["lane"],
352
+ lanes,
353
+ workload["prompt"],
354
+ env,
355
+ )
356
+ if plan is None:
357
+ items.append(
358
+ {
359
+ "id": workload["id"],
360
+ "requested_lane": workload["lane"],
361
+ "weight": workload["weight"],
362
+ "status": "unavailable",
363
+ "duration_seconds": 0.0,
364
+ "response_bytes": 0,
365
+ "error": error,
366
+ }
367
+ )
368
+ had_error = True
369
+ continue
370
+ if not args.run:
371
+ items.append(
372
+ {
373
+ "id": workload["id"],
374
+ "requested_lane": workload["lane"],
375
+ "resolved_lane": plan.get("lane", workload["lane"]),
376
+ "model": plan.get("model", "-"),
377
+ "effort": plan.get("effort", "-"),
378
+ "weight": workload["weight"],
379
+ "status": "planned",
380
+ "duration_seconds": 0.0,
381
+ "response_bytes": 0,
382
+ }
383
+ )
384
+ continue
385
+ provider_invoked = True
386
+ item, runner_error = run_one(
387
+ repo, vendor, plan, workload, args.timeout, env
388
+ )
389
+ items.append(item)
390
+ had_error = had_error or runner_error
391
+ if item["status"] == "passed":
392
+ passed += 1
393
+ score_earned += int(workload["weight"])
394
+
395
+ quality = None
396
+ if args.run and score_possible:
397
+ quality = round((score_earned / score_possible) * 100, 2)
398
+ if quality.is_integer():
399
+ quality = int(quality)
400
+ report["vendors"].append(
401
+ {
402
+ "vendor": vendor,
403
+ "planned_calls": len(workloads),
404
+ "passed": passed,
405
+ "score_earned": score_earned,
406
+ "score_possible": score_possible,
407
+ "quality_percent": quality,
408
+ "duration_seconds": round(time.monotonic() - vendor_started, 3),
409
+ "cost": cost_report(vendor, len(workloads), costs),
410
+ "workloads": items,
411
+ }
412
+ )
413
+ report["provider_invoked"] = provider_invoked
414
+ return report, 1 if had_error else 0
415
+
416
+
417
+ def print_human(report):
418
+ invoked = "yes" if report["provider_invoked"] else "no"
419
+ print(f"mode={report['mode']} provider_invoked={invoked}")
420
+ for vendor in report["vendors"]:
421
+ quality = vendor["quality_percent"]
422
+ quality_text = "planned" if quality is None else f"{quality}%"
423
+ cost = vendor["cost"]
424
+ cost_text = "not supplied" if cost is None else f"USD {cost['estimated_total_usd']}"
425
+ print(
426
+ f"{vendor['vendor']}: calls={vendor['planned_calls']} "
427
+ f"quality={quality_text} estimated_cost={cost_text}"
428
+ )
429
+
430
+
431
+ def main(argv=None):
432
+ args = parse_args(argv)
433
+ try:
434
+ report, return_code = execute(args)
435
+ except BenchmarkError as exc:
436
+ print(f"omnilane benchmark: {exc}", file=sys.stderr)
437
+ return 2
438
+ if args.json:
439
+ print(json.dumps(report, ensure_ascii=False, separators=(",", ":")))
440
+ else:
441
+ print_human(report)
442
+ return return_code
443
+
444
+
445
+ if __name__ == "__main__":
446
+ raise SystemExit(main())
package/scripts/check.sh CHANGED
@@ -75,7 +75,7 @@ fi
75
75
 
76
76
  # 4) python compile of the UI and test modules
77
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
78
+ for rel in scripts/ui.py scripts/benchmark.py tests/test_ui.py tests/test_benchmark.py tests/test_ci_policy.py tests/ui_browser_harness.py; do
79
79
  [[ -f "$REPO/$rel" ]] && py_files+=("$REPO/$rel")
80
80
  done
81
81
  if ! command -v python3 >/dev/null 2>&1; then
@@ -655,10 +655,58 @@ secure_job_files() {
655
655
  find "$JOB_DIR" -type f -exec chmod 600 {} +
656
656
  }
657
657
 
658
+ completion_tail() {
659
+ local path="$1" size=0 keep value="" sentinel=$'\001'
660
+ local note=$'[truncated: leading output omitted]\n'
661
+ local LC_ALL=C
662
+
663
+ if [[ -f "$path" && ! -L "$path" ]]; then
664
+ size="$(wc -c < "$path" 2>/dev/null | tr -d '[:space:]')" || return 1
665
+ [[ "$size" =~ ^[0-9]+$ ]] || return 1
666
+ if [[ "$size" -gt 2000 ]]; then
667
+ keep=$((2000 - ${#note}))
668
+ value="$(printf '%s' "$note"; tail -c "$keep" "$path"; printf '%s' "$sentinel")" || return 1
669
+ else
670
+ value="$(cat "$path"; printf '%s' "$sentinel")" || return 1
671
+ fi
672
+ value="${value%"$sentinel"}"
673
+ fi
674
+ printf '%s' "$value"
675
+ }
676
+
677
+ write_completion_record() {
678
+ local rc="$1" inbox="$OMNILANE_HOME/inbox"
679
+ local final="$inbox/$JOB_ID.json" tmp="$inbox/.$JOB_ID.tmp.$$-$RANDOM"
680
+ local tail_value finished old_umask write_rc=0 sentinel=$'\001'
681
+
682
+ prepare_inbox_store || return 1
683
+ tail_value="$(completion_tail "$JOB_DIR/out.txt"; printf '%s' "$sentinel")" || return 1
684
+ tail_value="${tail_value%"$sentinel"}"
685
+ finished="$(date -u +%FT%TZ)" || return 1
686
+ old_umask="$(umask)"
687
+ umask 077
688
+ printf '{"job_id":"%s","lane":"%s","vendor":"%s","model":"%s","mode":"%s","workdir":"%s","exit":%s,"finished":"%s","tail":"%s"}\n' \
689
+ "$(json_escape "$JOB_ID")" "$(json_escape "$LANE")" "$(json_escape "$VENDOR")" \
690
+ "$(json_escape "$MODEL")" "$(json_escape "$MODE")" "$(json_escape "$WORKDIR")" \
691
+ "$rc" "$(json_escape "$finished")" "$(json_escape "$tail_value")" > "$tmp" || write_rc=$?
692
+ if [[ "$write_rc" -eq 0 ]]; then
693
+ chmod 600 "$tmp" || write_rc=$?
694
+ fi
695
+ if [[ "$write_rc" -eq 0 ]]; then
696
+ mv "$tmp" "$final" || write_rc=$?
697
+ fi
698
+ umask "$old_umask"
699
+ rm -f "$tmp" 2>/dev/null || true
700
+ return "$write_rc"
701
+ }
702
+
658
703
  finish_job() {
659
704
  local rc="$1"
660
705
  secure_job_files
661
706
  (umask 077; printf '%s\n' "$rc" > "$JOB_DIR/exit")
707
+ if [[ "${OMNILANE_INBOX:-1}" != "0" ]]; then
708
+ write_completion_record "$rc" >/dev/null 2>&1 || true
709
+ fi
662
710
  }
663
711
 
664
712
  run_job() {
package/scripts/doctor.sh CHANGED
@@ -3,14 +3,34 @@ set -u
3
3
  # Read-only health report for routing, state, watchdog, and optional UI support.
4
4
 
5
5
  JSON_MODE=0
6
- if [[ $# -eq 1 && "$1" == "--json" ]]; then
7
- JSON_MODE=1
8
- elif [[ $# -ne 0 ]]; then
9
- echo "usage: omnilane doctor [--json]" >&2
6
+ STRICT_MODE=0
7
+ PROBE_VENDOR=""
8
+ PROBE_TIMEOUT=30
9
+ PROBE_TIMEOUT_GIVEN=0
10
+ while [[ $# -gt 0 ]]; do
11
+ case "$1" in
12
+ --json) JSON_MODE=1 ;;
13
+ --strict) STRICT_MODE=1 ;;
14
+ --probe)
15
+ [[ $# -ge 2 ]] || { echo "omnilane: --probe needs a vendor" >&2; exit 2; }
16
+ PROBE_VENDOR="$2"; shift ;;
17
+ --probe-timeout)
18
+ [[ $# -ge 2 ]] || { echo "omnilane: --probe-timeout needs seconds" >&2; exit 2; }
19
+ PROBE_TIMEOUT="$2"; PROBE_TIMEOUT_GIVEN=1; shift ;;
20
+ *)
21
+ echo "usage: omnilane doctor [--json] [--strict] [--probe V [--probe-timeout SEC]]" >&2
22
+ exit 2
23
+ ;;
24
+ esac
25
+ shift
26
+ done
27
+ [[ "$PROBE_TIMEOUT_GIVEN" -eq 0 || -n "$PROBE_VENDOR" ]] || {
28
+ echo "omnilane: --probe-timeout requires --probe V" >&2
10
29
  exit 2
11
- fi
30
+ }
12
31
  REPO="${OMNILANE_DOCTOR_REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
13
32
  OMNILANE_HOME="${OMNILANE_HOME:-$HOME/.omnilane}"
33
+ PROBE_SCRIPT="${OMNILANE_PROVIDER_PROBE_SCRIPT:-$REPO/scripts/provider-probe.sh}"
14
34
  PASS_COUNT=0
15
35
  WARN_COUNT=0
16
36
  FAIL_COUNT=0
@@ -115,7 +135,10 @@ if [[ -L "$OMNILANE_HOME/jobs" ||
115
135
  ( -e "$OMNILANE_HOME/jobs" && ! -d "$OMNILANE_HOME/jobs" ) ]]; then
116
136
  report FAIL job-privacy "$OMNILANE_HOME/jobs must be a real directory, not a symlink or file"
117
137
  elif [[ -d "$OMNILANE_HOME/jobs" ]]; then
118
- jobs_mode="$(stat -f '%Lp' "$OMNILANE_HOME/jobs" 2>/dev/null || stat -c '%a' "$OMNILANE_HOME/jobs" 2>/dev/null || true)"
138
+ jobs_mode="$(stat -f '%Lp' "$OMNILANE_HOME/jobs" 2>/dev/null || true)"
139
+ if [[ ! "$jobs_mode" =~ ^[0-7]{3,4}$ ]]; then
140
+ jobs_mode="$(stat -c '%a' "$OMNILANE_HOME/jobs" 2>/dev/null || true)"
141
+ fi
119
142
  if [[ "$jobs_mode" =~ ^[0-7]*00$ ]]; then
120
143
  report PASS job-privacy "$OMNILANE_HOME/jobs mode is $jobs_mode"
121
144
  elif [[ -n "$jobs_mode" ]]; then
@@ -178,6 +201,24 @@ else
178
201
  report WARN vendors "no vendor CLI reachable${vendor_absent:+ (missing: $vendor_absent)}; every lane degrades to off"
179
202
  fi
180
203
 
204
+ # A real inference call is explicit and single-vendor only. Default doctor
205
+ # remains local/offline. Never relay provider output or runner stderr.
206
+ if [[ -n "$PROBE_VENDOR" ]]; then
207
+ if [[ ! -x "$PROBE_SCRIPT" ]]; then
208
+ report FAIL provider-probe "probe runner is unavailable"
209
+ else
210
+ probe_output="$(OMNILANE_PROBE_REPO="$REPO" "$PROBE_SCRIPT" \
211
+ --vendor "$PROBE_VENDOR" --timeout "$PROBE_TIMEOUT" 2>/dev/null)"
212
+ probe_rc=$?
213
+ if [[ "$probe_rc" -eq 0 ]]; then
214
+ report PASS provider-probe "$PROBE_VENDOR bounded live inference succeeded"
215
+ else
216
+ report FAIL provider-probe "$PROBE_VENDOR bounded live inference failed (exit $probe_rc)"
217
+ fi
218
+ : "$probe_output"
219
+ fi
220
+ fi
221
+
181
222
  if command -v python3 >/dev/null 2>&1; then
182
223
  if python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' \
183
224
  >/dev/null 2>&1; then
@@ -192,13 +233,15 @@ fi
192
233
 
193
234
  if [[ "$JSON_MODE" -eq 1 ]]; then
194
235
  ok=true
195
- [[ "$FAIL_COUNT" -eq 0 ]] || ok=false
196
- printf '{"ok":%s,"checks":[%s],"summary":{"passed":%s,"warnings":%s,"failed":%s}}\n' \
197
- "$ok" "$JSON_REPORTS" "$PASS_COUNT" "$WARN_COUNT" "$FAIL_COUNT"
236
+ [[ "$FAIL_COUNT" -eq 0 && ( "$STRICT_MODE" -eq 0 || "$WARN_COUNT" -eq 0 ) ]] || ok=false
237
+ strict=false
238
+ [[ "$STRICT_MODE" -eq 0 ]] || strict=true
239
+ printf '{"ok":%s,"checks":[%s],"summary":{"passed":%s,"warnings":%s,"failed":%s},"strict":%s}\n' \
240
+ "$ok" "$JSON_REPORTS" "$PASS_COUNT" "$WARN_COUNT" "$FAIL_COUNT" "$strict"
198
241
  else
199
242
  warning_suffix=s
200
243
  [[ "$WARN_COUNT" -eq 1 ]] && warning_suffix=""
201
244
  printf '\nSummary: %s passed, %s warning%s, %s failed\n' \
202
245
  "$PASS_COUNT" "$WARN_COUNT" "$warning_suffix" "$FAIL_COUNT"
203
246
  fi
204
- [[ "$FAIL_COUNT" -eq 0 ]]
247
+ [[ "$FAIL_COUNT" -eq 0 && ( "$STRICT_MODE" -eq 0 || "$WARN_COUNT" -eq 0 ) ]]