omnilane 0.15.0 → 0.21.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.
@@ -28,6 +28,14 @@ msg() { # key -> localized string ({} = placeholder, see msgf)
28
28
  case "$L" in zh-TW) echo "已偵測到——請自行執行:";; zh-CN) echo "已检测到——请自行执行:";; ja) echo "検出——次を手動で実行:";; ko) echo "감지됨——직접 실행하세요:";; *) echo "found — run this yourself:";; esac ;;
29
29
  plugin_hint)
30
30
  case "$L" in zh-TW) echo "(選配:把本 repo 裝成 Claude Code 外掛,可用 /route 指令)";; zh-CN) echo "(可选:把本仓库装成 Claude Code 插件,获得 /route 命令)";; ja) echo "(任意:このリポジトリを Claude Code プラグインとして追加すると /route コマンドが使えます)";; ko) echo "(선택: 이 저장소를 Claude Code 플러그인으로 추가하면 /route 명령 사용 가능)";; *) echo "(optional: add this repo as a Claude Code plugin for /route commands)";; esac ;;
31
+ completion_notice_active)
32
+ case "$L" in zh-TW) echo "Claude Code 完成通知:已啟用(omnilane@omnilane 外掛已啟用)。";; zh-CN) echo "Claude Code 完成通知:已启用(omnilane@omnilane 插件已启用)。";; ja) echo "Claude Code 完了通知: 有効です(omnilane@omnilane プラグインが有効)。";; ko) echo "Claude Code 완료 알림: 활성 상태입니다(omnilane@omnilane 플러그인 활성화됨).";; *) echo "Claude Code completion notice: active (omnilane@omnilane is enabled).";; esac ;;
33
+ completion_notice_unknown)
34
+ case "$L" in zh-TW) echo "Claude Code 完成通知:狀態未知(查不到外掛狀態)。";; zh-CN) echo "Claude Code 完成通知:状态未知(无法查询插件状态)。";; ja) echo "Claude Code 完了通知: 状態不明(プラグイン状態を確認できません)。";; ko) echo "Claude Code 완료 알림: 상태를 알 수 없습니다(플러그인 상태 조회 실패).";; *) echo "Claude Code completion notice: unknown (plugin state could not be queried).";; esac ;;
35
+ completion_notice_inactive)
36
+ case "$L" in zh-TW) echo "Claude Code 完成通知:未啟用。這項功能需要安裝外掛;只有 skill symlink 不會送達通知。";; zh-CN) echo "Claude Code 完成通知:未启用。此功能需要安装插件;只有 skill symlink 不会送达通知。";; ja) echo "Claude Code 完了通知: 無効です。この機能にはプラグインのインストールが必要で、skill symlink だけでは通知されません。";; ko) echo "Claude Code 완료 알림: 비활성 상태입니다. 이 기능은 플러그인 설치가 필요하며 skill symlink만으로는 알림이 전달되지 않습니다.";; *) echo "Claude Code completion notice: inactive. This feature requires the plugin install; the skill symlink alone does not deliver notices.";; esac ;;
37
+ completion_notice_install)
38
+ case "$L" in zh-TW) echo "請執行:";; zh-CN) echo "请执行:";; ja) echo "次を実行してください:";; ko) echo "다음을 실행하세요:";; *) echo "Run:";; esac ;;
31
39
  linked)
32
40
  case "$L" in zh-TW) echo "已連結";; zh-CN) echo "已链接";; ja) echo "リンクしました";; ko) echo "링크됨";; *) echo "linked";; esac ;;
33
41
  removed)
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
- # Internal worker boundary for one dispatch. The optional whole-job supervisor
3
+
4
+ # Internal worker boundary for one dispatch. An optional whole-job supervisor
4
5
  # wraps this process so lock wait, retries, and vote rounds share one budget.
5
6
 
7
+ # Runtime-relative shared library.
8
+ # shellcheck disable=SC1091
6
9
  source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
