omnilane 0.20.0 → 0.31.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +37 -1
- package/README.ja.md +36 -3
- package/README.ko.md +36 -3
- package/README.md +38 -3
- package/README.zh-CN.md +36 -3
- package/README.zh-TW.md +36 -3
- package/VERSION +1 -1
- package/bin/omnilane +10 -1
- package/hooks/routing-instruction.md +16 -7
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/dispatch.sh +55 -7
- package/scripts/doctor.sh +26 -0
- package/scripts/jobs.sh +24 -12
- package/scripts/lib/goal-loop.sh +670 -0
- package/scripts/lib/job-worker.sh +98 -37
- package/scripts/lib/live-protocol.sh +70 -0
- package/scripts/runners/run-gemini.sh +109 -0
- package/skills/omnilane/SKILL.md +9 -4
package/scripts/dispatch.sh
CHANGED
|
@@ -3,9 +3,10 @@ set -euo pipefail
|
|
|
3
3
|
# omnilane dispatch — one routing table, any harness.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# dispatch.sh [--background] [--
|
|
6
|
+
# dispatch.sh [--background] [--live|--single-shot] [--dry-run]
|
|
7
|
+
# [--mode advise|work|sysops] [--workdir DIR]
|
|
7
8
|
# [--vendor V] [--model M] [--effort E] [--timeout SECONDS]
|
|
8
|
-
# [--job-timeout SECONDS] LANE "TASK TEXT"
|
|
9
|
+
# [--job-timeout SECONDS] [--idle-timeout SECONDS] LANE "TASK TEXT"
|
|
9
10
|
# dispatch.sh [--json] --list [--json]
|
|
10
11
|
# dispatch.sh [--json] --explain LANE [--json]
|
|
11
12
|
# dispatch.sh [--json] --validate [--json]
|
|
@@ -27,10 +28,12 @@ set -euo pipefail
|
|
|
27
28
|
# A separate --job-timeout can cap lock wait plus all calls in this dispatch.
|
|
28
29
|
|
|
29
30
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
|
31
|
+
# shellcheck disable=SC1091
|
|
32
|
+
source "$OMNILANE_REPO/scripts/lib/live-protocol.sh"
|
|
30
33
|
|
|
31
34
|
MODE="advise"; WORKDIR="$PWD"; BACKGROUND=0; DRY_RUN=0
|
|
32
35
|
OVERRIDE_VENDOR=""; OVERRIDE_MODEL=""; OVERRIDE_EFFORT=""; OVERRIDE_TIMEOUT=""
|
|
33
|
-
OVERRIDE_JOB_TIMEOUT=""
|
|
36
|
+
OVERRIDE_JOB_TIMEOUT=""; OVERRIDE_IDLE_TIMEOUT=""; SESSION_REQUEST="auto"
|
|
34
37
|
|
|
35
38
|
usage_error() {
|
|
36
39
|
echo 'usage: dispatch.sh [--background] [--dry-run] [flags] LANE "TASK" | [--json] --list|--validate [--json] | [--json] --explain LANE [--json] | --help' >&2
|
|
@@ -47,6 +50,9 @@ Dispatch one task to the first available vendor CLI in LANE's fallback chain.
|
|
|
47
50
|
A TASK of "-" reads the task text from stdin.
|
|
48
51
|
|
|
49
52
|
flags:
|
|
53
|
+
--live require a resident session (background only)
|
|
54
|
+
--single-shot force one-shot dispatch (background only)
|
|
55
|
+
--idle-timeout SECONDS close an idle live mailbox (default 900; 0 disables)
|
|
50
56
|
--background run in the background and print the JOB_ID
|
|
51
57
|
--dry-run print the fully resolved dispatch plan and stop
|
|
52
58
|
before any provider call or job state
|
|
@@ -139,6 +145,8 @@ print_dry_run_plan() {
|
|
|
139
145
|
print_dry_run_value workdir "$WORKDIR"
|
|
140
146
|
printf 'timeout=%s\n' "$TIMEOUT"
|
|
141
147
|
printf 'job_timeout=%s\n' "$job_timeout"
|
|
148
|
+
printf 'idle_timeout=%s\n' "$IDLE_TIMEOUT"
|
|
149
|
+
print_dry_run_value session_mode "$SESSION_MODE"
|
|
142
150
|
printf 'candidate=%s/%s\n' "$RESOLVED_IDX" "$RESOLVED_TOTAL"
|
|
143
151
|
printf 'background=%s\n' "$background"
|
|
144
152
|
printf 'task_source=%s\n' "$task_source"
|
|
@@ -461,8 +469,18 @@ while [[ $# -gt 0 ]]; do
|
|
|
461
469
|
case "$1" in
|
|
462
470
|
--list|--explain|--validate|--json) usage_error ;;
|
|
463
471
|
--background) BACKGROUND=1; shift ;;
|
|
472
|
+
--live)
|
|
473
|
+
[[ "$SESSION_REQUEST" != "single-shot" ]] || {
|
|
474
|
+
echo "omnilane: --live and --single-shot are mutually exclusive" >&2; exit 2
|
|
475
|
+
}
|
|
476
|
+
SESSION_REQUEST="live"; shift ;;
|
|
477
|
+
--single-shot)
|
|
478
|
+
[[ "$SESSION_REQUEST" != "live" ]] || {
|
|
479
|
+
echo "omnilane: --live and --single-shot are mutually exclusive" >&2; exit 2
|
|
480
|
+
}
|
|
481
|
+
SESSION_REQUEST="single-shot"; shift ;;
|
|
464
482
|
--dry-run) DRY_RUN=1; shift ;;
|
|
465
|
-
--mode|--workdir|--vendor|--model|--effort|--timeout|--job-timeout)
|
|
483
|
+
--mode|--workdir|--vendor|--model|--effort|--timeout|--job-timeout|--idle-timeout)
|
|
466
484
|
# Value-taking flags: a missing value must be a clean usage error (exit 2),
|
|
467
485
|
# not a `set -u` "unbound variable" crash on $2.
|
|
468
486
|
[[ $# -ge 2 ]] || { echo "omnilane: $1 needs a value" >&2; exit 2; }
|
|
@@ -474,6 +492,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
474
492
|
--effort) OVERRIDE_EFFORT="$2" ;;
|
|
475
493
|
--timeout) OVERRIDE_TIMEOUT="$2" ;;
|
|
476
494
|
--job-timeout) OVERRIDE_JOB_TIMEOUT="$2" ;;
|
|
495
|
+
--idle-timeout) OVERRIDE_IDLE_TIMEOUT="$2" ;;
|
|
477
496
|
esac
|
|
478
497
|
shift 2 ;;
|
|
479
498
|
-*) echo "omnilane: unknown flag" >&2; exit 2 ;;
|
|
@@ -487,6 +506,11 @@ done
|
|
|
487
506
|
echo 'omnilane: unexpected extra arguments; quote a multiword task' >&2
|
|
488
507
|
exit 2
|
|
489
508
|
}
|
|
509
|
+
if [[ "$SESSION_REQUEST" != "auto" && "$BACKGROUND" -ne 1 ]]; then
|
|
510
|
+
echo "omnilane: --${SESSION_REQUEST} requires --background" >&2
|
|
511
|
+
exit 2
|
|
512
|
+
fi
|
|
513
|
+
|
|
490
514
|
LANE="$1"
|
|
491
515
|
TASK="$2"
|
|
492
516
|
[[ "$LANE" =~ ^[a-z][a-z0-9-]*$ ]] || { echo "omnilane: invalid lane name" >&2; exit 2; }
|
|
@@ -541,6 +565,21 @@ fi
|
|
|
541
565
|
RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
|
|
542
566
|
[[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
|
|
543
567
|
|
|
568
|
+
SESSION_MODE="single-shot"
|
|
569
|
+
if [[ "$SESSION_REQUEST" != "single-shot" ]] && live_vendor_capable "$VENDOR"; then
|
|
570
|
+
SESSION_MODE="live"
|
|
571
|
+
fi
|
|
572
|
+
if [[ "$SESSION_REQUEST" == "live" ]] && ! live_vendor_capable "$VENDOR"; then
|
|
573
|
+
echo "omnilane: vendor '$VENDOR' cannot run live; live-capable vendors: $(live_capable_vendors)" >&2
|
|
574
|
+
exit 2
|
|
575
|
+
fi
|
|
576
|
+
export OMNILANE_SESSION_MODE="$SESSION_MODE"
|
|
577
|
+
if [[ "$SESSION_REQUEST" == "live" ]]; then
|
|
578
|
+
export OMNILANE_LIVE_REQUIRED=1
|
|
579
|
+
else
|
|
580
|
+
export OMNILANE_LIVE_REQUIRED=0
|
|
581
|
+
fi
|
|
582
|
+
|
|
544
583
|
# Watchdog seconds: --timeout > per-lane OMNILANE_TIMEOUT_<LANE> > OMNILANE_TIMEOUT > 600.
|
|
545
584
|
# Resolve here and export so every runner (and vote's sub-runners) inherits the
|
|
546
585
|
# same value without a per-runner code change; they already read OMNILANE_TIMEOUT.
|
|
@@ -556,6 +595,14 @@ fi
|
|
|
556
595
|
}
|
|
557
596
|
export OMNILANE_TIMEOUT="$TIMEOUT"
|
|
558
597
|
|
|
598
|
+
IDLE_TIMEOUT="$OVERRIDE_IDLE_TIMEOUT"
|
|
599
|
+
[[ -n "$IDLE_TIMEOUT" ]] || IDLE_TIMEOUT="${OMNILANE_IDLE_TIMEOUT:-900}"
|
|
600
|
+
[[ "$IDLE_TIMEOUT" =~ ^(0|[1-9][0-9]*)$ ]] || {
|
|
601
|
+
echo "omnilane: invalid idle timeout (want a non-negative integer of seconds)" >&2
|
|
602
|
+
exit 2
|
|
603
|
+
}
|
|
604
|
+
export OMNILANE_IDLE_TIMEOUT="$IDLE_TIMEOUT"
|
|
605
|
+
|
|
559
606
|
JOB_SUPERVISOR="$OMNILANE_REPO/scripts/lib/job-timeout.pl"
|
|
560
607
|
JOB_WORKER="$OMNILANE_REPO/scripts/lib/job-worker.sh"
|
|
561
608
|
|
|
@@ -649,9 +696,10 @@ else
|
|
|
649
696
|
fi
|
|
650
697
|
|
|
651
698
|
# meta "timeout" is the resolved per-CLI-call watchdog cap, not a whole-job total.
|
|
652
|
-
(umask 077; printf '{"lane":"%s","vendor":"%s","model":"%s","effort":"%s","timeout":%s,"job_timeout":%s,"mode":"%s","workdir":"%s","foreman_session":"%s","candidate":"%s/%s","started":"%s"}\n' \
|
|
653
|
-
"$(json_escape "$LANE")" "$(json_escape "$VENDOR")" "$(json_escape "$
|
|
654
|
-
"$(json_escape "$
|
|
699
|
+
(umask 077; printf '{"lane":"%s","vendor":"%s","session_mode":"%s","idle_timeout":%s,"model":"%s","effort":"%s","timeout":%s,"job_timeout":%s,"mode":"%s","workdir":"%s","foreman_session":"%s","candidate":"%s/%s","started":"%s"}\n' \
|
|
700
|
+
"$(json_escape "$LANE")" "$(json_escape "$VENDOR")" "$(json_escape "$SESSION_MODE")" \
|
|
701
|
+
"$IDLE_TIMEOUT" "$(json_escape "$MODEL")" "$(json_escape "$EFFORT")" \
|
|
702
|
+
"$TIMEOUT" "$JOB_TIMEOUT_JSON" "$(json_escape "$MODE")" \
|
|
655
703
|
"$(json_escape "$WORKDIR")" "$(json_escape "$FOREMAN_SESSION")" \
|
|
656
704
|
"$RESOLVED_IDX" "$RESOLVED_TOTAL" \
|
|
657
705
|
"$(date -u +%FT%TZ)" > "$JOB_DIR/meta.json")
|
package/scripts/doctor.sh
CHANGED
|
@@ -31,6 +31,7 @@ done
|
|
|
31
31
|
REPO="${OMNILANE_DOCTOR_REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
32
32
|
OMNILANE_HOME="${OMNILANE_HOME:-$HOME/.omnilane}"
|
|
33
33
|
PROBE_SCRIPT="${OMNILANE_PROVIDER_PROBE_SCRIPT:-$REPO/scripts/provider-probe.sh}"
|
|
34
|
+
GOAL_LOOP="${OMNILANE_DOCTOR_GOAL_LOOP:-$REPO/scripts/lib/goal-loop.sh}"
|
|
34
35
|
PASS_COUNT=0
|
|
35
36
|
WARN_COUNT=0
|
|
36
37
|
FAIL_COUNT=0
|
|
@@ -123,6 +124,31 @@ else
|
|
|
123
124
|
report PASS state "$OMNILANE_HOME is accessible"
|
|
124
125
|
fi
|
|
125
126
|
|
|
127
|
+
goals_store="$OMNILANE_HOME/goals"
|
|
128
|
+
if [[ ! -f "$GOAL_LOOP" || -L "$GOAL_LOOP" ]]; then
|
|
129
|
+
report FAIL goal-orchestrator "$GOAL_LOOP must be a regular file"
|
|
130
|
+
elif ! /bin/bash -n "$GOAL_LOOP" 2>/dev/null; then
|
|
131
|
+
report FAIL goal-orchestrator "$GOAL_LOOP has invalid bash syntax"
|
|
132
|
+
elif [[ -L "$goals_store" || ( -e "$goals_store" && ! -d "$goals_store" ) ]]; then
|
|
133
|
+
report FAIL goal-orchestrator "$goals_store must be a real directory, not a symlink or file"
|
|
134
|
+
elif [[ -d "$goals_store" ]]; then
|
|
135
|
+
goals_mode="$(stat -f '%Lp' "$goals_store" 2>/dev/null || true)"
|
|
136
|
+
if [[ ! "$goals_mode" =~ ^[0-7]{3,4}$ ]]; then
|
|
137
|
+
goals_mode="$(stat -c '%a' "$goals_store" 2>/dev/null || true)"
|
|
138
|
+
fi
|
|
139
|
+
if [[ "$goals_mode" =~ ^0?700$ ]]; then
|
|
140
|
+
report PASS goal-orchestrator "goal-loop.sh parses; goals store mode is $goals_mode"
|
|
141
|
+
elif [[ -n "$goals_mode" ]]; then
|
|
142
|
+
report WARN goal-orchestrator \
|
|
143
|
+
"goal-loop.sh parses; goals store mode is $goals_mode, owner-only 700 is safer"
|
|
144
|
+
else
|
|
145
|
+
report WARN goal-orchestrator \
|
|
146
|
+
"goal-loop.sh parses; could not determine goals store permissions"
|
|
147
|
+
fi
|
|
148
|
+
else
|
|
149
|
+
report PASS goal-orchestrator "goal-loop.sh parses; goals store is not present yet"
|
|
150
|
+
fi
|
|
151
|
+
|
|
126
152
|
completion_plugin_state() {
|
|
127
153
|
local config_dir state
|
|
128
154
|
config_dir="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
|
package/scripts/jobs.sh
CHANGED
|
@@ -10,6 +10,8 @@ set -euo pipefail
|
|
|
10
10
|
# Runtime-relative shared library.
|
|
11
11
|
# shellcheck disable=SC1091
|
|
12
12
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
|
13
|
+
# shellcheck disable=SC1091
|
|
14
|
+
source "$OMNILANE_REPO/scripts/lib/live-protocol.sh"
|
|
13
15
|
JOBS="$OMNILANE_HOME/jobs"
|
|
14
16
|
JOB_ID_PATTERN='^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$'
|
|
15
17
|
JSON_MODE=0
|
|
@@ -145,15 +147,28 @@ read_job_pid() {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
load_job_vendor() {
|
|
150
|
+
local session_mode_re='"session_mode":"(live|single-shot)"'
|
|
148
151
|
if ! read_public_metadata "$JOB_DIR/meta.json" || ! parse_stats_metadata "$PUBLIC_METADATA"; then
|
|
149
152
|
die 1 "job metadata is not safely readable"
|
|
150
153
|
fi
|
|
151
154
|
JOB_VENDOR="$STATS_VENDOR"
|
|
155
|
+
JOB_SESSION_MODE=""
|
|
156
|
+
if [[ "$PUBLIC_METADATA" =~ $session_mode_re ]]; then
|
|
157
|
+
JOB_SESSION_MODE="${BASH_REMATCH[1]}"
|
|
158
|
+
elif [[ "$JOB_VENDOR" == "claude" ]]; then
|
|
159
|
+
# Compatibility with live jobs created before session_mode metadata.
|
|
160
|
+
JOB_SESSION_MODE="live"
|
|
161
|
+
else
|
|
162
|
+
JOB_SESSION_MODE="single-shot"
|
|
163
|
+
fi
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
require_live_job() {
|
|
155
167
|
load_job_vendor
|
|
156
|
-
|
|
168
|
+
live_vendor_capable "$JOB_VENDOR" ||
|
|
169
|
+
die 1 "job vendor '$JOB_VENDOR' is not live-capable (live vendors: $(live_capable_vendors)); it runs in single-shot mode"
|
|
170
|
+
[[ "$JOB_SESSION_MODE" == "live" ]] ||
|
|
171
|
+
die 1 "job vendor '$JOB_VENDOR' was dispatched in single-shot mode"
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
wait_for_live_ready() {
|
|
@@ -204,10 +219,11 @@ parse_stats_metadata() {
|
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
parse_audit_metadata() {
|
|
207
|
-
local value="$1"
|
|
222
|
+
local value="$1" current_re legacy_re
|
|
208
223
|
local json_string='([^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*'
|
|
209
|
-
|
|
210
|
-
[[
|
|
224
|
+
current_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"session_mode\":\"(live|single-shot)\",\"idle_timeout\":(0|[1-9][0-9]*),\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work|sysops)\",\"workdir\":\"${json_string}\",\"foreman_session\":\"${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\"\\}$"
|
|
225
|
+
legacy_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|sysops)\",\"workdir\":\"${json_string}\",(\"foreman_session\":\"${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\"\\}$"
|
|
226
|
+
[[ "$value" =~ $current_re || "$value" =~ $legacy_re ]]
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
print_stat_counts() {
|
|
@@ -352,8 +368,7 @@ case "${1:-}" in
|
|
|
352
368
|
else
|
|
353
369
|
foreman_value="${foreman_session-}"
|
|
354
370
|
fi
|
|
355
|
-
payload="$(
|
|
356
|
-
"$(json_escape "$2")" "$(json_escape "$foreman_value")" "$(json_escape "$3")")"
|
|
371
|
+
payload="$(live_encode_message "$JOB_VENDOR" "$2" "$foreman_value" "$3")"
|
|
357
372
|
set +e
|
|
358
373
|
write_fifo_with_timeout "$JOB_DIR/inbox.fifo" "$payload"
|
|
359
374
|
send_rc=$?
|
|
@@ -404,11 +419,7 @@ case "${1:-}" in
|
|
|
404
419
|
done
|
|
405
420
|
read_exit_code "$JOB_DIR/exit" || die 1 "invalid recorded exit status"
|
|
406
421
|
events_path="$JOB_DIR/events.jsonl"
|
|
407
|
-
result_count=
|
|
408
|
-
if [[ -f "$events_path" && ! -L "$events_path" ]]; then
|
|
409
|
-
result_count="$(grep -Ec '"type"[[:space:]]*:[[:space:]]*"result"' "$events_path" || true)"
|
|
410
|
-
[[ "$result_count" =~ ^[0-9]+$ ]] || result_count=0
|
|
411
|
-
fi
|
|
422
|
+
result_count="$(live_count_result_events "$JOB_VENDOR" "$events_path")"
|
|
412
423
|
echo "closed $2 (result_events=$result_count exit=$RECORDED_EXIT)"
|
|
413
424
|
exit "$RECORDED_EXIT"
|
|
414
425
|
;;
|
|
@@ -565,7 +576,8 @@ case "${1:-}" in
|
|
|
565
576
|
elif [[ -d "$artifact" ]]; then
|
|
566
577
|
audit_emit "$id" nested-directory
|
|
567
578
|
findings=$((findings + 1)); job_failed=1
|
|
568
|
-
|
|
579
|
+
elif [[ -p "$artifact" && ( "${artifact##*/}" == "inbox.fifo" ||
|
|
580
|
+
"${artifact##*/}" == "runner-inbox.fifo" ) ]]; then
|
|
569
581
|
mode="$(path_mode "$artifact" 2>/dev/null || true)"
|
|
570
582
|
if [[ "$mode" != "600" ]]; then
|
|
571
583
|
audit_emit "$id" unsafe-fifo-mode
|