omnilane 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 ) ]]
package/scripts/jobs.sh CHANGED
@@ -2,6 +2,7 @@
2
2
  set -euo pipefail
3
3
  # omnilane background-job helper.
4
4
  # Usage: jobs.sh [--json] list | status JOB_ID | result JOB_ID | stats [--last N]
5
+ # jobs.sh [--json] recommend [--last N] [--lane L] [--min-samples N]
5
6
  # jobs.sh wait JOB_ID [--timeout N]
6
7
  # jobs.sh audit [--last N] [--json] | prune [--keep N] [--apply]
7
8
 
@@ -48,7 +49,7 @@ die() {
48
49
  exit "$rc"
49
50
  }
50
51
 
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
+ 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]|recommend [--last N] [--lane L] [--min-samples N]|wait ID [--timeout N]|cancel ID|rm ID|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
52
53
 
53
54
  usage() {
54
55
  die 2 "$USAGE_TEXT"
@@ -168,6 +169,83 @@ json_stat_counts() {
168
169
  printf ']'
169
170
  }
170
171
 
172
+ build_recommendation_rows() {
173
+ local minimum="$1"
174
+ shift
175
+ [[ $# -gt 0 ]] || return 0
176
+ printf '%s\n' "$@" | awk -F '\t' -v minimum="$minimum" '
177
+ {
178
+ key = $1 SUBSEP $2
179
+ samples[key]++
180
+ if ($3 == 0) succeeded[key]++
181
+ else failed[key]++
182
+ }
183
+ END {
184
+ for (key in samples) {
185
+ split(key, parts, SUBSEP)
186
+ rate = int((succeeded[key] * 100) / samples[key])
187
+ eligible = samples[key] >= minimum ? 1 : 0
188
+ printf "%s\t%d\t%d\t%d\t%s\t%d\t%d\n", \
189
+ parts[1], eligible, rate, samples[key], parts[2], \
190
+ succeeded[key] + 0, failed[key] + 0
191
+ }
192
+ }
193
+ ' | LC_ALL=C sort -t $'\t' -k1,1 -k2,2nr -k3,3nr -k4,4nr -k5,5
194
+ }
195
+
196
+ recommendations_json() {
197
+ local rows="$1"
198
+ [[ -n "$rows" ]] || { printf '[]'; return 0; }
199
+ printf '%s\n' "$rows" | awk -F '\t' '
200
+ function close_lane( status, recommendation) {
201
+ if (current == "") return
202
+ status = recommended == "" ? "insufficient_data" : "ready"
203
+ recommendation = recommended == "" ? "null" : "\"" recommended "\""
204
+ printf "],\"status\":\"%s\",\"recommended_vendor\":%s}", status, recommendation
205
+ }
206
+ BEGIN { printf "["; current = ""; first_lane = 1 }
207
+ {
208
+ if ($1 != current) {
209
+ close_lane()
210
+ if (!first_lane) printf ","
211
+ first_lane = 0
212
+ current = $1
213
+ first_candidate = 1
214
+ recommended = ""
215
+ printf "{\"lane\":\"%s\",\"candidates\":[", current
216
+ }
217
+ if (!first_candidate) printf ","
218
+ first_candidate = 0
219
+ eligible = $2 == 1 ? "true" : "false"
220
+ printf "{\"vendor\":\"%s\",\"samples\":%d,\"succeeded\":%d,\"failed\":%d,\"success_rate\":%d,\"eligible\":%s}", \
221
+ $5, $4, $6, $7, $3, eligible
222
+ if (recommended == "" && $2 == 1) recommended = $5
223
+ }
224
+ END { close_lane(); printf "]" }
225
+ '
226
+ }
227
+
228
+ print_recommendations() {
229
+ local rows="$1" minimum="$2"
230
+ [[ -n "$rows" ]] || return 0
231
+ printf '%s\n' "$rows" | awk -F '\t' -v minimum="$minimum" '
232
+ $1 != current {
233
+ current = $1
234
+ if ($2 == 1) {
235
+ printf "recommend lane=%s vendor=%s success_rate=%d%% samples=%d succeeded=%d failed=%d\n", \
236
+ $1, $5, $3, $4, $6, $7
237
+ } else {
238
+ printf "recommend lane=%s status=insufficient_data min_samples=%d\n", $1, minimum
239
+ }
240
+ }
241
+ {
242
+ eligible = $2 == 1 ? "yes" : "no"
243
+ printf "candidate lane=%s vendor=%s success_rate=%d%% samples=%d succeeded=%d failed=%d eligible=%s\n", \
244
+ $1, $5, $3, $4, $6, $7, eligible
245
+ }
246
+ '
247
+ }
248
+
171
249
  path_mode() {
172
250
  if stat -f '%Lp' "$1" >/dev/null 2>&1; then
173
251
  stat -f '%Lp' "$1"
@@ -191,7 +269,7 @@ set -- ${args[@]+"${args[@]}"}
191
269
  COMMAND="${1:-unknown}"
192
270
  if [[ "$JSON_MODE" -eq 1 ]]; then
193
271
  case "$COMMAND" in
194
- list|status|result|stats|audit) ;;
272
+ list|status|result|stats|recommend|audit) ;;
195
273
  *) usage ;;
196
274
  esac
197
275
  fi
@@ -418,6 +496,88 @@ case "${1:-}" in
418
496
  "$sampled" "$passed" "$failed" "$findings"
419
497
  fi
420
498
  [[ "$findings" -eq 0 ]] || exit 1 ;;
499
+ recommend)
500
+ limit=100
501
+ minimum_samples=3
502
+ filter_lane=""
503
+ shift
504
+ while [[ $# -gt 0 ]]; do
505
+ case "$1" in
506
+ --last)
507
+ [[ $# -ge 2 ]] || usage
508
+ limit="$2"; shift 2 ;;
509
+ --lane)
510
+ [[ $# -ge 2 ]] || usage
511
+ filter_lane="$2"; shift 2 ;;
512
+ --min-samples)
513
+ [[ $# -ge 2 ]] || usage
514
+ minimum_samples="$2"; shift 2 ;;
515
+ *) usage ;;
516
+ esac
517
+ done
518
+ [[ "$limit" =~ ^[1-9][0-9]{0,3}$ && "$limit" -le 10000 ]] || {
519
+ die 2 "invalid --last value (want 1..10000)"
520
+ }
521
+ [[ "$minimum_samples" =~ ^[1-9][0-9]{0,2}$ && "$minimum_samples" -le 1000 ]] || {
522
+ die 2 "invalid --min-samples value (want 1..1000)"
523
+ }
524
+ [[ -z "$filter_lane" || "$filter_lane" =~ ^[a-z][a-z0-9-]*$ ]] || die 2 "invalid --lane value"
525
+ sampled=0
526
+ completed=0
527
+ running=0
528
+ invalid_exit=0
529
+ invalid_metadata=0
530
+ ids=()
531
+ outcomes=()
532
+ if [[ -d "$JOBS" ]]; then
533
+ for job_dir in "$JOBS"/*; do
534
+ [[ -d "$job_dir" && ! -L "$job_dir" ]] || continue
535
+ id="${job_dir##*/}"
536
+ [[ "$id" =~ $JOB_ID_PATTERN ]] || continue
537
+ ids+=("$id")
538
+ done
539
+ fi
540
+ if [[ ${#ids[@]} -gt 0 ]]; then
541
+ while IFS= read -r id; do
542
+ job_dir="$JOBS/$id"
543
+ metadata_path="$job_dir/meta.json"
544
+ meta_ok=0
545
+ if read_public_metadata "$metadata_path" &&
546
+ parse_stats_metadata "$PUBLIC_METADATA"; then
547
+ meta_ok=1
548
+ fi
549
+ if [[ -n "$filter_lane" ]]; then
550
+ [[ "$meta_ok" -eq 1 && "$filter_lane" == "$STATS_LANE" ]] || continue
551
+ fi
552
+ sampled=$((sampled + 1))
553
+ if [[ "$meta_ok" -ne 1 ]]; then
554
+ invalid_metadata=$((invalid_metadata + 1))
555
+ continue
556
+ fi
557
+ exit_path="$job_dir/exit"
558
+ if [[ ! -e "$exit_path" && ! -L "$exit_path" ]]; then
559
+ running=$((running + 1))
560
+ continue
561
+ fi
562
+ if ! read_exit_code "$exit_path"; then
563
+ invalid_exit=$((invalid_exit + 1))
564
+ continue
565
+ fi
566
+ completed=$((completed + 1))
567
+ outcomes+=("$STATS_LANE"$'\t'"$STATS_VENDOR"$'\t'"$RECORDED_EXIT")
568
+ done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r | sed -n "1,${limit}p")
569
+ fi
570
+ rows="$(build_recommendation_rows "$minimum_samples" ${outcomes[@]+"${outcomes[@]}"})"
571
+ if [[ "$JSON_MODE" -eq 1 ]]; then
572
+ recommendations="$(recommendations_json "$rows")"
573
+ printf '{"schema_version":1,"command":"recommend","ok":true,"sampled":%s,"completed":%s,"excluded":{"running":%s,"invalid_exit":%s,"invalid_metadata":%s},"minimum_samples":%s,"recommendations":%s}\n' \
574
+ "$sampled" "$completed" "$running" "$invalid_exit" "$invalid_metadata" \
575
+ "$minimum_samples" "$recommendations"
576
+ else
577
+ printf 'recommendations: sampled=%s completed=%s running=%s invalid_exit=%s invalid_metadata=%s min_samples=%s\n' \
578
+ "$sampled" "$completed" "$running" "$invalid_exit" "$invalid_metadata" "$minimum_samples"
579
+ print_recommendations "$rows" "$minimum_samples"
580
+ fi ;;
421
581
  stats)
422
582
  limit=100
423
583
  filter_lane=""; filter_vendor=""
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Explicit, bounded live-inference probe for exactly one configured provider.
5
+ # No provider is contacted unless --vendor is present. Provider output and
6
+ # stderr stay inside a private temporary directory and are never relayed.
7
+
8
+ SELF_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
9
+ REPO="${OMNILANE_PROBE_REPO:-$SELF_REPO}"
10
+ DISPATCH="$REPO/scripts/dispatch.sh"
11
+ JSON_MODE=0
12
+ VENDOR=""
13
+ TIMEOUT=30
14
+
15
+ usage() {
16
+ echo "usage: provider-probe.sh --vendor V [--timeout SEC] [--json]" >&2
17
+ exit 2
18
+ }
19
+
20
+ json_escape() {
21
+ local s="$1" out="" ch escaped code i
22
+ for ((i = 0; i < ${#s}; i++)); do
23
+ ch="${s:i:1}"
24
+ case "$ch" in
25
+ '"') out="$out\\\"" ;;
26
+ '\\') out="$out\\\\" ;;
27
+ $'\b') out="$out\\b" ;;
28
+ $'\f') out="$out\\f" ;;
29
+ $'\n') out="$out\\n" ;;
30
+ $'\r') out="$out\\r" ;;
31
+ $'\t') out="$out\\t" ;;
32
+ *)
33
+ LC_CTYPE=C printf -v code '%d' "'$ch"
34
+ if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
35
+ printf -v escaped '\\u%04x' "$code"
36
+ out="$out$escaped"
37
+ else
38
+ out="$out$ch"
39
+ fi
40
+ ;;
41
+ esac
42
+ done
43
+ printf '%s' "$out"
44
+ }
45
+
46
+ emit_result() {
47
+ local ok="$1" status="$2" invoked="$3" reason="$4"
48
+ local duration="$5" response_bytes="$6"
49
+ if [[ "$JSON_MODE" -eq 1 ]]; then
50
+ printf '{"schema_version":1,"command":"provider-probe","ok":%s,"vendor":"%s","model":"%s","status":"%s","provider_invoked":%s,"timeout":%s,"duration_seconds":%s,"response_bytes":%s,"reason":"%s"}\n' \
51
+ "$ok" "$(json_escape "$VENDOR")" "$(json_escape "${MODEL:-}")" \
52
+ "$(json_escape "$status")" "$invoked" "$TIMEOUT" "$duration" \
53
+ "$response_bytes" "$(json_escape "$reason")"
54
+ else
55
+ printf 'provider_probe vendor=%s model=%s status=%s provider_invoked=%s timeout=%ss duration=%ss response_bytes=%s reason=%s\n' \
56
+ "$VENDOR" "${MODEL:-unknown}" "$status" "$invoked" "$TIMEOUT" \
57
+ "$duration" "$response_bytes" "$reason"
58
+ fi
59
+ }
60
+
61
+ while [[ $# -gt 0 ]]; do
62
+ case "$1" in
63
+ --vendor)
64
+ [[ $# -ge 2 ]] || usage
65
+ VENDOR="$2"; shift 2 ;;
66
+ --timeout)
67
+ [[ $# -ge 2 ]] || usage
68
+ TIMEOUT="$2"; shift 2 ;;
69
+ --json)
70
+ JSON_MODE=1; shift ;;
71
+ *) usage ;;
72
+ esac
73
+ done
74
+
75
+ [[ "$VENDOR" =~ ^(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|deepseek|zai|mistral|groq|cerebras)$ ]] || {
76
+ echo "omnilane: invalid vendor for provider probe" >&2
77
+ exit 2
78
+ }
79
+ [[ "$TIMEOUT" =~ ^[1-9][0-9]{0,2}$ && "$TIMEOUT" -le 300 ]] || {
80
+ echo "omnilane: invalid probe timeout (want 1..300 seconds)" >&2
81
+ exit 2
82
+ }
83
+ [[ -x "$DISPATCH" ]] || {
84
+ emit_result false unavailable false "dispatch is unavailable" 0 0
85
+ exit 1
86
+ }
87
+
88
+ PLAN=""
89
+ while IFS= read -r lane; do
90
+ [[ "$lane" =~ ^[a-z][a-z0-9-]*$ ]] || continue
91
+ set +e
92
+ candidate="$(bash "$DISPATCH" --dry-run --mode advise --vendor "$VENDOR" \
93
+ --timeout "$TIMEOUT" "$lane" "OMNILANE_PROVIDER_PROBE" 2>/dev/null)"
94
+ candidate_rc=$?
95
+ set -e
96
+ if [[ "$candidate_rc" -eq 0 && "$candidate" == *"provider_invoked=no"* ]]; then
97
+ PLAN="$candidate"
98
+ break
99
+ fi
100
+ done < <(bash "$DISPATCH" --list 2>/dev/null | sed -n 's/^\([a-z][a-z0-9-]*\):.*/\1/p')
101
+
102
+ if [[ -z "$PLAN" ]]; then
103
+ emit_result false unavailable false "vendor is not both configured and available" 0 0
104
+ exit 4
105
+ fi
106
+
107
+ field_value() {
108
+ local key="$1"
109
+ printf '%s\n' "$PLAN" | sed -n "s/^${key}=//p" | sed -n '1p'
110
+ }
111
+
112
+ decode_plan_value() {
113
+ local encoded="$1" decoded
114
+ [[ ${#encoded} -le 4096 && "$encoded" != *$'\n'* ]] || return 1
115
+ # dispatch.sh emits values with Bash printf %q. read without -r reverses
116
+ # its backslash quoting without evaluating command substitutions or code.
117
+ IFS= read decoded <<< "$encoded" || return 1
118
+ printf '%s' "$decoded"
119
+ }
120
+
121
+ RAW_MODEL="$(field_value model)"
122
+ RAW_EFFORT="$(field_value effort)"
123
+ if ! MODEL="$(decode_plan_value "$RAW_MODEL")" ||
124
+ ! EFFORT="$(decode_plan_value "$RAW_EFFORT")"; then
125
+ emit_result false unavailable false "invalid dry-run field encoding" 0 0
126
+ exit 4
127
+ fi
128
+ RUNNER="$REPO/scripts/runners/run-$VENDOR.sh"
129
+ [[ -n "$MODEL" && -x "$RUNNER" ]] || {
130
+ emit_result false unavailable false "resolved runner or model is unavailable" 0 0
131
+ exit 4
132
+ }
133
+
134
+ TMP_BASE="${TMPDIR:-/tmp}"
135
+ TMP_ROOT="$(mktemp -d "$TMP_BASE/omnilane-provider-probe.XXXXXX")"
136
+ cleanup() {
137
+ local path
138
+ case "${TMP_ROOT:-}" in
139
+ "$TMP_BASE"/omnilane-provider-probe.*)
140
+ if [[ -d "$TMP_ROOT" && ! -L "$TMP_ROOT" ]]; then
141
+ for path in "$TMP_ROOT"/* "$TMP_ROOT"/.[!.]*; do
142
+ [[ -f "$path" || -L "$path" ]] && rm -- "$path"
143
+ done
144
+ rmdir "$TMP_ROOT" 2>/dev/null || true
145
+ fi
146
+ ;;
147
+ esac
148
+ }
149
+ trap cleanup EXIT HUP INT TERM
150
+ PROMPT_FILE="$TMP_ROOT/prompt.txt"
151
+ OUTPUT_FILE="$TMP_ROOT/output.txt"
152
+ (umask 077; printf '%s\n' \
153
+ 'Reply with exactly OMNILANE_PROVIDER_PROBE_OK. Do not use tools or inspect files.' \
154
+ > "$PROMPT_FILE")
155
+
156
+ started="$(date +%s)"
157
+ set +e
158
+ OMNILANE_TIMEOUT="$TIMEOUT" "$RUNNER" advise \
159
+ "${OMNILANE_PROBE_WORKDIR:-$PWD}" "$MODEL" "$EFFORT" \
160
+ "$PROMPT_FILE" "$OUTPUT_FILE" >/dev/null 2>&1
161
+ runner_rc=$?
162
+ set -e
163
+ ended="$(date +%s)"
164
+ duration=$((ended - started))
165
+ response_bytes=0
166
+ if [[ -f "$OUTPUT_FILE" && ! -L "$OUTPUT_FILE" ]]; then
167
+ response_bytes="$(wc -c < "$OUTPUT_FILE" | tr -d '[:space:]')"
168
+ [[ "$response_bytes" =~ ^[0-9]+$ ]] || response_bytes=0
169
+ fi
170
+
171
+ if [[ "$runner_rc" -eq 0 && "$response_bytes" -gt 0 ]]; then
172
+ emit_result true usable true "bounded live inference returned output" "$duration" "$response_bytes"
173
+ exit 0
174
+ fi
175
+ if [[ "$runner_rc" -eq 124 || "$runner_rc" -eq 142 ]]; then
176
+ emit_result false timeout true "bounded live inference timed out" "$duration" "$response_bytes"
177
+ else
178
+ emit_result false failed true "runner exited with status $runner_rc" "$duration" "$response_bytes"
179
+ fi
180
+ exit 1
@@ -285,6 +285,43 @@ if [[ "$require_tag" -eq 1 ]]; then
285
285
  else
286
286
  fail annotated-release-tag
287
287
  fi
288
+
289
+ # A pushed tag with no GitHub release is invisible to anyone browsing the
290
+ # releases page, and nothing else in this audit notices: v0.11.0 sat that way
291
+ # for a week. Warn rather than fail — this audit must work offline and in CI,
292
+ # and the target's own release legitimately does not exist yet at audit time,
293
+ # so the target tag is excluded and only older tags are checked.
294
+ release_gap_checked=0
295
+ if command -v gh >/dev/null 2>&1 &&
296
+ git -C "$ROOT" remote get-url origin 2>/dev/null | grep -q 'github\.com' &&
297
+ gh auth status >/dev/null 2>&1; then
298
+ remote_tags="$(git -C "$ROOT" ls-remote --tags origin 2>/dev/null |
299
+ grep -v '\^{}$' | sed -n 's#.*refs/tags/##p' | sort -u)"
300
+ if [[ -n "$remote_tags" ]]; then
301
+ released_tags="$(gh release list --limit 200 --json tagName --jq '.[].tagName' 2>/dev/null || true)"
302
+ if [[ -n "$released_tags" ]]; then
303
+ release_gap_checked=1
304
+ missing_releases=""
305
+ # Only the few most recent tags. Older gaps are real but historical, and
306
+ # a check that stays yellow forever is a check everyone learns to ignore;
307
+ # this one exists to catch the release you just forgot.
308
+ while IFS= read -r remote_tag; do
309
+ [[ -n "$remote_tag" ]] || continue
310
+ [[ "$remote_tag" == "v$target" ]] && continue
311
+ printf '%s\n' "$remote_tags" | grep -Fxq "$remote_tag" || continue
312
+ if ! printf '%s\n' "$released_tags" | grep -Fxq "$remote_tag"; then
313
+ missing_releases="$missing_releases $remote_tag"
314
+ fi
315
+ done <<<"$(git -C "$ROOT" tag -l --sort=-creatordate | head -6)"
316
+ if [[ -n "$missing_releases" ]]; then
317
+ warn "github-release-missing:${missing_releases# }"
318
+ else
319
+ pass github-release-coverage
320
+ fi
321
+ fi
322
+ fi
323
+ fi
324
+ [[ "$release_gap_checked" -eq 1 ]] || warn github-release-check-skipped
288
325
  else
289
326
  warn release-tag-not-required
290
327
  fi
package/ui/app.js CHANGED
@@ -21,6 +21,7 @@
21
21
  connectionLabel: document.getElementById("connection-label"),
22
22
  languageSelect: document.getElementById("language-select"),
23
23
  jobCount: document.getElementById("job-count"), search: document.getElementById("job-search"),
24
+ exportVisible: document.getElementById("export-visible"),
24
25
  filter: document.getElementById("status-filter"),
25
26
  filterButtons: Array.from(document.querySelectorAll(".filter-button")),
26
27
  jobList: document.getElementById("job-list"), listMessage: document.getElementById("list-message"),
@@ -296,6 +297,7 @@
296
297
 
297
298
  function setControlsDisabled(disabled) {
298
299
  elements.search.disabled = disabled;
300
+ elements.exportVisible.disabled = disabled;
299
301
  elements.filterButtons.forEach(function (button) {
300
302
  button.disabled = disabled;
301
303
  });
@@ -471,6 +473,52 @@
471
473
  });
472
474
  }
473
475
 
476
+ function exportField(meta, key) {
477
+ return typeof meta[key] === "string" && meta[key].length > 0 ? meta[key] : null;
478
+ }
479
+
480
+ function exportVisibleJobs() {
481
+ const jobs = visibleJobs();
482
+ if (jobs.length === 0) {
483
+ return;
484
+ }
485
+ const exportedAt = new Date();
486
+ const payload = {
487
+ schema_version: 1,
488
+ exported_at: exportedAt.toISOString(),
489
+ query: state.query,
490
+ filter: state.filter,
491
+ jobs: jobs.map(function (job) {
492
+ const meta = job.meta;
493
+ return {
494
+ id: job.id,
495
+ state: job.state,
496
+ exit_code: job.exitCode,
497
+ lane: exportField(meta, "lane"),
498
+ vendor: exportField(meta, "vendor"),
499
+ model: exportField(meta, "model"),
500
+ effort: exportField(meta, "effort"),
501
+ mode: exportField(meta, "mode"),
502
+ candidate: exportField(meta, "candidate"),
503
+ started: exportField(meta, "started"),
504
+ timeout: Number.isInteger(meta.timeout) ? meta.timeout : null,
505
+ };
506
+ }),
507
+ };
508
+ const blob = new Blob([JSON.stringify(payload, null, 2) + "\n"], {
509
+ type: "application/json;charset=utf-8",
510
+ });
511
+ const url = URL.createObjectURL(blob);
512
+ const stamp = exportedAt.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
513
+ const anchor = document.createElement("a");
514
+ anchor.href = url;
515
+ anchor.download = "omnilane-jobs-" + stamp + ".json";
516
+ document.body.appendChild(anchor);
517
+ anchor.click();
518
+ anchor.remove();
519
+ window.setTimeout(function () { URL.revokeObjectURL(url); }, 0);
520
+ }
521
+
474
522
  function routeLabel(job) {
475
523
  const meta = job.meta;
476
524
  return [
@@ -525,6 +573,7 @@
525
573
 
526
574
  function renderQueue() {
527
575
  const jobs = visibleJobs();
576
+ elements.exportVisible.disabled = state.unauthorized || jobs.length === 0;
528
577
  const listScroll = elements.jobList.scrollTop;
529
578
  const inspectorScroll = elements.inspector.scrollTop;
530
579
  const focusedCard = document.activeElement && document.activeElement.closest
@@ -1270,6 +1319,8 @@
1270
1319
  updateFilterSelection();
1271
1320
  });
1272
1321
 
1322
+ elements.exportVisible.addEventListener("click", exportVisibleJobs);
1323
+
1273
1324
  elements.mobileBack.addEventListener("click", function () {
1274
1325
  returnToMobileList(true);
1275
1326
  });
@@ -1327,6 +1378,7 @@
1327
1378
  "filter.active": "Active",
1328
1379
  "filter.done": "Done",
1329
1380
  "filter.issues": "Issues",
1381
+ "export.visible": "Export visible",
1330
1382
  "list.ariaLabel": "Recent tasks",
1331
1383
  "list.waiting": "Waiting for tasks.",
1332
1384
  "list.empty": "No tasks yet.",
@@ -1438,6 +1490,7 @@
1438
1490
  "filter.active": "実行中",
1439
1491
  "filter.done": "完了",
1440
1492
  "filter.issues": "問題あり",
1493
+ "export.visible": "表示中をエクスポート",
1441
1494
  "list.ariaLabel": "最近のタスク",
1442
1495
  "list.waiting": "タスクを待機しています。",
1443
1496
  "list.empty": "タスクはまだありません。",
@@ -1549,6 +1602,7 @@
1549
1602
  "filter.active": "진행 중",
1550
1603
  "filter.done": "완료",
1551
1604
  "filter.issues": "문제",
1605
+ "export.visible": "표시 항목 내보내기",
1552
1606
  "list.ariaLabel": "최근 작업",
1553
1607
  "list.waiting": "작업을 기다리는 중입니다.",
1554
1608
  "list.empty": "아직 작업이 없습니다.",
@@ -1660,6 +1714,7 @@
1660
1714
  "filter.active": "進行中",
1661
1715
  "filter.done": "完成",
1662
1716
  "filter.issues": "有問題",
1717
+ "export.visible": "匯出目前結果",
1663
1718
  "list.ariaLabel": "最近的任務",
1664
1719
  "list.waiting": "等待任務中。",
1665
1720
  "list.empty": "尚無任務。",
@@ -1771,6 +1826,7 @@
1771
1826
  "filter.active": "进行中",
1772
1827
  "filter.done": "完成",
1773
1828
  "filter.issues": "有问题",
1829
+ "export.visible": "导出当前结果",
1774
1830
  "list.ariaLabel": "最近的任务",
1775
1831
  "list.waiting": "等待任务中。",
1776
1832
  "list.empty": "暂无任务。",
package/ui/index.html CHANGED
@@ -39,6 +39,7 @@
39
39
  <p class="section-label" data-i18n="index.sectionLabel">Recent work</p>
40
40
  <h1 id="dispatch-index-title" data-i18n="index.title">Task record</h1>
41
41
  </div>
42
+ <button type="button" class="export-button" id="export-visible" data-i18n="export.visible">Export visible</button>
42
43
  </div>
43
44
 
44
45
  <div class="queue-controls">
package/ui/styles.css CHANGED
@@ -210,6 +210,28 @@ summary:focus-visible,
210
210
  border-bottom: 1px solid var(--line);
211
211
  }
212
212
 
213
+ .export-button {
214
+ min-height: 36px;
215
+ margin-left: auto;
216
+ border: 1px solid var(--line-strong);
217
+ border-radius: 4px;
218
+ padding: 0 10px;
219
+ background: var(--paper);
220
+ color: var(--muted);
221
+ font-size: 12px;
222
+ font-weight: 650;
223
+ }
224
+
225
+ .export-button:hover:not(:disabled) {
226
+ color: var(--ink);
227
+ background: #edf0f2;
228
+ }
229
+
230
+ .export-button:disabled {
231
+ cursor: not-allowed;
232
+ opacity: 0.5;
233
+ }
234
+
213
235
  .section-label,
214
236
  .state-code {
215
237
  margin: 0 0 3px;