10
+ # shellcheck disable=SC1091
11
+ source "$OMNILANE_REPO/scripts/lib/live-protocol.sh"
7
12
 
8
13
  [[ $# -eq 7 ]] || { echo "omnilane: internal job worker received invalid arguments" >&2; exit 2; }
9
14
  VENDOR="$1"; MODE="$2"; WORKDIR="$3"; MODEL="$4"; EFFORT="$5"
@@ -13,11 +18,236 @@ PROMPT_FILE="$6"; OUTPUT_FILE="$7"
13
18
  RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
14
19
  [[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
15
20
 
16
- # Two concurrent codex exec in one target dir corrupt its job index — serialize.
21
+ # Two concurrent codex execs in one target dir corrupt its job index — serialize.
17
22
  [[ "$VENDOR" == "codex" ]] && acquire_cwd_lock codex "$WORKDIR"
18
23
 
24
+ # The backslash case pattern is intentional.
25
+ # shellcheck disable=SC1003
26
+ json_escape() {
27
+ local s="$1" out="" ch escaped code i
28
+ for ((i = 0; i < ${#s}; i++)); do
29
+ ch="${s:i:1}"
30
+ case "$ch" in
31
+ '"') out="$out\\\"" ;;
32
+ '\\') out="$out\\\\" ;;
33
+ $'\b') out="$out\\b" ;;
34
+ $'\f') out="$out\\f" ;;
35
+ $'\n') out="$out\\n" ;;
36
+ $'\r') out="$out\\r" ;;
37
+ $'\t') out="$out\\t" ;;
38
+ *)
39
+ LC_CTYPE=C printf -v code '%d' "'$ch"
40
+ if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
41
+ printf -v escaped '\\u%04x' "$code"
42
+ out="$out$escaped"
43
+ else
44
+ out="$out$ch"
45
+ fi
46
+ ;;
47
+ esac
48
+ done
49
+ printf '%s' "$out"
50
+ }
51
+
52
+ emit_mode_notice() {
53
+ local notice="$1" notice_file="${OUTPUT_FILE%/*}/mode-notice.txt"
54
+ printf '%s\n' "$notice" >&2
55
+ if [[ -L "$notice_file" || ( -e "$notice_file" && ! -f "$notice_file" ) ]]; then
56
+ echo "omnilane: unsafe mode notice path" >&2
57
+ return 1
58
+ fi
59
+ (umask 077; printf '%s\n' "$notice" > "$notice_file")
60
+ }
61
+
62
+ run_single_shot() {
63
+ local notice="$1" rc
64
+ set +e
65
+ (
66
+ unset OMNILANE_INBOX
67
+ "$RUNNER" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" "$PROMPT_FILE" "$OUTPUT_FILE"
68
+ )
69
+ rc=$?
70
+ set -e
71
+ emit_mode_notice "$notice"
72
+ return "$rc"
73
+ }
74
+
75
+ SESSION_MODE="${OMNILANE_SESSION_MODE:-auto}"
76
+ IDLE_TIMEOUT="${OMNILANE_IDLE_TIMEOUT:-900}"
77
+ LIVE_REQUIRED="${OMNILANE_LIVE_REQUIRED:-0}"
78
+ [[ "$SESSION_MODE" == "auto" || "$SESSION_MODE" == "live" || "$SESSION_MODE" == "single-shot" ]] || {
79
+ echo "omnilane: invalid worker session mode" >&2; exit 2
80
+ }
81
+ [[ "$IDLE_TIMEOUT" =~ ^(0|[1-9][0-9]*)$ ]] || {
82
+ echo "omnilane: invalid worker idle timeout" >&2; exit 2
83
+ }
84
+ [[ "$LIVE_REQUIRED" == "0" || "$LIVE_REQUIRED" == "1" ]] || {
85
+ echo "omnilane: invalid worker live requirement" >&2; exit 2
86
+ }
87
+ if [[ "$SESSION_MODE" == "auto" ]]; then
88
+ if live_vendor_capable "$VENDOR"; then SESSION_MODE="live"; else SESSION_MODE="single-shot"; fi
89
+ fi
90
+
91
+ if [[ "$SESSION_MODE" == "single-shot" ]]; then
92
+ set +e
93
+ if live_vendor_capable "$VENDOR"; then
94
+ run_single_shot "omnilane: vendor '$VENDOR' was resolved to single-shot mode"
95
+ else
96
+ run_single_shot "omnilane: vendor '$VENDOR' is not live-capable; ran in single-shot mode"
97
+ fi
98
+ rc=$?
99
+ set -e
100
+ exit "$rc"
101
+ fi
102
+
103
+ if ! live_vendor_capable "$VENDOR"; then
104
+ echo "omnilane: vendor '$VENDOR' cannot run required live session; live-capable vendors: $(live_capable_vendors)" >&2
105
+ exit 2
106
+ fi
107
+
108
+ JOB_DIR="${OUTPUT_FILE%/*}"
109
+ JOB_ID="${JOB_DIR##*/}"
110
+ INBOX_FIFO="$JOB_DIR/inbox.fifo"
111
+ RUNNER_INBOX_FIFO="$JOB_DIR/runner-inbox.fifo"
112
+ HOLDER_PID_FILE="$JOB_DIR/inbox.holder.pid"
113
+ READY_FILE="$JOB_DIR/inbox.ready"
114
+ EVENTS_FILE="${OUTPUT_FILE}.events.jsonl"
115
+ EVENTS_ALIAS="$JOB_DIR/events.jsonl"
116
+ close_requested=0
117
+ close_reason=""
118
+ runner_writer_open=0
119
+ inbox_open=0
120
+ events_reader_open=0
121
+ runner_pid=""
122
+
123
+ # Invoked by the EXIT trap below.
124
+ # shellcheck disable=SC2329
125
+ cleanup_live_mailbox() {
126
+ if [[ "$runner_writer_open" -eq 1 ]]; then exec 3>&-; runner_writer_open=0; fi
127
+ if [[ "$inbox_open" -eq 1 ]]; then exec 4>&-; inbox_open=0; fi
128
+ if [[ "$events_reader_open" -eq 1 ]]; then exec 5<&-; events_reader_open=0; fi
129
+ rm "$READY_FILE" "$HOLDER_PID_FILE" "$INBOX_FIFO" "$RUNNER_INBOX_FIFO" 2>/dev/null || true
130
+ }
131
+
132
+ prepare_live_mailbox() {
133
+ local path old_umask rc=0
134
+ for path in "$INBOX_FIFO" "$RUNNER_INBOX_FIFO" "$HOLDER_PID_FILE" \
135
+ "$READY_FILE" "$EVENTS_FILE" "$EVENTS_ALIAS"; do
136
+ [[ ! -e "$path" && ! -L "$path" ]] || return 1
137
+ done
138
+ old_umask="$(umask)"
139
+ umask 077
140
+ mkfifo "$INBOX_FIFO" "$RUNNER_INBOX_FIFO" || rc=$?
141
+ if [[ "$rc" -eq 0 ]]; then
142
+ : > "$EVENTS_FILE" || rc=$?
143
+ fi
144
+ if [[ "$rc" -eq 0 ]]; then
145
+ ln "$EVENTS_FILE" "$EVENTS_ALIAS" || rc=$?
146
+ fi
147
+ if [[ "$rc" -eq 0 ]]; then
148
+ chmod 600 "$INBOX_FIFO" "$RUNNER_INBOX_FIFO" "$EVENTS_FILE" "$EVENTS_ALIAS" || rc=$?
149
+ fi
150
+ umask "$old_umask"
151
+ if [[ "$rc" -ne 0 ]]; then
152
+ rm "$EVENTS_ALIAS" "$EVENTS_FILE" "$INBOX_FIFO" "$RUNNER_INBOX_FIFO" 2>/dev/null || true
153
+ fi
154
+ return "$rc"
155
+ }
156
+
157
+ last_result_status() {
158
+ local event last_result=""
159
+ while IFS= read -r event || [[ -n "$event" ]]; do
160
+ if live_event_is_result "$VENDOR" "$event"; then last_result="$event"; fi
161
+ done < "$EVENTS_FILE"
162
+ [[ -n "$last_result" ]] || return 1
163
+ live_event_is_success "$VENDOR" "$last_result"
164
+ }
165
+
166
+ if ! prepare_live_mailbox; then
167
+ if [[ "$LIVE_REQUIRED" -eq 1 ]]; then
168
+ echo "omnilane: required $VENDOR live mailbox unavailable because FIFO setup failed" >&2
169
+ exit 1
170
+ fi
171
+ set +e
172
+ run_single_shot "omnilane: $VENDOR live mailbox unavailable because FIFO setup failed; ran in single-shot mode"
173
+ rc=$?
174
+ set -e
175
+ exit "$rc"
176
+ fi
177
+
178
+ trap 'close_requested=1' USR1
179
+ trap 'close_requested=1' PIPE
180
+ trap cleanup_live_mailbox EXIT
181
+ truncate_payload "$PROMPT_FILE" 102400
182
+ INITIAL_TEXT="$(cat "$PROMPT_FILE")"
183
+ if [[ "${FOREMAN_SESSION+x}" == "x" ]]; then
184
+ FOREMAN_SESSION_VALUE="$FOREMAN_SESSION"
185
+ else
186
+ FOREMAN_SESSION_VALUE="${foreman_session-}"
187
+ fi
188
+
189
+ write_current_pid_file "$HOLDER_PID_FILE"
190
+ export OMNILANE_INBOX="$RUNNER_INBOX_FIFO"
191
+ "$RUNNER" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" "$PROMPT_FILE" "$OUTPUT_FILE" &
192
+ runner_pid=$!
193
+
194
+ # Open the runner writer only after its FIFO reader starts.
195
+ exec 3> "$RUNNER_INBOX_FIFO"
196
+ runner_writer_open=1
197
+ exec 4<> "$INBOX_FIFO"
198
+ inbox_open=1
199
+ exec 5< "$EVENTS_FILE"
200
+ events_reader_open=1
201
+ initial_payload="$(live_encode_message "$VENDOR" "$JOB_ID" "$FOREMAN_SESSION_VALUE" "$INITIAL_TEXT")"
202
+ if ! printf '%s\n' "$initial_payload" >&3; then close_requested=1; fi
203
+ (umask 077; : > "$READY_FILE")
204
+
205
+ last_activity=$SECONDS
206
+ while kill -0 "$runner_pid" 2>/dev/null; do
207
+ [[ "$close_requested" -eq 0 ]] || break
208
+ incoming=""
209
+ if IFS= read -r -t 1 incoming <&4; then
210
+ if ! printf '%s\n' "$incoming" >&3; then close_requested=1; fi
211
+ last_activity=$SECONDS
212
+ fi
213
+ while IFS= read -r event <&5; do
214
+ if live_event_is_result "$VENDOR" "$event"; then
215
+ last_activity=$SECONDS
216
+ fi
217
+ done
218
+ if [[ "$IDLE_TIMEOUT" -gt 0 && $((SECONDS - last_activity)) -ge "$IDLE_TIMEOUT" ]]; then
219
+ close_reason="closed by idle cap after ${IDLE_TIMEOUT}s"
220
+ close_requested=1
221
+ fi
222
+ done
223
+
224
+ if [[ "$runner_writer_open" -eq 1 ]]; then exec 3>&-; runner_writer_open=0; fi
225
+ if [[ "$inbox_open" -eq 1 ]]; then exec 4>&-; inbox_open=0; fi
226
+ if [[ "$events_reader_open" -eq 1 ]]; then exec 5<&-; events_reader_open=0; fi
227
+
19
228
  set +e
