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.
@@ -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] [--dry-run] [--mode advise|work|sysops] [--workdir DIR]
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 "$MODEL")" \
654
- "$(json_escape "$EFFORT")" "$TIMEOUT" "$JOB_TIMEOUT_JSON" "$(json_escape "$MODE")" \
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
- [[ "$JOB_VENDOR" == "claude" ]] || die 1 "job vendor '$JOB_VENDOR' is not live-capable; it runs in single-shot mode"
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" metadata_re
222
+ local value="$1" current_re legacy_re
208
223
  local json_string='([^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*'
209
- metadata_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work)\",\"workdir\":\"${json_string}\",\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"\\}$"
210
- [[ "$value" =~ $metadata_re ]]
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="$(printf '{"type":"user","omnilane_job_id":"%s","foreman_session":"%s","message":{"role":"user","content":[{"type":"text","text":"%s"}]}}' \
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=0
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
- elif [[ -p "$artifact" && "${artifact##*/}" == "inbox.fifo" ]]; then
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