omnilane 0.7.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/CHANGELOG.md +209 -0
- package/LICENSE +21 -0
- package/README.ja.md +345 -0
- package/README.ko.md +337 -0
- package/README.md +368 -0
- package/README.zh-CN.md +317 -0
- package/README.zh-TW.md +327 -0
- package/SECURITY.md +37 -0
- package/VERSION +1 -0
- package/bin/omnilane +72 -0
- package/completions/_omnilane +114 -0
- package/completions/omnilane.bash +123 -0
- package/local.sh.example +37 -0
- package/package.json +54 -0
- package/routing.local.yaml.example +41 -0
- package/routing.yaml +34 -0
- package/scripts/configure.sh +134 -0
- package/scripts/dispatch.sh +705 -0
- package/scripts/doctor.sh +166 -0
- package/scripts/jobs.sh +802 -0
- package/scripts/lib/common.sh +247 -0
- package/scripts/lib/i18n.sh +104 -0
- package/scripts/lib/job-timeout.pl +109 -0
- package/scripts/lib/job-worker.sh +23 -0
- package/scripts/release-audit.sh +314 -0
- package/scripts/runners/run-claude.sh +37 -0
- package/scripts/runners/run-codex.sh +51 -0
- package/scripts/runners/run-exec.sh +33 -0
- package/scripts/runners/run-gemini.sh +60 -0
- package/scripts/runners/run-grok.sh +56 -0
- package/scripts/runners/run-vote.sh +119 -0
- package/scripts/ui.py +1236 -0
- package/ui/app.js +1106 -0
- package/ui/index.html +192 -0
- package/ui/styles.css +1023 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane dispatch — one routing table, any harness.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# dispatch.sh [--background] [--dry-run] [--mode advise|work] [--workdir DIR]
|
|
7
|
+
# [--vendor V] [--model M] [--effort E] [--timeout SECONDS]
|
|
8
|
+
# [--job-timeout SECONDS] LANE "TASK TEXT"
|
|
9
|
+
# dispatch.sh [--json] --list [--json]
|
|
10
|
+
# dispatch.sh [--json] --explain LANE [--json]
|
|
11
|
+
# dispatch.sh [--json] --validate [--json]
|
|
12
|
+
#
|
|
13
|
+
# TASK TEXT of "-" reads the task from stdin.
|
|
14
|
+
#
|
|
15
|
+
# A lane line may hold a fallback chain:
|
|
16
|
+
# lane: vendor model effort | vendor model effort | off
|
|
17
|
+
# The first candidate whose vendor CLI exists on this machine wins, so the same
|
|
18
|
+
# table degrades gracefully for people with fewer subscriptions.
|
|
19
|
+
#
|
|
20
|
+
# The watchdog caps EACH CLI invocation, highest priority first:
|
|
21
|
+
# --timeout SECONDS > OMNILANE_TIMEOUT_<LANE> > OMNILANE_TIMEOUT > 600
|
|
22
|
+
# The per-lane knob is the lane upper-cased with "-" turned into "_"
|
|
23
|
+
# (hard-judgment -> OMNILANE_TIMEOUT_HARD_JUDGMENT), so it can live in local.sh.
|
|
24
|
+
# This is a per-call hang-guard, NOT a whole-job budget: a retrying vendor
|
|
25
|
+
# (grok, up to OMNILANE_GROK_MAX_ATTEMPTS) or the vote panel (voters x rounds)
|
|
26
|
+
# spawns several CLI calls, so total wall-clock can be a multiple of this value.
|
|
27
|
+
# A separate --job-timeout can cap lock wait plus all calls in this dispatch.
|
|
28
|
+
|
|
29
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
|
30
|
+
|
|
31
|
+
MODE="advise"; WORKDIR="$PWD"; BACKGROUND=0; DRY_RUN=0
|
|
32
|
+
OVERRIDE_VENDOR=""; OVERRIDE_MODEL=""; OVERRIDE_EFFORT=""; OVERRIDE_TIMEOUT=""
|
|
33
|
+
OVERRIDE_JOB_TIMEOUT=""
|
|
34
|
+
|
|
35
|
+
usage_error() {
|
|
36
|
+
echo 'usage: dispatch.sh [--background] [--dry-run] [flags] LANE "TASK" | [--json] --list|--validate [--json] | [--json] --explain LANE [--json] | --help' >&2
|
|
37
|
+
exit 2
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
print_usage() {
|
|
41
|
+
cat <<'EOF'
|
|
42
|
+
usage: dispatch.sh [flags] LANE "TASK"
|
|
43
|
+
dispatch.sh [--json] --list | --explain LANE | --validate
|
|
44
|
+
dispatch.sh --help
|
|
45
|
+
|
|
46
|
+
Dispatch one task to the first available vendor CLI in LANE's fallback chain.
|
|
47
|
+
A TASK of "-" reads the task text from stdin.
|
|
48
|
+
|
|
49
|
+
flags:
|
|
50
|
+
--background run in the background and print the JOB_ID
|
|
51
|
+
--dry-run print the fully resolved dispatch plan and stop
|
|
52
|
+
before any provider call or job state
|
|
53
|
+
--mode advise|work advise (read-only, default) or work (may edit files)
|
|
54
|
+
--workdir DIR working directory handed to the vendor CLI
|
|
55
|
+
--vendor V pin one configured vendor (codex|claude|grok|gemini)
|
|
56
|
+
--model M override the routed model
|
|
57
|
+
--effort E override the routed effort
|
|
58
|
+
--timeout SECONDS cap each CLI call (default 600)
|
|
59
|
+
--job-timeout SECONDS cap the whole dispatch (lock wait plus all calls)
|
|
60
|
+
|
|
61
|
+
read-only queries (no provider call, no job state; --json for one envelope):
|
|
62
|
+
--list effective routing table (local overrides win)
|
|
63
|
+
--explain LANE candidate availability for one lane
|
|
64
|
+
--validate lint the effective routing table
|
|
65
|
+
--help, -h this help
|
|
66
|
+
EOF
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
json_escape() {
|
|
70
|
+
local s="$1" out="" ch escaped code i
|
|
71
|
+
for ((i = 0; i < ${#s}; i++)); do
|
|
72
|
+
ch="${s:i:1}"
|
|
73
|
+
case "$ch" in
|
|
74
|
+
'"') out="$out\\\"" ;;
|
|
75
|
+
'\\') out="$out\\\\" ;;
|
|
76
|
+
$'\b') out="$out\\b" ;;
|
|
77
|
+
$'\f') out="$out\\f" ;;
|
|
78
|
+
$'\n') out="$out\\n" ;;
|
|
79
|
+
$'\r') out="$out\\r" ;;
|
|
80
|
+
$'\t') out="$out\\t" ;;
|
|
81
|
+
*)
|
|
82
|
+
LC_CTYPE=C printf -v code '%d' "'$ch"
|
|
83
|
+
# Bash 3.2 on macOS reports UTF-8 bytes above 0x7f as signed values.
|
|
84
|
+
# Only non-negative C0 bytes are JSON control characters.
|
|
85
|
+
if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
|
|
86
|
+
printf -v escaped '\\u%04x' "$code"
|
|
87
|
+
out="$out$escaped"
|
|
88
|
+
else
|
|
89
|
+
out="$out$ch"
|
|
90
|
+
fi
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
printf '%s' "$out"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Run one read-only inspection command once, preserving its human output and
|
|
98
|
+
# exit status inside a stable JSON envelope. This avoids a second routing
|
|
99
|
+
# implementation drifting from the human CLI contract.
|
|
100
|
+
emit_json_inspection() {
|
|
101
|
+
local command="$1" output rc ok=false first=1 line
|
|
102
|
+
shift
|
|
103
|
+
if output="$("$@" 2>&1)"; then rc=0; else rc=$?; fi
|
|
104
|
+
[[ "$rc" -eq 0 ]] && ok=true
|
|
105
|
+
printf '{"schema_version":1,"command":"%s","ok":%s,"exit_code":%d,"lines":[' \
|
|
106
|
+
"$(json_escape "$command")" "$ok" "$rc"
|
|
107
|
+
if [[ -n "$output" ]]; then
|
|
108
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
109
|
+
[[ "$first" -eq 1 ]] || printf ','
|
|
110
|
+
first=0
|
|
111
|
+
printf '"%s"' "$(json_escape "$line")"
|
|
112
|
+
done <<< "$output"
|
|
113
|
+
fi
|
|
114
|
+
printf ']}\n'
|
|
115
|
+
exit "$rc"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
print_dry_run_value() {
|
|
119
|
+
printf '%s=' "$1"
|
|
120
|
+
printf '%q\n' "$2"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
print_dry_run_plan() {
|
|
124
|
+
local background=no task_source=argument write_worktree=no job_timeout=disabled
|
|
125
|
+
[[ "$BACKGROUND" -eq 1 ]] && background=yes
|
|
126
|
+
[[ "$TASK" == "-" ]] && task_source=stdin
|
|
127
|
+
[[ "$MODE" == "work" ]] && write_worktree=yes
|
|
128
|
+
[[ -n "$JOB_TIMEOUT" ]] && job_timeout="$JOB_TIMEOUT"
|
|
129
|
+
printf 'dry_run=yes\n'
|
|
130
|
+
print_dry_run_value lane "$LANE"
|
|
131
|
+
print_dry_run_value vendor "$VENDOR"
|
|
132
|
+
print_dry_run_value model "$MODEL"
|
|
133
|
+
print_dry_run_value effort "$EFFORT"
|
|
134
|
+
print_dry_run_value mode "$MODE"
|
|
135
|
+
print_dry_run_value workdir "$WORKDIR"
|
|
136
|
+
printf 'timeout=%s\n' "$TIMEOUT"
|
|
137
|
+
printf 'job_timeout=%s\n' "$job_timeout"
|
|
138
|
+
printf 'candidate=%s/%s\n' "$RESOLVED_IDX" "$RESOLVED_TOTAL"
|
|
139
|
+
printf 'background=%s\n' "$background"
|
|
140
|
+
printf 'task_source=%s\n' "$task_source"
|
|
141
|
+
printf 'provider_invoked=no\n'
|
|
142
|
+
printf 'job_state_created=no\n'
|
|
143
|
+
printf 'would_invoke_provider=yes\n'
|
|
144
|
+
printf 'would_create_job=yes\n'
|
|
145
|
+
printf 'would_write_worktree=%s\n' "$write_worktree"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
raw_lane_line() { # LANE -> chain text (comments stripped); local file wins
|
|
149
|
+
local lane="$1" f line
|
|
150
|
+
for f in "$OMNILANE_HOME/routing.local.yaml" "$OMNILANE_REPO/routing.yaml"; do
|
|
151
|
+
[[ -f "$f" ]] || continue
|
|
152
|
+
line="$(grep -E "^${lane}:" "$f" | head -1 | sed 's/#.*$//' | cut -d: -f2-)" || true
|
|
153
|
+
[[ -n "${line// /}" ]] && { printf '%s\n' "$line"; return 0; }
|
|
154
|
+
done
|
|
155
|
+
return 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Split one routing segment without invoking the shell. Double quotes group a
|
|
159
|
+
# model containing spaces; every other character is literal data.
|
|
160
|
+
parse_lane_segment() {
|
|
161
|
+
local input="$1" token="" ch i in_quote=0 have_token=0
|
|
162
|
+
PARSED_FIELDS=()
|
|
163
|
+
for ((i = 0; i < ${#input}; i++)); do
|
|
164
|
+
ch="${input:i:1}"
|
|
165
|
+
case "$ch" in
|
|
166
|
+
'"')
|
|
167
|
+
if [[ "$in_quote" -eq 1 ]]; then in_quote=0; else in_quote=1; fi
|
|
168
|
+
have_token=1
|
|
169
|
+
;;
|
|
170
|
+
' '|$'\t')
|
|
171
|
+
if [[ "$in_quote" -eq 1 ]]; then
|
|
172
|
+
token="$token$ch"
|
|
173
|
+
elif [[ "$have_token" -eq 1 ]]; then
|
|
174
|
+
PARSED_FIELDS+=("$token"); token=""; have_token=0
|
|
175
|
+
fi
|
|
176
|
+
;;
|
|
177
|
+
*) token="$token$ch"; have_token=1 ;;
|
|
178
|
+
esac
|
|
179
|
+
done
|
|
180
|
+
[[ "$in_quote" -eq 0 ]] || return 1
|
|
181
|
+
[[ "$have_token" -eq 1 ]] && PARSED_FIELDS+=("$token")
|
|
182
|
+
[[ ${#PARSED_FIELDS[@]} -gt 0 ]]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
routing_candidate_available() {
|
|
186
|
+
local vendor="$1" model="${2:-}" script
|
|
187
|
+
if [[ "$vendor" == "exec" ]]; then
|
|
188
|
+
script="$(expand_home_path "$model")"
|
|
189
|
+
[[ -n "$script" && -f "$script" && -x "$script" ]]
|
|
190
|
+
else
|
|
191
|
+
vendor_available "$vendor"
|
|
192
|
+
fi
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Pick the first candidate whose vendor CLI is present ("off" always matches).
|
|
196
|
+
# Sets RESOLVED_SPEC / RESOLVED_FIELDS / RESOLVED_IDX / RESOLVED_TOTAL.
|
|
197
|
+
resolve_chain() {
|
|
198
|
+
local chain="$1" requested_vendor="${2:-}" seg i=0 vendor
|
|
199
|
+
RESOLVED_SPEC=""; RESOLVED_IDX=0; RESOLVED_TOTAL=0; RESOLVED_FIELDS=()
|
|
200
|
+
local SEGS=() F=()
|
|
201
|
+
IFS='|' read -ra SEGS <<< "$chain"
|
|
202
|
+
RESOLVED_TOTAL="${#SEGS[@]}"
|
|
203
|
+
# ${SEGS[@]} on an empty chain is an unbound-variable error under set -u on
|
|
204
|
+
# Bash 3.2 and would abort --list mid-table; the guard makes it iterate zero
|
|
205
|
+
# times instead.
|
|
206
|
+
for seg in ${SEGS[@]+"${SEGS[@]}"}; do
|
|
207
|
+
i=$((i + 1))
|
|
208
|
+
[[ -n "${seg// /}" ]] || continue
|
|
209
|
+
parse_lane_segment "$seg" || {
|
|
210
|
+
echo "omnilane: malformed quoted routing segment: $seg" >&2
|
|
211
|
+
return 2
|
|
212
|
+
}
|
|
213
|
+
F=("${PARSED_FIELDS[@]}")
|
|
214
|
+
vendor="${F[0]:-}"
|
|
215
|
+
if [[ -n "$requested_vendor" ]]; then
|
|
216
|
+
[[ "$vendor" == "$requested_vendor" ]] || continue
|
|
217
|
+
RESOLVED_SPEC="$seg"; RESOLVED_FIELDS=("${F[@]}"); RESOLVED_IDX="$i"
|
|
218
|
+
if routing_candidate_available "$vendor" "${F[1]:-}"; then return 0; fi
|
|
219
|
+
return 4
|
|
220
|
+
fi
|
|
221
|
+
if [[ "$vendor" == "off" ]] || routing_candidate_available "$vendor" "${F[1]:-}"; then
|
|
222
|
+
RESOLVED_SPEC="$seg"; RESOLVED_FIELDS=("${F[@]}")
|
|
223
|
+
RESOLVED_IDX="$i"; return 0
|
|
224
|
+
fi
|
|
225
|
+
done
|
|
226
|
+
[[ -n "$requested_vendor" ]] && return 5
|
|
227
|
+
return 1
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
print_effective_routing() {
|
|
231
|
+
local seen=" " f line lane chain spec note
|
|
232
|
+
for f in "$OMNILANE_HOME/routing.local.yaml" "$OMNILANE_REPO/routing.yaml"; do
|
|
233
|
+
[[ -f "$f" ]] || continue
|
|
234
|
+
while IFS= read -r line; do
|
|
235
|
+
[[ "$line" =~ ^([a-z][a-z0-9-]*): ]] || continue
|
|
236
|
+
lane="${BASH_REMATCH[1]}"
|
|
237
|
+
[[ "$seen" == *" $lane "* ]] && continue
|
|
238
|
+
seen="$seen$lane "
|
|
239
|
+
chain="${line%%#*}"; chain="${chain#*:}"
|
|
240
|
+
if resolve_chain "$chain"; then
|
|
241
|
+
spec="$(printf '%s' "$RESOLVED_SPEC" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
242
|
+
note=""
|
|
243
|
+
[[ "$RESOLVED_IDX" -gt 1 ]] && note=" # fallback ($RESOLVED_IDX/$RESOLVED_TOTAL)"
|
|
244
|
+
printf '%-17s%s%s\n' "$lane:" "$spec" "$note"
|
|
245
|
+
else
|
|
246
|
+
printf '%-17s%s\n' "$lane:" "unavailable # no vendor CLI found in chain"
|
|
247
|
+
fi
|
|
248
|
+
done < "$f"
|
|
249
|
+
done
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
explain_lane() {
|
|
253
|
+
local lane="$1" chain seg i=0 total selected=0 vendor model effort status
|
|
254
|
+
local available=0
|
|
255
|
+
local SEGS=() F=()
|
|
256
|
+
[[ "$lane" =~ ^[a-z][a-z0-9-]*$ ]] || {
|
|
257
|
+
echo "omnilane: invalid lane name" >&2
|
|
258
|
+
return 2
|
|
259
|
+
}
|
|
260
|
+
chain="$(raw_lane_line "$lane")" || {
|
|
261
|
+
echo "omnilane: unknown lane '$lane' (try --list)" >&2
|
|
262
|
+
return 2
|
|
263
|
+
}
|
|
264
|
+
IFS='|' read -ra SEGS <<< "$chain"
|
|
265
|
+
total="${#SEGS[@]}"
|
|
266
|
+
printf 'lane: %s\n' "$lane"
|
|
267
|
+
for seg in "${SEGS[@]}"; do
|
|
268
|
+
i=$((i + 1))
|
|
269
|
+
[[ -n "${seg// /}" ]] || continue
|
|
270
|
+
parse_lane_segment "$seg" || {
|
|
271
|
+
echo "omnilane: malformed quoted routing segment: $seg" >&2
|
|
272
|
+
return 2
|
|
273
|
+
}
|
|
274
|
+
F=("${PARSED_FIELDS[@]}")
|
|
275
|
+
vendor="${F[0]:-}"
|
|
276
|
+
model="${F[1]:--}"
|
|
277
|
+
effort="${F[2]:--}"
|
|
278
|
+
available=0
|
|
279
|
+
status="unavailable"
|
|
280
|
+
if [[ "$vendor" == "off" ]]; then
|
|
281
|
+
available=1
|
|
282
|
+
status="available-disabled"
|
|
283
|
+
elif routing_candidate_available "$vendor" "$model"; then
|
|
284
|
+
available=1
|
|
285
|
+
status="available"
|
|
286
|
+
fi
|
|
287
|
+
if [[ "$available" -eq 1 && "$selected" -eq 0 ]]; then
|
|
288
|
+
selected="$i"
|
|
289
|
+
if [[ "$vendor" == "off" ]]; then status="selected-disabled"; else status="selected"; fi
|
|
290
|
+
elif [[ "$available" -eq 1 ]]; then
|
|
291
|
+
status="available-not-selected"
|
|
292
|
+
fi
|
|
293
|
+
printf 'candidate %d: vendor=%s model=%s effort=%s status=%s\n' \
|
|
294
|
+
"$i" "$vendor" "$model" "$effort" "$status"
|
|
295
|
+
done
|
|
296
|
+
if [[ "$selected" -gt 0 ]]; then
|
|
297
|
+
printf 'decision: candidate %d/%d\n' "$selected" "$total"
|
|
298
|
+
return 0
|
|
299
|
+
fi
|
|
300
|
+
printf 'decision: unavailable\n'
|
|
301
|
+
return 4
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
validate_routing() {
|
|
305
|
+
local effective_seen=" " file_seen f line content lane chain seg vendor
|
|
306
|
+
local line_no i total selected lane_invalid invalid=0 unreachable=0
|
|
307
|
+
local SEGS=() F=()
|
|
308
|
+
for f in "$OMNILANE_HOME/routing.local.yaml" "$OMNILANE_REPO/routing.yaml"; do
|
|
309
|
+
[[ -f "$f" ]] || continue
|
|
310
|
+
file_seen=" "
|
|
311
|
+
line_no=0
|
|
312
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
313
|
+
line_no=$((line_no + 1))
|
|
314
|
+
content="${line%%#*}"
|
|
315
|
+
[[ -n "${content//[[:space:]]/}" ]] || continue
|
|
316
|
+
if ! [[ "$content" =~ ^([a-z][a-z0-9-]*):[[:space:]]*(.*)$ ]]; then
|
|
317
|
+
printf 'FAIL line-%d invalid-line\n' "$line_no"
|
|
318
|
+
invalid=$((invalid + 1))
|
|
319
|
+
continue
|
|
320
|
+
fi
|
|
321
|
+
lane="${BASH_REMATCH[1]}"
|
|
322
|
+
chain="${BASH_REMATCH[2]}"
|
|
323
|
+
if [[ "$file_seen" == *" $lane "* ]]; then
|
|
324
|
+
printf 'FAIL %s duplicate-lane\n' "$lane"
|
|
325
|
+
invalid=$((invalid + 1))
|
|
326
|
+
continue
|
|
327
|
+
fi
|
|
328
|
+
file_seen="$file_seen$lane "
|
|
329
|
+
[[ "$effective_seen" == *" $lane "* ]] && continue
|
|
330
|
+
effective_seen="$effective_seen$lane "
|
|
331
|
+
IFS='|' read -ra SEGS <<< "$chain"
|
|
332
|
+
total="${#SEGS[@]}"
|
|
333
|
+
if [[ "$total" -eq 0 ]]; then
|
|
334
|
+
printf 'FAIL %s empty-chain\n' "$lane"
|
|
335
|
+
invalid=$((invalid + 1))
|
|
336
|
+
continue
|
|
337
|
+
fi
|
|
338
|
+
selected=0
|
|
339
|
+
lane_invalid=0
|
|
340
|
+
i=0
|
|
341
|
+
for seg in "${SEGS[@]}"; do
|
|
342
|
+
i=$((i + 1))
|
|
343
|
+
if [[ -z "${seg//[[:space:]]/}" ]]; then
|
|
344
|
+
printf 'FAIL %s candidate=%d empty-segment\n' "$lane" "$i"
|
|
345
|
+
lane_invalid=1
|
|
346
|
+
break
|
|
347
|
+
fi
|
|
348
|
+
if ! parse_lane_segment "$seg"; then
|
|
349
|
+
printf 'FAIL %s candidate=%d malformed-quotes\n' "$lane" "$i"
|
|
350
|
+
lane_invalid=1
|
|
351
|
+
break
|
|
352
|
+
fi
|
|
353
|
+
F=("${PARSED_FIELDS[@]}")
|
|
354
|
+
vendor="${F[0]:-}"
|
|
355
|
+
if [[ "$vendor" == "off" ]]; then
|
|
356
|
+
if [[ "${#F[@]}" -ne 1 && "${#F[@]}" -ne 3 ]]; then
|
|
357
|
+
printf 'FAIL %s candidate=%d field-count=%d\n' "$lane" "$i" "${#F[@]}"
|
|
358
|
+
lane_invalid=1
|
|
359
|
+
break
|
|
360
|
+
fi
|
|
361
|
+
elif [[ "${#F[@]}" -ne 3 ]]; then
|
|
362
|
+
printf 'FAIL %s candidate=%d field-count=%d\n' "$lane" "$i" "${#F[@]}"
|
|
363
|
+
lane_invalid=1
|
|
364
|
+
break
|
|
365
|
+
fi
|
|
366
|
+
if ! [[ "$vendor" =~ ^(codex|claude|grok|gemini|exec|off)$ ]]; then
|
|
367
|
+
printf 'FAIL %s unknown-vendor=%s\n' "$lane" "$vendor"
|
|
368
|
+
lane_invalid=1
|
|
369
|
+
break
|
|
370
|
+
fi
|
|
371
|
+
if printf '%s%s%s' "${F[0]}" "${F[1]:-}" "${F[2]:-}" | LC_ALL=C grep -q '[[:cntrl:]]'; then
|
|
372
|
+
printf 'FAIL %s candidate=%d control-character\n' "$lane" "$i"
|
|
373
|
+
lane_invalid=1
|
|
374
|
+
break
|
|
375
|
+
fi
|
|
376
|
+
if [[ "$selected" -eq 0 ]] && { [[ "$vendor" == "off" ]] || routing_candidate_available "$vendor" "${F[1]}"; }; then
|
|
377
|
+
selected="$i"
|
|
378
|
+
fi
|
|
379
|
+
done
|
|
380
|
+
if [[ "$lane_invalid" -eq 1 ]]; then
|
|
381
|
+
invalid=$((invalid + 1))
|
|
382
|
+
elif [[ "$selected" -eq 0 ]]; then
|
|
383
|
+
printf 'WARN %s no-candidate-available\n' "$lane"
|
|
384
|
+
unreachable=$((unreachable + 1))
|
|
385
|
+
else
|
|
386
|
+
parse_lane_segment "${SEGS[$((selected - 1))]}"
|
|
387
|
+
printf 'PASS %s selected=%d/%d vendor=%s\n' \
|
|
388
|
+
"$lane" "$selected" "$total" "${PARSED_FIELDS[0]}"
|
|
389
|
+
fi
|
|
390
|
+
done < "$f"
|
|
391
|
+
done
|
|
392
|
+
[[ "$invalid" -eq 0 ]] || return 2
|
|
393
|
+
[[ "$unreachable" -eq 0 ]] || return 4
|
|
394
|
+
return 0
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Inspection modes are deliberately parsed before dispatch flags. JSON may be
|
|
398
|
+
# placed before or after the inspection command, but can never decorate a real
|
|
399
|
+
# dispatch and therefore cannot accidentally create job state.
|
|
400
|
+
JSON_INSPECTION=0
|
|
401
|
+
if [[ "${1:-}" == "--json" ]]; then
|
|
402
|
+
JSON_INSPECTION=1
|
|
403
|
+
shift
|
|
404
|
+
[[ $# -gt 0 ]] || usage_error
|
|
405
|
+
fi
|
|
406
|
+
case "${1:-}" in
|
|
407
|
+
--help|-h)
|
|
408
|
+
[[ "$JSON_INSPECTION" -eq 0 && $# -eq 1 ]] || usage_error
|
|
409
|
+
print_usage
|
|
410
|
+
exit 0 ;;
|
|
411
|
+
--list)
|
|
412
|
+
if [[ "${2:-}" == "--json" ]]; then
|
|
413
|
+
[[ "$JSON_INSPECTION" -eq 0 && $# -eq 2 ]] || usage_error
|
|
414
|
+
JSON_INSPECTION=1
|
|
415
|
+
else
|
|
416
|
+
[[ $# -eq 1 ]] || usage_error
|
|
417
|
+
fi
|
|
418
|
+
if [[ "$JSON_INSPECTION" -eq 1 ]]; then
|
|
419
|
+
emit_json_inspection list print_effective_routing
|
|
420
|
+
fi
|
|
421
|
+
print_effective_routing
|
|
422
|
+
exit 0
|
|
423
|
+
;;
|
|
424
|
+
--explain)
|
|
425
|
+
[[ $# -ge 2 ]] || usage_error
|
|
426
|
+
if [[ "${3:-}" == "--json" ]]; then
|
|
427
|
+
[[ "$JSON_INSPECTION" -eq 0 && $# -eq 3 ]] || usage_error
|
|
428
|
+
JSON_INSPECTION=1
|
|
429
|
+
else
|
|
430
|
+
[[ $# -eq 2 ]] || usage_error
|
|
431
|
+
fi
|
|
432
|
+
if [[ "$JSON_INSPECTION" -eq 1 ]]; then
|
|
433
|
+
emit_json_inspection explain explain_lane "$2"
|
|
434
|
+
fi
|
|
435
|
+
explain_lane "$2"
|
|
436
|
+
exit $?
|
|
437
|
+
;;
|
|
438
|
+
--validate)
|
|
439
|
+
if [[ "${2:-}" == "--json" ]]; then
|
|
440
|
+
[[ "$JSON_INSPECTION" -eq 0 && $# -eq 2 ]] || usage_error
|
|
441
|
+
JSON_INSPECTION=1
|
|
442
|
+
else
|
|
443
|
+
[[ $# -eq 1 ]] || usage_error
|
|
444
|
+
fi
|
|
445
|
+
if [[ "$JSON_INSPECTION" -eq 1 ]]; then
|
|
446
|
+
emit_json_inspection validate validate_routing
|
|
447
|
+
fi
|
|
448
|
+
validate_routing
|
|
449
|
+
exit $?
|
|
450
|
+
;;
|
|
451
|
+
*)
|
|
452
|
+
[[ "$JSON_INSPECTION" -eq 0 ]] || usage_error
|
|
453
|
+
;;
|
|
454
|
+
esac
|
|
455
|
+
|
|
456
|
+
while [[ $# -gt 0 ]]; do
|
|
457
|
+
case "$1" in
|
|
458
|
+
--list|--explain|--validate|--json) usage_error ;;
|
|
459
|
+
--background) BACKGROUND=1; shift ;;
|
|
460
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
461
|
+
--mode|--workdir|--vendor|--model|--effort|--timeout|--job-timeout)
|
|
462
|
+
# Value-taking flags: a missing value must be a clean usage error (exit 2),
|
|
463
|
+
# not a `set -u` "unbound variable" crash on $2.
|
|
464
|
+
[[ $# -ge 2 ]] || { echo "omnilane: $1 needs a value" >&2; exit 2; }
|
|
465
|
+
case "$1" in
|
|
466
|
+
--mode) MODE="$2" ;;
|
|
467
|
+
--workdir) WORKDIR="$2" ;;
|
|
468
|
+
--vendor) OVERRIDE_VENDOR="$2" ;;
|
|
469
|
+
--model) OVERRIDE_MODEL="$2" ;;
|
|
470
|
+
--effort) OVERRIDE_EFFORT="$2" ;;
|
|
471
|
+
--timeout) OVERRIDE_TIMEOUT="$2" ;;
|
|
472
|
+
--job-timeout) OVERRIDE_JOB_TIMEOUT="$2" ;;
|
|
473
|
+
esac
|
|
474
|
+
shift 2 ;;
|
|
475
|
+
-*) echo "omnilane: unknown flag" >&2; exit 2 ;;
|
|
476
|
+
*) break ;;
|
|
477
|
+
esac
|
|
478
|
+
done
|
|
479
|
+
|
|
480
|
+
[[ $# -ge 1 ]] || usage_error
|
|
481
|
+
[[ $# -ge 2 ]] || { echo "omnilane: missing task text (use - for stdin)" >&2; exit 2; }
|
|
482
|
+
[[ $# -eq 2 ]] || {
|
|
483
|
+
echo 'omnilane: unexpected extra arguments; quote a multiword task' >&2
|
|
484
|
+
exit 2
|
|
485
|
+
}
|
|
486
|
+
LANE="$1"
|
|
487
|
+
TASK="$2"
|
|
488
|
+
[[ "$LANE" =~ ^[a-z][a-z0-9-]*$ ]] || { echo "omnilane: invalid lane name" >&2; exit 2; }
|
|
489
|
+
# A typo like --mode advice must not fall through to the write-enabled branch.
|
|
490
|
+
[[ "$MODE" == "advise" || "$MODE" == "work" ]] || { echo "omnilane: invalid --mode (advise|work)" >&2; exit 2; }
|
|
491
|
+
if [[ -n "$OVERRIDE_VENDOR" ]] &&
|
|
492
|
+
! [[ "$OVERRIDE_VENDOR" =~ ^(codex|claude|grok|gemini)$ ]]; then
|
|
493
|
+
echo "omnilane: invalid vendor (codex|claude|grok|gemini)" >&2
|
|
494
|
+
exit 2
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
depth_guard
|
|
498
|
+
|
|
499
|
+
CHAIN="$(raw_lane_line "$LANE")" || { echo "omnilane: unknown lane '$LANE' (try --list)" >&2; exit 2; }
|
|
500
|
+
if [[ -n "$OVERRIDE_VENDOR" ]]; then
|
|
501
|
+
if resolve_chain "$CHAIN" "$OVERRIDE_VENDOR"; then
|
|
502
|
+
:
|
|
503
|
+
else
|
|
504
|
+
resolve_rc=$?
|
|
505
|
+
case "$resolve_rc" in
|
|
506
|
+
2) exit 2 ;;
|
|
507
|
+
4)
|
|
508
|
+
echo "omnilane: requested vendor '$OVERRIDE_VENDOR' is configured for lane '$LANE' but its CLI is unavailable" >&2
|
|
509
|
+
exit 4
|
|
510
|
+
;;
|
|
511
|
+
5)
|
|
512
|
+
echo "omnilane: requested vendor '$OVERRIDE_VENDOR' is not configured for lane '$LANE'" >&2
|
|
513
|
+
exit 2
|
|
514
|
+
;;
|
|
515
|
+
*)
|
|
516
|
+
echo "omnilane: could not resolve requested vendor '$OVERRIDE_VENDOR' for lane '$LANE'" >&2
|
|
517
|
+
exit 2
|
|
518
|
+
;;
|
|
519
|
+
esac
|
|
520
|
+
fi
|
|
521
|
+
else
|
|
522
|
+
resolve_chain "$CHAIN" || {
|
|
523
|
+
echo "omnilane: no vendor CLI available for lane '$LANE' (chain:$CHAIN)." >&2
|
|
524
|
+
echo "omnilane: install a vendor CLI or override the lane in ~/.omnilane/routing.local.yaml" >&2
|
|
525
|
+
exit 4
|
|
526
|
+
}
|
|
527
|
+
fi
|
|
528
|
+
|
|
529
|
+
FIELDS=("${RESOLVED_FIELDS[@]}")
|
|
530
|
+
VENDOR="${FIELDS[0]}"; MODEL="${FIELDS[1]:-}"; EFFORT="${FIELDS[2]:-}"
|
|
531
|
+
[[ -n "$OVERRIDE_MODEL" ]] && MODEL="$OVERRIDE_MODEL"
|
|
532
|
+
[[ -n "$OVERRIDE_EFFORT" ]] && EFFORT="$OVERRIDE_EFFORT"
|
|
533
|
+
|
|
534
|
+
if [[ "$VENDOR" == "off" ]]; then
|
|
535
|
+
echo "omnilane: lane '$LANE' is disabled in routing config" >&2; exit 3
|
|
536
|
+
fi
|
|
537
|
+
RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
|
|
538
|
+
[[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
|
|
539
|
+
|
|
540
|
+
# Watchdog seconds: --timeout > per-lane OMNILANE_TIMEOUT_<LANE> > OMNILANE_TIMEOUT > 600.
|
|
541
|
+
# Resolve here and export so every runner (and vote's sub-runners) inherits the
|
|
542
|
+
# same value without a per-runner code change; they already read OMNILANE_TIMEOUT.
|
|
543
|
+
# This bounds each runner CLI call, not the whole dispatch (see header note).
|
|
544
|
+
TIMEOUT="$OVERRIDE_TIMEOUT"
|
|
545
|
+
if [[ -z "$TIMEOUT" ]]; then
|
|
546
|
+
LANE_TIMEOUT_VAR="OMNILANE_TIMEOUT_$(printf '%s' "${LANE//-/_}" | tr '[:lower:]' '[:upper:]')"
|
|
547
|
+
TIMEOUT="${!LANE_TIMEOUT_VAR:-}"
|
|
548
|
+
fi
|
|
549
|
+
[[ -n "$TIMEOUT" ]] || TIMEOUT="${OMNILANE_TIMEOUT:-600}"
|
|
550
|
+
[[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || {
|
|
551
|
+
echo "omnilane: invalid timeout (want a positive integer of seconds)" >&2; exit 2
|
|
552
|
+
}
|
|
553
|
+
export OMNILANE_TIMEOUT="$TIMEOUT"
|
|
554
|
+
|
|
555
|
+
JOB_SUPERVISOR="$OMNILANE_REPO/scripts/lib/job-timeout.pl"
|
|
556
|
+
JOB_WORKER="$OMNILANE_REPO/scripts/lib/job-worker.sh"
|
|
557
|
+
|
|
558
|
+
# Optional whole-job seconds: flag > per-lane env > global env > automatic
|
|
559
|
+
# non-Git Codex work guard > disabled.
|
|
560
|
+
# Validate lexically only. Bash arithmetic recursively evaluates variable text,
|
|
561
|
+
# so untrusted timeout text must never enter [[ -gt ]] or $((...)).
|
|
562
|
+
JOB_TIMEOUT="$OVERRIDE_JOB_TIMEOUT"
|
|
563
|
+
if [[ -z "$JOB_TIMEOUT" ]]; then
|
|
564
|
+
LANE_JOB_TIMEOUT_VAR="OMNILANE_JOB_TIMEOUT_$(printf '%s' "${LANE//-/_}" | tr '[:lower:]' '[:upper:]')"
|
|
565
|
+
JOB_TIMEOUT="${!LANE_JOB_TIMEOUT_VAR:-}"
|
|
566
|
+
fi
|
|
567
|
+
[[ -n "$JOB_TIMEOUT" ]] || JOB_TIMEOUT="${OMNILANE_JOB_TIMEOUT:-}"
|
|
568
|
+
CODEX_NONGIT_WORK=0
|
|
569
|
+
CODEX_NONGIT_AUTO_JOB_TIMEOUT=0
|
|
570
|
+
if [[ "$VENDOR" == "codex" && "$MODE" == "work" ]]; then
|
|
571
|
+
# The target directory is authoritative. Caller-supplied GIT_* state must not
|
|
572
|
+
# redirect or corrupt discovery, so probe with a minimal clean environment.
|
|
573
|
+
GIT_WORKTREE_STATE="$(env -i PATH="$PATH" HOME="${HOME:-}" \
|
|
574
|
+
git -C "$WORKDIR" rev-parse --is-inside-work-tree 2>/dev/null || true)"
|
|
575
|
+
if [[ "$GIT_WORKTREE_STATE" != "true" ]]; then
|
|
576
|
+
CODEX_NONGIT_WORK=1
|
|
577
|
+
if [[ -z "$JOB_TIMEOUT" ]]; then
|
|
578
|
+
if [[ -f "$JOB_SUPERVISOR" ]] && command -v perl &>/dev/null &&
|
|
579
|
+
perl -MPOSIX=setsid -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC -e 'exit 0' \
|
|
580
|
+
>/dev/null 2>&1; then
|
|
581
|
+
CODEX_NONGIT_AUTO_JOB_TIMEOUT=1
|
|
582
|
+
# Preserve the per-call budget while adding process-group cleanup. The
|
|
583
|
+
# supervisor accepts at most nine digits, so larger values receive its
|
|
584
|
+
# effective ceiling rather than making non-Git work unavailable.
|
|
585
|
+
if [[ "$TIMEOUT" =~ ^[1-9][0-9]{0,8}$ ]]; then
|
|
586
|
+
JOB_TIMEOUT="$TIMEOUT"
|
|
587
|
+
else
|
|
588
|
+
JOB_TIMEOUT=999999999
|
|
589
|
+
fi
|
|
590
|
+
else
|
|
591
|
+
echo "omnilane: automatic non-Git Codex job guard is unavailable; continuing without a whole-job fuse (the per-call watchdog path remains)" >&2
|
|
592
|
+
fi
|
|
593
|
+
fi
|
|
594
|
+
fi
|
|
595
|
+
fi
|
|
596
|
+
if [[ -n "$JOB_TIMEOUT" && ! "$JOB_TIMEOUT" =~ ^[1-9][0-9]{0,8}$ ]]; then
|
|
597
|
+
echo "omnilane: invalid job timeout (want 1..999999999 seconds)" >&2
|
|
598
|
+
exit 2
|
|
599
|
+
fi
|
|
600
|
+
JOB_TIMEOUT_JSON="${JOB_TIMEOUT:-null}"
|
|
601
|
+
unset OMNILANE_JOB_SUPERVISED
|
|
602
|
+
[[ -x "$JOB_WORKER" ]] || { echo "omnilane: internal job worker is unavailable" >&2; exit 2; }
|
|
603
|
+
if [[ -n "$JOB_TIMEOUT" ]]; then
|
|
604
|
+
[[ -f "$JOB_SUPERVISOR" ]] || { echo "omnilane: whole-job timeout supervisor is unavailable" >&2; exit 2; }
|
|
605
|
+
command -v perl &>/dev/null || { echo "omnilane: --job-timeout requires perl" >&2; exit 2; }
|
|
606
|
+
perl -MPOSIX=setsid -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC -e 'exit 0' \
|
|
607
|
+
>/dev/null 2>&1 || { echo "omnilane: perl lacks whole-job timeout support" >&2; exit 2; }
|
|
608
|
+
fi
|
|
609
|
+
|
|
610
|
+
JOBS_ROOT="$OMNILANE_HOME/jobs"
|
|
611
|
+
if [[ -L "$JOBS_ROOT" || ( -e "$JOBS_ROOT" && ! -d "$JOBS_ROOT" ) ]]; then
|
|
612
|
+
echo "omnilane: unsafe jobs store path (want a real directory): $JOBS_ROOT" >&2
|
|
613
|
+
exit 1
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
617
|
+
print_dry_run_plan
|
|
618
|
+
exit 0
|
|
619
|
+
fi
|
|
620
|
+
|
|
621
|
+
mkdir -p "$OMNILANE_HOME"
|
|
622
|
+
if [[ ! -d "$JOBS_ROOT" ]]; then
|
|
623
|
+
mkdir -m 700 "$JOBS_ROOT"
|
|
624
|
+
fi
|
|
625
|
+
[[ -d "$JOBS_ROOT" && ! -L "$JOBS_ROOT" ]] || {
|
|
626
|
+
echo "omnilane: jobs store changed while preparing it" >&2
|
|
627
|
+
exit 1
|
|
628
|
+
}
|
|
629
|
+
# Prompts and model answers may contain private source code or credentials.
|
|
630
|
+
# Keep the privacy boundary on Omnilane's own job store instead of changing the
|
|
631
|
+
# runner umask, which would also affect files a model creates in --mode work.
|
|
632
|
+
chmod 700 "$JOBS_ROOT"
|
|
633
|
+
JOB_ID="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
|
|
634
|
+
JOB_DIR="$JOBS_ROOT/$JOB_ID"
|
|
635
|
+
mkdir -m 700 "$JOB_DIR"
|
|
636
|
+
|
|
637
|
+
if [[ "$TASK" == "-" ]]; then
|
|
638
|
+
(umask 077; cat > "$JOB_DIR/task.txt")
|
|
639
|
+
else
|
|
640
|
+
(umask 077; printf '%s\n' "$TASK" > "$JOB_DIR/task.txt")
|
|
641
|
+
fi
|
|
642
|
+
|
|
643
|
+
# meta "timeout" is the resolved per-CLI-call watchdog cap, not a whole-job total.
|
|
644
|
+
(umask 077; printf '{"lane":"%s","vendor":"%s","model":"%s","effort":"%s","timeout":%s,"job_timeout":%s,"mode":"%s","workdir":"%s","candidate":"%s/%s","started":"%s"}\n' \
|
|
645
|
+
"$(json_escape "$LANE")" "$(json_escape "$VENDOR")" "$(json_escape "$MODEL")" \
|
|
646
|
+
"$(json_escape "$EFFORT")" "$TIMEOUT" "$JOB_TIMEOUT_JSON" "$(json_escape "$MODE")" \
|
|
647
|
+
"$(json_escape "$WORKDIR")" "$RESOLVED_IDX" "$RESOLVED_TOTAL" \
|
|
648
|
+
"$(date -u +%FT%TZ)" > "$JOB_DIR/meta.json")
|
|
649
|
+
|
|
650
|
+
secure_job_files() {
|
|
651
|
+
find "$JOB_DIR" -type f -exec chmod 600 {} +
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
finish_job() {
|
|
655
|
+
local rc="$1"
|
|
656
|
+
secure_job_files
|
|
657
|
+
(umask 077; printf '%s\n' "$rc" > "$JOB_DIR/exit")
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
run_job() {
|
|
661
|
+
local rc=0
|
|
662
|
+
write_current_pid_file "$JOB_DIR/pid"
|
|
663
|
+
set +e
|
|
664
|
+
if [[ -n "$JOB_TIMEOUT" ]]; then
|
|
665
|
+
OMNILANE_JOB_SUPERVISED=1 perl "$JOB_SUPERVISOR" "$JOB_TIMEOUT" \
|
|
666
|
+
"$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
|
|
667
|
+
"$JOB_DIR/task.txt" "$JOB_DIR/out.txt"
|
|
668
|
+
else
|
|
669
|
+
"$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
|
|
670
|
+
"$JOB_DIR/task.txt" "$JOB_DIR/out.txt"
|
|
671
|
+
fi
|
|
672
|
+
rc=$?
|
|
673
|
+
set -e
|
|
674
|
+
if [[ "$CODEX_NONGIT_WORK" -eq 1 && "$rc" -eq 124 ]]; then
|
|
675
|
+
echo "omnilane: Codex work in a non-Git directory timed out after ${JOB_TIMEOUT}s; the supervised process group was terminated" >&2
|
|
676
|
+
elif [[ "$CODEX_NONGIT_AUTO_JOB_TIMEOUT" -eq 1 && "$rc" -eq 142 ]]; then
|
|
677
|
+
# Equal implicit inner/outer deadlines can race. Only the automatic case is
|
|
678
|
+
# normalized; an explicitly longer whole-job fuse must preserve status 142.
|
|
679
|
+
echo "omnilane: Codex work in a non-Git directory timed out after ${TIMEOUT}s; the supervised process group was terminated" >&2
|
|
680
|
+
rc=124
|
|
681
|
+
fi
|
|
682
|
+
finish_job "$rc"
|
|
683
|
+
return "$rc"
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if [[ "$BACKGROUND" == "1" ]]; then
|
|
687
|
+
# set -m gives the worker its own process group so it survives the caller's
|
|
688
|
+
# exit and group-wide signals; traps persist a best-effort exit code so
|
|
689
|
+
# jobs.sh never reports a killed worker as still running.
|
|
690
|
+
set -m
|
|
691
|
+
(umask 077; : > "$JOB_DIR/worker.log")
|
|
692
|
+
(
|
|
693
|
+
trap 'finish_job 129; exit 129' HUP
|
|
694
|
+
trap 'finish_job 143; exit 143' TERM
|
|
695
|
+
run_job
|
|
696
|
+
) < /dev/null > "$JOB_DIR/worker.log" 2>&1 &
|
|
697
|
+
disown
|
|
698
|
+
set +m
|
|
699
|
+
echo "$JOB_ID"
|
|
700
|
+
exit 0
|
|
701
|
+
fi
|
|
702
|
+
|
|
703
|
+
set +e; run_job; RC=$?; set -e
|
|
704
|
+
[[ -f "$JOB_DIR/out.txt" ]] && cat "$JOB_DIR/out.txt"
|
|
705
|
+
exit "$RC"
|