20
- "$RUNNER" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" "$PROMPT_FILE" "$OUTPUT_FILE"
21
- rc=$?
229
+ if [[ "$close_requested" -eq 1 ]]; then
230
+ kill -TERM "$runner_pid" 2>/dev/null || true
231
+ fi
232
+ wait "$runner_pid" 2>/dev/null
233
+ runner_rc=$?
22
234
  set -e
235
+
236
+ if [[ -n "$close_reason" ]]; then
237
+ printf '\n%s\n' "$close_reason" >> "$OUTPUT_FILE"
238
+ emit_mode_notice "$close_reason" || true
239
+ fi
240
+
241
+ if [[ "$close_requested" -eq 1 ]]; then
242
+ if last_result_status; then
243
+ rc=0
244
+ else
245
+ rc=1
246
+ emit_mode_notice "omnilane: $VENDOR live mailbox closed without a successful result event" || true
247
+ fi
248
+ else
249
+ rc="$runner_rc"
250
+ fi
251
+
252
+ trap - USR1 PIPE
23
253
  exit "$rc"
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Shared live-mailbox protocol differences. Callers provide json_escape().
4
+
5
+ live_capable_vendors() {
6
+ printf 'claude, gemini'
7
+ }
8
+
9
+ live_vendor_capable() {
10
+ case "$1" in
11
+ claude|gemini) return 0 ;;
12
+ *) return 1 ;;
13
+ esac
14
+ }
15
+
16
+ live_encode_message() {
17
+ local vendor="$1" job_id="$2" foreman_session="$3" text="$4"
18
+ case "$vendor" in
19
+ claude)
20
+ printf '{"type":"user","omnilane_job_id":"%s","foreman_session":"%s","message":{"role":"user","content":[{"type":"text","text":"%s"}]}}' \
21
+ "$(json_escape "$job_id")" "$(json_escape "$foreman_session")" \
22
+ "$(json_escape "$text")"
23
+ ;;
24
+ gemini)
25
+ printf '{"event":"user","message":{"role":"user","content":[{"type":"text","text":"%s"}]}}' \
26
+ "$(json_escape "$text")"
27
+ ;;
28
+ *) return 2 ;;
29
+ esac
30
+ }
31
+
32
+ live_event_is_result() {
33
+ local vendor="$1" event="$2" pattern
34
+ case "$vendor" in
35
+ claude) pattern='"type"[[:space:]]*:[[:space:]]*"result"' ;;
36
+ gemini) pattern='"event"[[:space:]]*:[[:space:]]*"result"' ;;
37
+ *) return 2 ;;
38
+ esac
39
+ [[ "$event" =~ $pattern ]]
40
+ }
41
+
42
+ live_event_is_success() {
43
+ local vendor="$1" event="$2" pattern
44
+ live_event_is_result "$vendor" "$event" || return 1
45
+ case "$vendor" in
46
+ claude)
47
+ pattern='"is_error"[[:space:]]*:[[:space:]]*true'
48
+ [[ ! "$event" =~ $pattern ]]
49
+ ;;
50
+ gemini)
51
+ pattern='"status"[[:space:]]*:[[:space:]]*"SUCCESS"'
52
+ [[ "$event" =~ $pattern ]]
53
+ ;;
54
+ *) return 2 ;;
55
+ esac
56
+ }
57
+
58
+ live_count_result_events() {
59
+ local vendor="$1" path="$2" event count=0
60
+ [[ -f "$path" && ! -L "$path" ]] || {
61
+ printf '0'
62
+ return 0
63
+ }
64
+ while IFS= read -r event || [[ -n "$event" ]]; do
65
+ if live_event_is_result "$vendor" "$event"; then
66
+ count=$((count + 1))
67
+ fi
68
+ done < "$path"
69
+ printf '%s' "$count"
70
+ }
@@ -3,6 +3,8 @@ set -euo pipefail
3
3
  # omnilane runner: Claude Code CLI
