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,247 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# omnilane shared helpers — sourced by dispatch.sh and runners.
|
|
3
|
+
|
|
4
|
+
OMNILANE_HOME="${OMNILANE_HOME:-$HOME/.omnilane}"
|
|
5
|
+
OMNILANE_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
6
|
+
export OMNILANE_REPO
|
|
7
|
+
|
|
8
|
+
# Optional local overlay: proxies, auth wrappers, per-machine binaries.
|
|
9
|
+
# Publishable default is plain CLIs on PATH; power users add ~/.omnilane/local.sh.
|
|
10
|
+
[[ -f "$OMNILANE_HOME/local.sh" ]] && source "$OMNILANE_HOME/local.sh"
|
|
11
|
+
|
|
12
|
+
resolve_timeout_cmd() {
|
|
13
|
+
if command -v timeout &>/dev/null; then echo "timeout";
|
|
14
|
+
elif command -v gtimeout &>/dev/null; then echo "gtimeout";
|
|
15
|
+
else echo ""; fi
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Portable watchdog: timeout/gtimeout when present, perl alarm otherwise
|
|
19
|
+
# (stock macOS has perl but no coreutils timeout). Warns when neither exists
|
|
20
|
+
# so a hung vendor CLI cannot silently block forever.
|
|
21
|
+
run_with_timeout() { # seconds, command...
|
|
22
|
+
local secs="$1"; shift
|
|
23
|
+
# GNU timeout creates a nested process group. Under the whole-job supervisor,
|
|
24
|
+
# keep calls in its isolated group so one outer signal reaches every runner.
|
|
25
|
+
if [[ "${OMNILANE_JOB_SUPERVISED:-0}" == "1" ]]; then
|
|
26
|
+
command -v perl &>/dev/null || {
|
|
27
|
+
echo "omnilane: supervised timeout requires perl" >&2; return 125
|
|
28
|
+
}
|
|
29
|
+
perl -e 'alarm shift; exec @ARGV or die "exec: $!"' "$secs" "$@"
|
|
30
|
+
return $?
|
|
31
|
+
fi
|
|
32
|
+
local t; t="$(resolve_timeout_cmd)"
|
|
33
|
+
if [[ -n "$t" ]]; then
|
|
34
|
+
"$t" "$secs" "$@"
|
|
35
|
+
elif command -v perl &>/dev/null; then
|
|
36
|
+
perl -e 'alarm shift; exec @ARGV or die "exec: $!"' "$secs" "$@"
|
|
37
|
+
else
|
|
38
|
+
echo "omnilane: no timeout/gtimeout/perl — running without a watchdog" >&2
|
|
39
|
+
"$@"
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hash_str() { # stdin -> short stable token (sha256sum/shasum/cksum fallback)
|
|
44
|
+
if command -v sha256sum &>/dev/null; then sha256sum | cut -c1-12
|
|
45
|
+
elif command -v shasum &>/dev/null; then shasum | cut -c1-12
|
|
46
|
+
else cksum | cut -d' ' -f1; fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
current_pid() { # Call with direct redirection; command substitution records its short-lived shell.
|
|
50
|
+
if [[ -n "${BASHPID:-}" ]]; then printf '%s' "$BASHPID"
|
|
51
|
+
else (exec sh -c 'printf %s "$PPID"'); fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
write_current_pid_file() {
|
|
55
|
+
local path="$1" tmp="${1}.tmp.$$-$RANDOM" old_umask rc=0
|
|
56
|
+
old_umask="$(umask)"
|
|
57
|
+
umask 077
|
|
58
|
+
current_pid > "$tmp" || rc=$?
|
|
59
|
+
umask "$old_umask"
|
|
60
|
+
if [[ "$rc" -ne 0 ]]; then
|
|
61
|
+
rm "$tmp" 2>/dev/null || true
|
|
62
|
+
return "$rc"
|
|
63
|
+
fi
|
|
64
|
+
mv "$tmp" "$path"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
vendor_bin() { # vendor -> binary (honors local.sh overrides)
|
|
68
|
+
case "$1" in
|
|
69
|
+
codex) echo "${CODEX_BIN:-codex}" ;;
|
|
70
|
+
claude) echo "${CLAUDE_BIN:-claude}" ;;
|
|
71
|
+
grok) echo "${GROK_BIN:-grok}" ;;
|
|
72
|
+
gemini) echo "${AGY_BIN:-agy}" ;;
|
|
73
|
+
*) echo "" ;;
|
|
74
|
+
esac
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
vendor_available() {
|
|
78
|
+
# "exec" is the bring-your-own-gate vendor: the model field is a script path,
|
|
79
|
+
# checked at dispatch time (the chain resolver cannot see it here).
|
|
80
|
+
# "vote" is the built-in multi-model panel: needs >=2 voters, checked at dispatch.
|
|
81
|
+
[[ "$1" == "exec" || "$1" == "vote" ]] && return 0
|
|
82
|
+
local b; b="$(vendor_bin "$1")"
|
|
83
|
+
[[ -n "$b" ]] && command -v "$b" >/dev/null 2>&1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
expand_home_path() {
|
|
87
|
+
case "$1" in
|
|
88
|
+
\~/*) printf '%s' "$HOME/${1#\~/}" ;;
|
|
89
|
+
*) printf '%s' "$1" ;;
|
|
90
|
+
esac
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Depth guard: a dispatched worker must not fan out again (quota-burn chains).
|
|
94
|
+
depth_guard() {
|
|
95
|
+
local depth="${OMNILANE_DEPTH:-0}"
|
|
96
|
+
[[ "$depth" =~ ^(0|[1-9][0-9]{0,8})$ ]] || {
|
|
97
|
+
echo "omnilane: invalid OMNILANE_DEPTH (want 0..999999999)" >&2
|
|
98
|
+
exit 2
|
|
99
|
+
}
|
|
100
|
+
if [[ "$depth" -ge 1 ]]; then
|
|
101
|
+
echo "omnilane: refusing nested dispatch (OMNILANE_DEPTH=$depth)" >&2
|
|
102
|
+
exit 86
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Same-cwd serial lock for codex: two concurrent `codex exec` in one repo
|
|
107
|
+
# corrupt the job index and cross-pollute pytest. mkdir is the portable lock.
|
|
108
|
+
read_lock_owner() {
|
|
109
|
+
local path="$1" size value length
|
|
110
|
+
local LC_ALL=C
|
|
111
|
+
LOCK_OWNER_VALUE=""
|
|
112
|
+
[[ -f "$path" && ! -L "$path" ]] || return 1
|
|
113
|
+
size="$({ wc -c < "$path"; } 2>/dev/null | tr -d '[:space:]')" || return 2
|
|
114
|
+
[[ "$size" =~ ^[0-9]+$ && "$size" -le 11 ]] || return 2
|
|
115
|
+
value="$(cat "$path" 2>/dev/null)" || return 2
|
|
116
|
+
length="${#value}"
|
|
117
|
+
[[ "$size" -eq "$length" || "$size" -eq $((length + 1)) ]] || return 2
|
|
118
|
+
[[ "$value" =~ ^[1-9][0-9]{0,9}$ ]] || return 2
|
|
119
|
+
LOCK_OWNER_VALUE="$value"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
prepare_lock_store() {
|
|
123
|
+
local lock_root="$OMNILANE_HOME/locks"
|
|
124
|
+
mkdir -p "$OMNILANE_HOME"
|
|
125
|
+
if [[ -L "$lock_root" || ( -e "$lock_root" && ! -d "$lock_root" ) ]]; then
|
|
126
|
+
echo "omnilane: unsafe lock store path (want a real directory): $lock_root" >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
[[ -d "$lock_root" ]] || mkdir -m 700 "$lock_root"
|
|
130
|
+
[[ -d "$lock_root" && ! -L "$lock_root" ]] || {
|
|
131
|
+
echo "omnilane: lock store changed while preparing it" >&2
|
|
132
|
+
exit 1
|
|
133
|
+
}
|
|
134
|
+
chmod 700 "$lock_root"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
write_lock_owner() {
|
|
138
|
+
local path="$1" tmp="${1}.tmp.$$-$RANDOM" old_umask rc=0
|
|
139
|
+
old_umask="$(umask)"
|
|
140
|
+
umask 077
|
|
141
|
+
current_pid > "$tmp" || rc=$?
|
|
142
|
+
umask "$old_umask"
|
|
143
|
+
if [[ "$rc" -eq 0 ]] && ln "$tmp" "$path" 2>/dev/null; then
|
|
144
|
+
rm "$tmp"
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
rm "$tmp" 2>/dev/null || true
|
|
148
|
+
return 1
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
acquire_cwd_lock() { # vendor, workdir — the lock keys on the TARGET dir, not $PWD
|
|
152
|
+
local vendor="$1" dir="${2:-$PWD}" key lockdir waited=0 owner owner_state
|
|
153
|
+
local empty_since=-1 empty_grace="${OMNILANE_LOCK_EMPTY_GRACE:-10}"
|
|
154
|
+
local lock_timeout="${OMNILANE_LOCK_TIMEOUT:-600}"
|
|
155
|
+
[[ "$empty_grace" =~ ^(0|[1-9][0-9]{0,5})$ ]] || {
|
|
156
|
+
echo "omnilane: invalid OMNILANE_LOCK_EMPTY_GRACE (want 0..999999)" >&2
|
|
157
|
+
exit 2
|
|
158
|
+
}
|
|
159
|
+
[[ "$lock_timeout" =~ ^[1-9][0-9]{0,5}$ ]] || {
|
|
160
|
+
echo "omnilane: invalid OMNILANE_LOCK_TIMEOUT (want 1..999999)" >&2
|
|
161
|
+
exit 2
|
|
162
|
+
}
|
|
163
|
+
dir="$(cd "$dir" 2>/dev/null && pwd -P)" || dir="$2"
|
|
164
|
+
key="$(printf '%s' "$dir" | hash_str)-$vendor"
|
|
165
|
+
lockdir="$OMNILANE_HOME/locks/$key"
|
|
166
|
+
prepare_lock_store
|
|
167
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
168
|
+
# Steal the lock if its owner is gone (crash / kill -9 leaves the dir behind).
|
|
169
|
+
owner=""
|
|
170
|
+
owner_state="empty"
|
|
171
|
+
if [[ -e "$lockdir/pid" || -L "$lockdir/pid" ]]; then
|
|
172
|
+
if read_lock_owner "$lockdir/pid"; then
|
|
173
|
+
owner="$LOCK_OWNER_VALUE"
|
|
174
|
+
owner_state="valid"
|
|
175
|
+
else
|
|
176
|
+
owner_state="invalid"
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
179
|
+
if [[ "$owner_state" == "invalid" ]]; then
|
|
180
|
+
rm "$lockdir/pid" 2>/dev/null || true
|
|
181
|
+
if rmdir "$lockdir" 2>/dev/null; then
|
|
182
|
+
continue
|
|
183
|
+
fi
|
|
184
|
+
elif [[ "$owner_state" == "valid" ]] && ! kill -0 "$owner" 2>/dev/null; then
|
|
185
|
+
rm "$lockdir/pid" 2>/dev/null || true
|
|
186
|
+
if rmdir "$lockdir" 2>/dev/null; then
|
|
187
|
+
continue
|
|
188
|
+
fi
|
|
189
|
+
elif [[ "$owner_state" == "empty" ]]; then
|
|
190
|
+
[[ "$empty_since" -ge 0 ]] || empty_since="$waited"
|
|
191
|
+
if [[ $((waited - empty_since)) -ge "$empty_grace" ]]; then
|
|
192
|
+
# The creator normally writes pid immediately after mkdir. A lock that
|
|
193
|
+
# stays empty beyond the grace period is an interrupted acquisition.
|
|
194
|
+
rmdir "$lockdir" 2>/dev/null || true
|
|
195
|
+
empty_since=-1
|
|
196
|
+
continue
|
|
197
|
+
fi
|
|
198
|
+
else
|
|
199
|
+
empty_since=-1
|
|
200
|
+
fi
|
|
201
|
+
if [[ "$waited" -ge "$lock_timeout" ]]; then
|
|
202
|
+
echo "omnilane: lock timeout for $vendor in $dir" >&2; exit 87
|
|
203
|
+
fi
|
|
204
|
+
sleep 2; waited=$((waited + 2))
|
|
205
|
+
done
|
|
206
|
+
# Real subshell PID, not $$: in a backgrounded subshell $$ is the (soon-dead)
|
|
207
|
+
# parent, which would make the live lock look stale and steal-able.
|
|
208
|
+
# If an empty lock was reclaimed while this process was descheduled, never
|
|
209
|
+
# overwrite the replacement owner's PID when execution resumes.
|
|
210
|
+
if ! write_lock_owner "$lockdir/pid"; then
|
|
211
|
+
echo "omnilane: lost lock ownership for $vendor in $dir" >&2
|
|
212
|
+
exit 87
|
|
213
|
+
fi
|
|
214
|
+
read_lock_owner "$lockdir/pid" || {
|
|
215
|
+
echo "omnilane: invalid owner metadata after lock acquisition" >&2
|
|
216
|
+
exit 87
|
|
217
|
+
}
|
|
218
|
+
OMNILANE_LOCK_OWNER="$LOCK_OWNER_VALUE"
|
|
219
|
+
OMNILANE_LOCKDIR="$lockdir"
|
|
220
|
+
trap 'release_cwd_lock' EXIT
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
release_cwd_lock() {
|
|
224
|
+
[[ -n "${OMNILANE_LOCKDIR:-}" ]] || return 0
|
|
225
|
+
# Only the recorded owner may remove the lock — a stale-steal may have
|
|
226
|
+
# handed this path to a newer job.
|
|
227
|
+
read_lock_owner "$OMNILANE_LOCKDIR/pid" || return 0
|
|
228
|
+
[[ "$LOCK_OWNER_VALUE" == "${OMNILANE_LOCK_OWNER:-}" ]] || return 0
|
|
229
|
+
rm "$OMNILANE_LOCKDIR/pid" 2>/dev/null || true
|
|
230
|
+
rmdir "$OMNILANE_LOCKDIR" 2>/dev/null || true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Cap payload so a runaway prompt cannot blow a worker's context.
|
|
234
|
+
truncate_payload() { # file, cap_bytes
|
|
235
|
+
local f="$1" cap="${2:-140000}" size
|
|
236
|
+
size=$(wc -c < "$f")
|
|
237
|
+
if [[ "$size" -gt "$cap" ]]; then
|
|
238
|
+
{ head -c $((cap * 7 / 10)) "$f"
|
|
239
|
+
printf '\n\n--- TRUNCATED (original: %s bytes) ---\n\n' "$size"
|
|
240
|
+
tail -c $((cap / 4)) "$f"; } > "$f.tmp"
|
|
241
|
+
mv "$f.tmp" "$f"
|
|
242
|
+
fi
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
strip_ansi() { # file
|
|
246
|
+
sed -i '' 's/\x1b\[[0-9;]*m//g' "$1" 2>/dev/null || sed -i 's/\x1b\[[0-9;]*m//g' "$1" 2>/dev/null || true
|
|
247
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# omnilane i18n — interactive-surface strings for install.sh / configure.sh.
|
|
3
|
+
# Language picked from OMNILANE_LANG, else LC_ALL/LANG, else macOS AppleLocale.
|
|
4
|
+
# bash 3.2 compatible (no associative arrays). LLM-facing text stays English.
|
|
5
|
+
|
|
6
|
+
detect_lang() {
|
|
7
|
+
local l="${LC_ALL:-${LANG:-}}"
|
|
8
|
+
if [[ -z "$l" ]] && command -v defaults >/dev/null 2>&1; then
|
|
9
|
+
l="$(defaults read -g AppleLocale 2>/dev/null || true)"
|
|
10
|
+
fi
|
|
11
|
+
case "$l" in
|
|
12
|
+
zh_TW*|zh-TW*|zh_Hant*|zh-Hant*|zh_HK*|zh-HK*) echo "zh-TW" ;;
|
|
13
|
+
zh*) echo "zh-CN" ;;
|
|
14
|
+
ja*) echo "ja" ;;
|
|
15
|
+
ko*) echo "ko" ;;
|
|
16
|
+
*) echo "en" ;;
|
|
17
|
+
esac
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
OMNILANE_LANG="${OMNILANE_LANG:-$(detect_lang)}"
|
|
21
|
+
|
|
22
|
+
msg() { # key -> localized string ({} = placeholder, see msgf)
|
|
23
|
+
local L="$OMNILANE_LANG"
|
|
24
|
+
case "$1" in
|
|
25
|
+
found)
|
|
26
|
+
case "$L" in zh-TW) echo "已偵測到";; zh-CN) echo "已检测到";; ja) echo "検出しました";; ko) echo "감지됨";; *) echo "found";; esac ;;
|
|
27
|
+
run_manually)
|
|
28
|
+
case "$L" in zh-TW) echo "已偵測到——請自行執行:";; zh-CN) echo "已检测到——请自行执行:";; ja) echo "検出——次を手動で実行:";; ko) echo "감지됨——직접 실행하세요:";; *) echo "found — run this yourself:";; esac ;;
|
|
29
|
+
plugin_hint)
|
|
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
|
+
linked)
|
|
32
|
+
case "$L" in zh-TW) echo "已連結";; zh-CN) echo "已链接";; ja) echo "リンクしました";; ko) echo "링크됨";; *) echo "linked";; esac ;;
|
|
33
|
+
removed)
|
|
34
|
+
case "$L" in zh-TW) echo "已移除";; zh-CN) echo "已移除";; ja) echo "削除しました";; ko) echo "제거됨";; *) echo "removed";; esac ;;
|
|
35
|
+
skip_exists)
|
|
36
|
+
case "$L" in zh-TW) echo "跳過 {}(已存在且不是 symlink——請手動處理)";; zh-CN) echo "跳过 {}(已存在且不是 symlink——请手动处理)";; ja) echo "{} をスキップ(symlink 以外の実体があります——手動で解決してください)";; ko) echo "{} 건너뜀(symlink 가 아닌 항목 존재——수동으로 해결하세요)";; *) echo "skip {} (exists and is not a symlink — resolve manually)";; esac ;;
|
|
37
|
+
foreign_link)
|
|
38
|
+
case "$L" in zh-TW) echo "既有 symlink 不屬於目前 checkout,保持不變: {}";; zh-CN) echo "现有 symlink 不属于当前 checkout,保持不变: {}";; ja) echo "既存 symlink は現在の checkout の所有ではないため変更しません: {}";; ko) echo "기존 symlink 는 현재 checkout 소유가 아니므로 변경하지 않음: {}";; *) echo "existing symlink is not owned by this checkout; unchanged: {}";; esac ;;
|
|
39
|
+
path_hint)
|
|
40
|
+
case "$L" in zh-TW) echo "(請確認 ~/.local/bin 在 PATH 上)";; zh-CN) echo "(请确认 ~/.local/bin 在 PATH 上)";; ja) echo "(~/.local/bin が PATH にあるか確認してください)";; ko) echo "(~/.local/bin 이 PATH 에 있는지 확인하세요)";; *) echo "(make sure ~/.local/bin is on your PATH)";; esac ;;
|
|
41
|
+
no_cli)
|
|
42
|
+
case "$L" in zh-TW) echo "PATH 上找不到支援的 CLI(claude/codex/grok/agy)";; zh-CN) echo "PATH 上找不到支持的 CLI(claude/codex/grok/agy)";; ja) echo "対応 CLI(claude/codex/grok/agy)が PATH に見つかりません";; ko) echo "PATH 에서 지원 CLI(claude/codex/grok/agy)를 찾을 수 없습니다";; *) echo "no supported CLI (claude/codex/grok/agy) on PATH";; esac ;;
|
|
43
|
+
overrides_header)
|
|
44
|
+
case "$L" in zh-TW) echo "本機覆寫檔都在 ~/.omnilane/(永不進版控):";; zh-CN) echo "本机覆盖文件都在 ~/.omnilane/(永不进版本控制):";; ja) echo "マシン固有の設定は ~/.omnilane/ に置かれます(コミットされません):";; ko) echo "머신별 설정은 ~/.omnilane/ 에 있습니다(커밋되지 않음):";; *) echo "Per-machine overrides live in ~/.omnilane/ (never committed):";; esac ;;
|
|
45
|
+
overrides_routing)
|
|
46
|
+
case "$L" in zh-TW) echo "routing.local.yaml — 覆寫任何通道(見 routing.local.yaml.example)";; zh-CN) echo "routing.local.yaml — 覆盖任意通道(见 routing.local.yaml.example)";; ja) echo "routing.local.yaml — 任意のレーンをオーバーライド(routing.local.yaml.example 参照)";; ko) echo "routing.local.yaml — 레인 오버라이드(routing.local.yaml.example 참고)";; *) echo "routing.local.yaml — override any lane (see routing.local.yaml.example)";; esac ;;
|
|
47
|
+
overrides_local)
|
|
48
|
+
case "$L" in zh-TW) echo "local.sh — 執行器用的執行檔路徑/環境變數(見 local.sh.example)";; zh-CN) echo "local.sh — 执行器用的可执行文件路径/环境变量(见 local.sh.example)";; ja) echo "local.sh — ランナー用のバイナリ/環境変数(local.sh.example 参照)";; ko) echo "local.sh — 러너용 바이너리/환경 변수(local.sh.example 참고)";; *) echo "local.sh — binaries/env for the runners (see local.sh.example)";; esac ;;
|
|
49
|
+
effective)
|
|
50
|
+
case "$L" in zh-TW) echo "這台機器的生效路由表(候選鏈已解析):";; zh-CN) echo "这台机器的生效路由表(候选链已解析):";; ja) echo "このマシンの実効ルーティング(フォールバック解決済み):";; ko) echo "이 머신의 실효 라우팅(폴백 해석 완료):";; *) echo "Effective routing on this machine (fallback chains resolved):";; esac ;;
|
|
51
|
+
customize_prompt)
|
|
52
|
+
case "$L" in zh-TW) echo "現在就自訂通道→模型配置嗎?[y/N] ";; zh-CN) echo "现在就自定义通道→模型配置吗?[y/N] ";; ja) echo "レーン→モデル割当を今カスタマイズしますか? [y/N] ";; ko) echo "지금 레인→모델 배정을 사용자화할까요? [y/N] ";; *) echo "Customize lane -> model assignments now? [y/N] ";; esac ;;
|
|
53
|
+
hook_section)
|
|
54
|
+
case "$L" in zh-TW) echo "選配:各 CLI 常駐路由提示(在指令檔尾端加一段有標記的區塊;--uninstall 可移除):";; zh-CN) echo "可选:各 CLI 常驻路由提示(在指令文件末尾加一段带标记的区块;--uninstall 可移除):";; ja) echo "任意:CLI ごとの常駐ルーティングリマインダー(指示ファイル末尾にマーカー付きブロックを追記;--uninstall で削除可):";; ko) echo "선택: CLI 별 상시 라우팅 리마인더(지침 파일 끝에 마커 블록 추가; --uninstall 로 제거 가능):";; *) echo "Optional: per-CLI persistent routing reminder (a marked block appended to each CLI's instruction file; reversed by --uninstall):";; esac ;;
|
|
55
|
+
hook_prompt)
|
|
56
|
+
case "$L" in zh-TW) echo "要把常駐路由提示寫進 {} 嗎?[y/N] ";; zh-CN) echo "要把常驻路由提示写进 {} 吗?[y/N] ";; ja) echo "{} に常駐リマインダーを書き込みますか? [y/N] ";; ko) echo "{} 에 상시 리마인더를 설치할까요? [y/N] ";; *) echo "Install the routing reminder into {}? [y/N] ";; esac ;;
|
|
57
|
+
hook_written)
|
|
58
|
+
case "$L" in zh-TW) echo "路由提示已寫入";; zh-CN) echo "路由提示已写入";; ja) echo "リマインダーを書き込みました:";; ko) echo "리마인더 기록됨:";; *) echo "routing reminder written to";; esac ;;
|
|
59
|
+
hook_removed)
|
|
60
|
+
case "$L" in zh-TW) echo "已移除路由提示:";; zh-CN) echo "已移除路由提示:";; ja) echo "リマインダーを削除しました:";; ko) echo "리마인더 제거됨:";; *) echo "routing reminder removed from";; esac ;;
|
|
61
|
+
cfg_title)
|
|
62
|
+
case "$L" in zh-TW) echo "omnilane 通道設定——目前生效路由:";; zh-CN) echo "omnilane 通道设置——当前生效路由:";; ja) echo "omnilane レーン設定 — 現在の実効ルーティング:";; ko) echo "omnilane 레인 설정 — 현재 실효 라우팅:";; *) echo "omnilane lane configurator — current effective routing:";; esac ;;
|
|
63
|
+
cfg_pick_lane)
|
|
64
|
+
case "$L" in zh-TW) echo "選要覆寫的通道編號,或按 Enter 結束:";; zh-CN) echo "选要覆盖的通道编号,或按 Enter 结束:";; ja) echo "上書きするレーン番号を選択、Enter で終了:";; ko) echo "오버라이드할 레인 번호 선택, Enter 로 종료:";; *) echo "Pick a lane number to override, or press Enter to finish:";; esac ;;
|
|
65
|
+
cfg_pick_range)
|
|
66
|
+
case "$L" in zh-TW) echo "輸入 1-{} 或 Enter";; zh-CN) echo "输入 1-{} 或 Enter";; ja) echo "1-{} か Enter を入力";; ko) echo "1-{} 또는 Enter";; *) echo "pick 1-{} or Enter";; esac ;;
|
|
67
|
+
cfg_vendor_for)
|
|
68
|
+
case "$L" in zh-TW) echo "'{}' 要用哪家:";; zh-CN) echo "'{}' 要用哪家:";; ja) echo "'{}' のベンダー:";; ko) echo "'{}' 의 벤더:";; *) echo "vendor for '{}':";; esac ;;
|
|
69
|
+
cfg_model)
|
|
70
|
+
case "$L" in zh-TW) echo "模型:";; zh-CN) echo "模型:";; ja) echo "モデル:";; ko) echo "모델:";; *) echo "model:";; esac ;;
|
|
71
|
+
cfg_effort)
|
|
72
|
+
case "$L" in zh-TW) echo "推理檔位:";; zh-CN) echo "推理档位:";; ja) echo "推論エフォート:";; ko) echo "추론 강도:";; *) echo "effort:";; esac ;;
|
|
73
|
+
cfg_voters_count)
|
|
74
|
+
case "$L" in zh-TW) echo "要幾個評審?(每評審每輪燒一次額度)";; zh-CN) echo "要几个评审?(每评审每轮烧一次额度)";; ja) echo "投票者は何人?(1 人につき 1 ラウンド 1 コール)";; ko) echo "투표자 수는? (1명당 라운드마다 1콜)";; *) echo "how many voters? (each costs one call per round)";; esac ;;
|
|
75
|
+
cfg_voters_range)
|
|
76
|
+
case "$L" in zh-TW) echo "評審數須為 1-4";; zh-CN) echo "评审数须为 1-4";; ja) echo "投票者は 1-4 人";; ko) echo "투표자는 1-4명";; *) echo "voters must be 1-4";; esac ;;
|
|
77
|
+
cfg_voter_n)
|
|
78
|
+
case "$L" in zh-TW) echo "評審 {}:";; zh-CN) echo "评审 {}:";; ja) echo "投票者 {}:";; ko) echo "투표자 {}:";; *) echo "voter {}:";; esac ;;
|
|
79
|
+
cfg_rounds)
|
|
80
|
+
case "$L" in zh-TW) echo "輪數(2 = 評審互駁):";; zh-CN) echo "轮数(2 = 评审互驳):";; ja) echo "ラウンド数(2 = 相互反論):";; ko) echo "라운드 수(2 = 상호 반박):";; *) echo "rounds (2 = voters rebut each other):";; esac ;;
|
|
81
|
+
cfg_exec_path)
|
|
82
|
+
case "$L" in zh-TW) echo "腳本路徑(參數:MODE WORKDIR EFFORT PROMPT_FILE OUTPUT_FILE): ";; zh-CN) echo "脚本路径(参数:MODE WORKDIR EFFORT PROMPT_FILE OUTPUT_FILE): ";; ja) echo "スクリプトパス(引数: MODE WORKDIR EFFORT PROMPT_FILE OUTPUT_FILE): ";; ko) echo "스크립트 경로(인자: MODE WORKDIR EFFORT PROMPT_FILE OUTPUT_FILE): ";; *) echo "script path (gets MODE WORKDIR EFFORT PROMPT_FILE OUTPUT_FILE): ";; esac ;;
|
|
83
|
+
cfg_empty_path)
|
|
84
|
+
case "$L" in zh-TW) echo "路徑空白,略過";; zh-CN) echo "路径空白,跳过";; ja) echo "パスが空のためスキップ";; ko) echo "경로가 비어 있어 건너뜀";; *) echo "empty path, skipped";; esac ;;
|
|
85
|
+
cfg_custom)
|
|
86
|
+
case "$L" in zh-TW) echo " c) 自訂(自己輸入)";; zh-CN) echo " c) 自定义(自己输入)";; ja) echo " c) カスタム(自由入力)";; ko) echo " c) 직접 입력";; *) echo " c) custom (type your own)";; esac ;;
|
|
87
|
+
cfg_custom_value)
|
|
88
|
+
case "$L" in zh-TW) echo "自訂值: ";; zh-CN) echo "自定义值: ";; ja) echo "カスタム値: ";; ko) echo "직접 입력값: ";; *) echo "custom value: ";; esac ;;
|
|
89
|
+
cfg_pick_or_c)
|
|
90
|
+
case "$L" in zh-TW) echo "輸入 1-{} 或 c";; zh-CN) echo "输入 1-{} 或 c";; ja) echo "1-{} か c を入力";; ko) echo "1-{} 또는 c";; *) echo "pick 1-{} or c";; esac ;;
|
|
91
|
+
cfg_aborted)
|
|
92
|
+
case "$L" in zh-TW) echo "已中止";; zh-CN) echo "已中止";; ja) echo "中止しました";; ko) echo "중단됨";; *) echo "aborted";; esac ;;
|
|
93
|
+
cfg_no_changes)
|
|
94
|
+
case "$L" in zh-TW) echo "沒有變更。";; zh-CN) echo "没有变更。";; ja) echo "変更なし。";; ko) echo "변경 없음.";; *) echo "no changes.";; esac ;;
|
|
95
|
+
cfg_wrote)
|
|
96
|
+
case "$L" in zh-TW) echo "已寫入 {} ——目前生效路由:";; zh-CN) echo "已写入 {} ——当前生效路由:";; ja) echo "{} に書き込みました — 現在の実効ルーティング:";; ko) echo "{} 에 기록됨 — 현재 실효 라우팅:";; *) echo "wrote {} — effective routing now:";; esac ;;
|
|
97
|
+
*) echo "$1" ;;
|
|
98
|
+
esac
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
msgf() { # key, value -> localized string with {} replaced
|
|
102
|
+
local m; m="$(msg "$1")"
|
|
103
|
+
printf '%s' "${m/\{\}/$2}"
|
|
104
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env perl
|
|
2
|
+
use strict;
|
|
3
|
+
use warnings;
|
|
4
|
+
use POSIX qw(setsid WNOHANG);
|
|
5
|
+
use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC sleep);
|
|
6
|
+
|
|
7
|
+
my $POLL_SECONDS = 0.05;
|
|
8
|
+
my $TERM_GRACE_SECONDS = 1.0;
|
|
9
|
+
|
|
10
|
+
sub usage_error {
|
|
11
|
+
print STDERR "usage: job-timeout.pl SECONDS COMMAND [ARG...]\n";
|
|
12
|
+
exit 2;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@ARGV >= 2 or usage_error();
|
|
16
|
+
my $seconds = shift @ARGV;
|
|
17
|
+
$seconds =~ /\A[1-9][0-9]{0,8}\z/ or usage_error();
|
|
18
|
+
|
|
19
|
+
sub monotonic_now {
|
|
20
|
+
return clock_gettime(CLOCK_MONOTONIC);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
sub group_exists {
|
|
24
|
+
my ($pgid) = @_;
|
|
25
|
+
return kill 0, -$pgid;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sub reap_nonblocking {
|
|
29
|
+
my ($pid) = @_;
|
|
30
|
+
my $waited = waitpid($pid, WNOHANG);
|
|
31
|
+
return (1, $?) if $waited == $pid;
|
|
32
|
+
return (1, undef) if $waited == -1;
|
|
33
|
+
return (0, undef);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
sub terminate_group {
|
|
37
|
+
my ($pgid, $pid, $already_reaped) = @_;
|
|
38
|
+
# A signal can arrive in the tiny fork-to-setsid window. Signal the child
|
|
39
|
+
# PID as well as the future group until it is reaped; after reaping, avoid
|
|
40
|
+
# the positive PID because the kernel may reuse it for an unrelated process.
|
|
41
|
+
kill 'TERM', $pid unless $already_reaped;
|
|
42
|
+
kill 'TERM', -$pgid;
|
|
43
|
+
|
|
44
|
+
my $grace_deadline = monotonic_now() + $TERM_GRACE_SECONDS;
|
|
45
|
+
my $reaped = $already_reaped;
|
|
46
|
+
while (monotonic_now() < $grace_deadline) {
|
|
47
|
+
my ($done) = reap_nonblocking($pid);
|
|
48
|
+
$reaped = 1 if $done;
|
|
49
|
+
last unless group_exists($pgid);
|
|
50
|
+
sleep($POLL_SECONDS);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
kill 'KILL', -$pgid if group_exists($pgid);
|
|
54
|
+
kill 'KILL', $pid unless $reaped;
|
|
55
|
+
waitpid($pid, 0) unless $reaped;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
my $forwarded_exit;
|
|
59
|
+
$SIG{HUP} = sub { $forwarded_exit = 129; };
|
|
60
|
+
$SIG{INT} = sub { $forwarded_exit = 130; };
|
|
61
|
+
$SIG{TERM} = sub { $forwarded_exit = 143; };
|
|
62
|
+
|
|
63
|
+
my $deadline = monotonic_now() + $seconds;
|
|
64
|
+
my $pid = fork();
|
|
65
|
+
if (!defined $pid) {
|
|
66
|
+
print STDERR "omnilane: job supervisor could not fork: $!\n";
|
|
67
|
+
exit 125;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ($pid == 0) {
|
|
71
|
+
$SIG{HUP} = $SIG{INT} = $SIG{TERM} = 'DEFAULT';
|
|
72
|
+
setsid() or do {
|
|
73
|
+
print STDERR "omnilane: job supervisor could not create process group: $!\n";
|
|
74
|
+
exit 125;
|
|
75
|
+
};
|
|
76
|
+
exec { $ARGV[0] } @ARGV or do {
|
|
77
|
+
print STDERR "omnilane: job supervisor could not start command: $!\n";
|
|
78
|
+
exit 127;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
my $worker_status;
|
|
83
|
+
while (1) {
|
|
84
|
+
if (defined $forwarded_exit) {
|
|
85
|
+
terminate_group($pid, $pid, 0);
|
|
86
|
+
exit $forwarded_exit;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
my ($reaped, $status) = reap_nonblocking($pid);
|
|
90
|
+
if ($reaped) {
|
|
91
|
+
$worker_status = $status;
|
|
92
|
+
last;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
my $remaining = $deadline - monotonic_now();
|
|
96
|
+
if ($remaining <= 0) {
|
|
97
|
+
terminate_group($pid, $pid, 0);
|
|
98
|
+
exit 124;
|
|
99
|
+
}
|
|
100
|
+
sleep($remaining < $POLL_SECONDS ? $remaining : $POLL_SECONDS);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# A worker can exit while a child it spawned is still alive. The isolated
|
|
104
|
+
# process group belongs only to this job, so clean any such remainder before
|
|
105
|
+
# reporting completion.
|
|
106
|
+
terminate_group($pid, $pid, 1) if group_exists($pid);
|
|
107
|
+
|
|
108
|
+
defined $worker_status or exit 125;
|
|
109
|
+
exit(($worker_status & 127) ? 128 + ($worker_status & 127) : ($worker_status >> 8));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# Internal worker boundary for one dispatch. The optional whole-job supervisor
|
|
4
|
+
# wraps this process so lock wait, retries, and vote rounds share one budget.
|
|
5
|
+
|
|
6
|
+
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
|
7
|
+
|
|
8
|
+
[[ $# -eq 7 ]] || { echo "omnilane: internal job worker received invalid arguments" >&2; exit 2; }
|
|
9
|
+
VENDOR="$1"; MODE="$2"; WORKDIR="$3"; MODEL="$4"; EFFORT="$5"
|
|
10
|
+
PROMPT_FILE="$6"; OUTPUT_FILE="$7"
|
|
11
|
+
|
|
12
|
+
[[ "$VENDOR" =~ ^[a-z][a-z0-9-]*$ ]] || { echo "omnilane: invalid worker vendor" >&2; exit 2; }
|
|
13
|
+
RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
|
|
14
|
+
[[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
|
|
15
|
+
|
|
16
|
+
# Two concurrent codex exec in one target dir corrupt its job index — serialize.
|
|
17
|
+
[[ "$VENDOR" == "codex" ]] && acquire_cwd_lock codex "$WORKDIR"
|
|
18
|
+
|
|
19
|
+
set +e
|
|
20
|
+
"$RUNNER" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" "$PROMPT_FILE" "$OUTPUT_FILE"
|
|
21
|
+
rc=$?
|
|
22
|
+
set -e
|
|
23
|
+
exit "$rc"
|