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.
- package/.claude-plugin/marketplace.json +27 -0
- package/.claude-plugin/plugin.json +9 -0
- package/CHANGELOG.md +58 -1
- package/README.ja.md +46 -0
- package/README.ko.md +44 -0
- package/README.md +56 -1
- package/README.zh-CN.md +41 -0
- package/README.zh-TW.md +41 -0
- package/VERSION +1 -1
- package/bin/omnilane +5 -1
- package/hooks/hooks.json +33 -0
- package/hooks/record-foreman-session.sh +57 -0
- package/hooks/report-completions.sh +134 -0
- package/hooks/routing-instruction.md +18 -0
- package/package.json +6 -2
- package/plugin.json +6 -0
- package/scripts/dispatch.sh +65 -10
- package/scripts/doctor.sh +126 -0
- package/scripts/jobs.sh +164 -11
- package/scripts/lib/common.sh +88 -0
- package/scripts/lib/goal-loop.sh +776 -0
- package/scripts/lib/i18n.sh +8 -0
- package/scripts/lib/job-worker.sh +234 -4
- package/scripts/lib/live-protocol.sh +70 -0
- package/scripts/runners/run-claude.sh +106 -0
- package/scripts/runners/run-gemini.sh +109 -0
- package/skills/omnilane/SKILL.md +172 -0
package/scripts/lib/i18n.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
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
|