4
4
  # Usage: run-claude.sh MODE WORKDIR MODEL EFFORT PROMPT_FILE OUTPUT_FILE
5
5
 
6
+ # Runtime-relative shared library.
7
+ # shellcheck disable=SC1091
6
8
  source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
7
9
 
8
10
  MODE="$1"; WORKDIR="$2"; MODEL="$3"; EFFORT="$4"; PROMPT_FILE="$5"; OUTPUT_FILE="$6"
@@ -12,6 +14,110 @@ RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
12
14
 
13
15
  truncate_payload "$PROMPT_FILE" 102400
14
16
 
17
+ LIVE_INBOX="${OMNILANE_INBOX:-}"
18
+ if [[ -n "$LIVE_INBOX" && -p "$LIVE_INBOX" ]]; then
19
+ EVENTS_FILE="${OUTPUT_FILE}.events.jsonl"
20
+ STDERR_FILE="${OUTPUT_FILE}.stderr.log"
21
+ LIVE_CHILD_PID=""
22
+
23
+ if [[ -L "$EVENTS_FILE" || ( -e "$EVENTS_FILE" && ! -f "$EVENTS_FILE" ) ]]; then
24
+ echo "omnilane: unsafe Claude live event path" >&2
25
+ exit 125
26
+ fi
27
+ (umask 077; : > "$EVENTS_FILE"; : > "$STDERR_FILE")
28
+
29
+ LIVE_ARGS=(--disable-slash-commands --model "$MODEL")
30
+ [[ -n "$EFFORT" && "$EFFORT" != "-" ]] && LIVE_ARGS+=(--effort "$EFFORT")
31
+ if [[ "$MODE" == "advise" ]]; then
32
+ LIVE_ARGS+=(--tools Read Glob Grep)
33
+ else
34
+ LIVE_ARGS+=(--permission-mode acceptEdits)
35
+ fi
36
+ LIVE_ARGS+=(-p --verbose --input-format stream-json --output-format stream-json)
37
+
38
+ finalize_live_output() {
39
+ local tmp="${OUTPUT_FILE}.tmp"
40
+ if ! command -v python3 >/dev/null 2>&1; then
41
+ echo "omnilane: cannot extract Claude live result: python3 not found" >> "$STDERR_FILE"
42
+ return 1
43
+ fi
44
+ if ! python3 - "$EVENTS_FILE" "$tmp" <<'PY'
45
+ import json
46
+ import pathlib
47
+ import sys
48
+
49
+ events_path = pathlib.Path(sys.argv[1])
50
+ output_path = pathlib.Path(sys.argv[2])
51
+ last_result = None
52
+
53
+ with events_path.open(encoding="utf-8") as events:
54
+ for raw_line in events:
55
+ try:
56
+ event = json.loads(raw_line)
57
+ except json.JSONDecodeError:
58
+ continue
59
+ if event.get("type") == "result" and isinstance(event.get("result"), str):
60
+ last_result = event["result"]
61
+
62
+ if last_result is None:
63
+ raise SystemExit(1)
64
+
65
+ output_path.write_text(last_result.rstrip("\n") + "\n", encoding="utf-8")
66
+ PY
67
+ then
68
+ echo "omnilane: Claude live stream ended without a readable result event" >> "$STDERR_FILE"
69
+ return 1
70
+ fi
71
+ mv "$tmp" "$OUTPUT_FILE"
72
+ }
73
+
74
+ # Invoked by signal traps below.
75
+ # shellcheck disable=SC2329
76
+ stop_live_child() {
77
+ local signal_rc="$1" waited=0
78
+ trap - TERM HUP INT
79
+ set +e
80
+ if [[ -n "$LIVE_CHILD_PID" ]] && kill -0 "$LIVE_CHILD_PID" 2>/dev/null; then
81
+ kill -TERM "-$LIVE_CHILD_PID" 2>/dev/null || kill -TERM "$LIVE_CHILD_PID" 2>/dev/null || true
82
+ while kill -0 "$LIVE_CHILD_PID" 2>/dev/null && [[ "$waited" -lt 50 ]]; do
83
+ sleep 0.1
84
+ waited=$((waited + 1))
85
+ done
86
+ if kill -0 "$LIVE_CHILD_PID" 2>/dev/null; then
87
+ kill -KILL "-$LIVE_CHILD_PID" 2>/dev/null || kill -KILL "$LIVE_CHILD_PID" 2>/dev/null || true
88
+ fi
89
+ wait "$LIVE_CHILD_PID" 2>/dev/null
90
+ fi
91
+ finalize_live_output || true
92
+ [[ -s "$STDERR_FILE" ]] || rm "$STDERR_FILE" 2>/dev/null || true
93
+ exit "$signal_rc"
94
+ }
95
+
96
+ set -m
97
+ (
98
+ cd "$WORKDIR" || exit 127
99
+ run_with_timeout "$RUN_TIMEOUT" env \
100
+ OMNILANE_DEPTH=1 \
101
+ "$CLAUDE_BIN" "${LIVE_ARGS[@]}" < "$LIVE_INBOX" > "$EVENTS_FILE" 2> "$STDERR_FILE"
102
+ ) &
103
+ LIVE_CHILD_PID=$!
104
+ set +m
105
+ trap 'stop_live_child 143' TERM
106
+ trap 'stop_live_child 129' HUP
107
+ trap 'stop_live_child 130' INT
108
+
109
+ set +e
110
+ wait "$LIVE_CHILD_PID"
111
+ RC=$?
112
+ set -e
113
+ trap - TERM HUP INT
114
+ if ! finalize_live_output && [[ "$RC" -eq 0 ]]; then
115
+ RC=1
116
+ fi
117
+ [[ -s "$STDERR_FILE" ]] || rm "$STDERR_FILE" 2>/dev/null || true
118
+ exit "$RC"
119
+ fi
120
+
15
121
  ARGS=(--disable-slash-commands --model "$MODEL" --output-format text)
16
122
  [[ -n "$EFFORT" && "$EFFORT" != "-" ]] && ARGS+=(--effort "$EFFORT")
17
123
  if [[ "$MODE" == "advise" ]]; then
@@ -33,6 +33,115 @@ MODEL_ARGS=()
33
33
  # plan = read-only tools (advise), accept-edits = file edits allowed (work).
34
34
  if [[ "$MODE" == "advise" ]]; then MODE_ARGS=(--mode plan); else MODE_ARGS=(--mode accept-edits); fi
35
35
 
36
+ LIVE_INBOX="${OMNILANE_INBOX:-}"
37
+ if [[ -n "$LIVE_INBOX" && -p "$LIVE_INBOX" ]]; then
38
+ EVENTS_FILE="${OUTPUT_FILE}.events.jsonl"
39
+ STDERR_FILE="${OUTPUT_FILE}.stderr.log"
40
+ LIVE_CHILD_PID=""
41
+
42
+ if [[ -L "$EVENTS_FILE" || ( -e "$EVENTS_FILE" && ! -f "$EVENTS_FILE" ) ]]; then
43
+ echo "omnilane: unsafe Gemini live event path" >&2
44
+ exit 125
45
+ fi
46
+ (umask 077; : > "$EVENTS_FILE"; : > "$STDERR_FILE")
47
+
48
+ finalize_live_output() {
49
+ local tmp="${OUTPUT_FILE}.tmp"
50
+ if ! command -v python3 >/dev/null 2>&1; then
51
+ echo "omnilane: cannot extract Gemini live result: python3 not found" >> "$STDERR_FILE"
52
+ return 1
53
+ fi
54
+ if ! python3 - "$EVENTS_FILE" "$tmp" <<'PY'
55
+ import json
56
+ import pathlib
57
+ import sys
58
+
59
+ events_path = pathlib.Path(sys.argv[1])
60
+ output_path = pathlib.Path(sys.argv[2])
61
+ last_response = None
62
+
63
+ with events_path.open(encoding="utf-8") as events:
64
+ for raw_line in events:
65
+ try:
66
+ event = json.loads(raw_line)
67
+ except json.JSONDecodeError:
68
+ continue
69
+ result = event.get("result")
70
+ if (
71
+ event.get("event") == "result"
72
+ and isinstance(result, dict)
73
+ and result.get("status") == "SUCCESS"
74
+ and isinstance(result.get("response"), str)
75
+ ):
76
+ last_response = result["response"]
77
+
78
+ if last_response is None:
79
+ raise SystemExit(1)
80
+
81
+ output_path.write_text(last_response.rstrip("\n") + "\n", encoding="utf-8")
82
+ PY
83
+ then
84
+ echo "omnilane: Gemini live stream ended without a readable SUCCESS result" >> "$STDERR_FILE"
85
+ return 1
86
+ fi
87
+ mv "$tmp" "$OUTPUT_FILE"
88
+ }
89
+
90
+ # Invoked by signal traps below.
91
+ # shellcheck disable=SC2329
92
+ stop_live_child() {
93
+ local signal_rc="$1" waited=0
94
+ trap - TERM HUP INT
95
+ set +e
96
+ if [[ -n "$LIVE_CHILD_PID" ]] && kill -0 "$LIVE_CHILD_PID" 2>/dev/null; then
97
+ kill -TERM "-$LIVE_CHILD_PID" 2>/dev/null || kill -TERM "$LIVE_CHILD_PID" 2>/dev/null || true
98
+ while kill -0 "$LIVE_CHILD_PID" 2>/dev/null && [[ "$waited" -lt 50 ]]; do
99
+ sleep 0.1
100
+ waited=$((waited + 1))
101
+ done
102
+ if kill -0 "$LIVE_CHILD_PID" 2>/dev/null; then
103
+ kill -KILL "-$LIVE_CHILD_PID" 2>/dev/null || kill -KILL "$LIVE_CHILD_PID" 2>/dev/null || true
104
+ fi
105
+ wait "$LIVE_CHILD_PID" 2>/dev/null
106
+ fi
107
+ finalize_live_output || true
108
+ [[ -s "$STDERR_FILE" ]] || rm "$STDERR_FILE" 2>/dev/null || true
109
+ exit "$signal_rc"
110
+ }
111
+
112
+ set -m
113
+ (
114
+ cd "$RUN_DIR" || exit 127
115
+ run_with_timeout "$RUN_TIMEOUT" env \
116
+ -u GEMINI_API_KEY -u GOOGLE_API_KEY -u GOOGLE_AI_API_KEY \
117
+ NO_BROWSER=1 OMNILANE_DEPTH=1 \
118
+ "$AGY_BIN" --dangerously-skip-permissions --add-dir "$RUN_DIR" \
119
+ "${MODE_ARGS[@]}" ${MODEL_ARGS[@]+"${MODEL_ARGS[@]}"} \
120
+ --input-format stream-json --output-format stream-json -p "" \
121
+ < "$LIVE_INBOX" > "$EVENTS_FILE" 2> "$STDERR_FILE"
122
+ ) &
123
+ LIVE_CHILD_PID=$!
124
+ set +m
125
+ trap 'stop_live_child 143' TERM
126
+ trap 'stop_live_child 129' HUP
127
+ trap 'stop_live_child 130' INT
128
+
129
+ set +e
130
+ wait "$LIVE_CHILD_PID"
131
+ RC=$?
132
+ set -e
133
+ trap - TERM HUP INT
134
+ if ! finalize_live_output && [[ "$RC" -eq 0 ]]; then
135
+ RC=1
136
+ fi
137
+ if grep -Eiq "$CAPACITY_PATTERN" "$EVENTS_FILE" "$STDERR_FILE" 2>/dev/null; then
138
+ echo "omnilane: gemini capacity exhausted" >> "$STDERR_FILE"
139
+ RC=126
140
+ fi
141
+ [[ -s "$STDERR_FILE" ]] || rm "$STDERR_FILE" 2>/dev/null || true
142
+ exit "$RC"
143
+ fi
144
+
36
145
  set +e
37
146
  (
38
147
  cd "$RUN_DIR" || exit 127