omnilane 0.15.0 → 0.20.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 +47 -1
- package/README.ja.md +40 -0
- package/README.ko.md +38 -0
- package/README.md +50 -1
- package/README.zh-CN.md +35 -0
- package/README.zh-TW.md +35 -0
- package/VERSION +1 -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 +11 -4
- package/scripts/doctor.sh +126 -0
- package/scripts/jobs.sh +149 -8
- package/scripts/lib/common.sh +88 -0
- package/scripts/lib/i18n.sh +8 -0
- package/scripts/lib/job-worker.sh +173 -4
- package/scripts/runners/run-claude.sh +106 -0
- package/skills/omnilane/SKILL.md +172 -0
|
@@ -1,8 +1,11 @@
|
|
|
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"
|
|
7
10
|
|
|
8
11
|
[[ $# -eq 7 ]] || { echo "omnilane: internal job worker received invalid arguments" >&2; exit 2; }
|
|
@@ -13,11 +16,177 @@ PROMPT_FILE="$6"; OUTPUT_FILE="$7"
|
|
|
13
16
|
RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
|
|
14
17
|
[[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
|
|
15
18
|
|
|
16
|
-
# Two concurrent codex
|
|
19
|
+
# Two concurrent codex execs in one target dir corrupt its job index — serialize.
|
|
17
20
|
[[ "$VENDOR" == "codex" ]] && acquire_cwd_lock codex "$WORKDIR"
|
|
18
21
|
|
|
22
|
+
# The backslash case pattern is intentional.
|
|
23
|
+
# shellcheck disable=SC1003
|
|
24
|
+
json_escape() {
|
|
25
|
+
local s="$1" out="" ch escaped code i
|
|
26
|
+
for ((i = 0; i < ${#s}; i++)); do
|
|
27
|
+
ch="${s:i:1}"
|
|
28
|
+
case "$ch" in
|
|
29
|
+
'"') out="$out\\\"" ;;
|
|
30
|
+
'\\') out="$out\\\\" ;;
|
|
31
|
+
$'\b') out="$out\\b" ;;
|
|
32
|
+
$'\f') out="$out\\f" ;;
|
|
33
|
+
$'\n') out="$out\\n" ;;
|
|
34
|
+
$'\r') out="$out\\r" ;;
|
|
35
|
+
$'\t') out="$out\\t" ;;
|
|
36
|
+
*)
|
|
37
|
+
LC_CTYPE=C printf -v code '%d' "'$ch"
|
|
38
|
+
if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
|
|
39
|
+
printf -v escaped '\\u%04x' "$code"
|
|
40
|
+
out="$out$escaped"
|
|
41
|
+
else
|
|
42
|
+
out="$out$ch"
|
|
43
|
+
fi
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
printf '%s' "$out"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
emit_mode_notice() {
|
|
51
|
+
local notice="$1" notice_file="${OUTPUT_FILE%/*}/mode-notice.txt"
|
|
52
|
+
printf '%s\n' "$notice" >&2
|
|
53
|
+
if [[ -L "$notice_file" || ( -e "$notice_file" && ! -f "$notice_file" ) ]]; then
|
|
54
|
+
echo "omnilane: unsafe mode notice path" >&2
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
(umask 077; printf '%s\n' "$notice" > "$notice_file")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
run_single_shot() {
|
|
61
|
+
local notice="$1" rc
|
|
62
|
+
set +e
|
|
63
|
+
(
|
|
64
|
+
unset OMNILANE_INBOX
|
|
65
|
+
"$RUNNER" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" "$PROMPT_FILE" "$OUTPUT_FILE"
|
|
66
|
+
)
|
|
67
|
+
rc=$?
|
|
68
|
+
set -e
|
|
69
|
+
emit_mode_notice "$notice"
|
|
70
|
+
return "$rc"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if [[ "$VENDOR" != "claude" ]]; then
|
|
74
|
+
set +e
|
|
75
|
+
run_single_shot "omnilane: vendor '$VENDOR' is not live-capable; ran in single-shot mode"
|
|
76
|
+
rc=$?
|
|
77
|
+
set -e
|
|
78
|
+
exit "$rc"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
JOB_DIR="${OUTPUT_FILE%/*}"
|
|
82
|
+
JOB_ID="${JOB_DIR##*/}"
|
|
83
|
+
INBOX_FIFO="$JOB_DIR/inbox.fifo"
|
|
84
|
+
HOLDER_PID_FILE="$JOB_DIR/inbox.holder.pid"
|
|
85
|
+
READY_FILE="$JOB_DIR/inbox.ready"
|
|
86
|
+
EVENTS_FILE="${OUTPUT_FILE}.events.jsonl"
|
|
87
|
+
EVENTS_ALIAS="$JOB_DIR/events.jsonl"
|
|
88
|
+
close_requested=0
|
|
89
|
+
inbox_holder_open=0
|
|
90
|
+
runner_pid=""
|
|
91
|
+
|
|
92
|
+
# Invoked by the EXIT trap below.
|
|
93
|
+
# shellcheck disable=SC2329
|
|
94
|
+
cleanup_live_mailbox() {
|
|
95
|
+
if [[ "$inbox_holder_open" -eq 1 ]]; then
|
|
96
|
+
exec 3>&-
|
|
97
|
+
inbox_holder_open=0
|
|
98
|
+
fi
|
|
99
|
+
rm "$READY_FILE" "$HOLDER_PID_FILE" "$INBOX_FIFO" 2>/dev/null || true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
prepare_live_mailbox() {
|
|
103
|
+
local path old_umask rc=0
|
|
104
|
+
for path in "$INBOX_FIFO" "$HOLDER_PID_FILE" "$READY_FILE" "$EVENTS_FILE" "$EVENTS_ALIAS"; do
|
|
105
|
+
[[ ! -e "$path" && ! -L "$path" ]] || return 1
|
|
106
|
+
done
|
|
107
|
+
old_umask="$(umask)"
|
|
108
|
+
umask 077
|
|
109
|
+
mkfifo "$INBOX_FIFO" || rc=$?
|
|
110
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
111
|
+
: > "$EVENTS_FILE" || rc=$?
|
|
112
|
+
fi
|
|
113
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
114
|
+
ln "$EVENTS_FILE" "$EVENTS_ALIAS" || rc=$?
|
|
115
|
+
fi
|
|
116
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
117
|
+
chmod 600 "$INBOX_FIFO" "$EVENTS_FILE" "$EVENTS_ALIAS" || rc=$?
|
|
118
|
+
fi
|
|
119
|
+
umask "$old_umask"
|
|
120
|
+
if [[ "$rc" -ne 0 ]]; then
|
|
121
|
+
rm "$EVENTS_ALIAS" "$EVENTS_FILE" "$INBOX_FIFO" 2>/dev/null || true
|
|
122
|
+
fi
|
|
123
|
+
return "$rc"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
last_result_status() {
|
|
127
|
+
local last_result
|
|
128
|
+
last_result="$(grep -E '"type"[[:space:]]*:[[:space:]]*"result"' "$EVENTS_FILE" 2>/dev/null | tail -n 1 || true)"
|
|
129
|
+
[[ -n "$last_result" ]] || return 1
|
|
130
|
+
[[ ! "$last_result" =~ "is_error"[[:space:]]*:[[:space:]]*true ]]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ! prepare_live_mailbox; then
|
|
134
|
+
set +e
|
|
135
|
+
run_single_shot "omnilane: Claude live mailbox unavailable because FIFO setup failed; ran in single-shot mode"
|
|
136
|
+
rc=$?
|
|
137
|
+
set -e
|
|
138
|
+
exit "$rc"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
trap 'close_requested=1' USR1
|
|
142
|
+
trap cleanup_live_mailbox EXIT
|
|
143
|
+
truncate_payload "$PROMPT_FILE" 102400
|
|
144
|
+
INITIAL_TEXT="$(cat "$PROMPT_FILE")"
|
|
145
|
+
if [[ "${FOREMAN_SESSION+x}" == "x" ]]; then
|
|
146
|
+
FOREMAN_SESSION_VALUE="$FOREMAN_SESSION"
|
|
147
|
+
else
|
|
148
|
+
FOREMAN_SESSION_VALUE="${foreman_session-}"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
write_current_pid_file "$HOLDER_PID_FILE"
|
|
152
|
+
export OMNILANE_INBOX="$INBOX_FIFO"
|
|
153
|
+
"$RUNNER" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" "$PROMPT_FILE" "$OUTPUT_FILE" &
|
|
154
|
+
runner_pid=$!
|
|
155
|
+
|
|
156
|
+
# Open only after the reader starts. The runner was launched first, so it cannot
|
|
157
|
+
# inherit this write descriptor. FD 3 is the named holder for the conversation.
|
|
158
|
+
exec 3> "$INBOX_FIFO"
|
|
159
|
+
inbox_holder_open=1
|
|
160
|
+
if [[ "$close_requested" -eq 0 ]]; then
|
|
161
|
+
printf '{"type":"user","omnilane_job_id":"%s","foreman_session":"%s","message":{"role":"user","content":[{"type":"text","text":"%s"}]}}\n' \
|
|
162
|
+
"$(json_escape "$JOB_ID")" "$(json_escape "$FOREMAN_SESSION_VALUE")" \
|
|
163
|
+
"$(json_escape "$INITIAL_TEXT")" >&3
|
|
164
|
+
fi
|
|
165
|
+
(umask 077; : > "$READY_FILE")
|
|
166
|
+
|
|
19
167
|
set +e
|
|
20
|
-
|
|
21
|
-
|
|
168
|
+
wait "$runner_pid"
|
|
169
|
+
runner_rc=$?
|
|
22
170
|
set -e
|
|
171
|
+
|
|
172
|
+
if [[ "$close_requested" -eq 1 ]]; then
|
|
173
|
+
exec 3>&-
|
|
174
|
+
inbox_holder_open=0
|
|
175
|
+
set +e
|
|
176
|
+
kill -TERM "$runner_pid" 2>/dev/null || true
|
|
177
|
+
wait "$runner_pid" 2>/dev/null
|
|
178
|
+
set -e
|
|
179
|
+
if last_result_status; then
|
|
180
|
+
rc=0
|
|
181
|
+
else
|
|
182
|
+
rc=1
|
|
183
|
+
emit_mode_notice "omnilane: Claude live mailbox closed without a successful result event"
|
|
184
|
+
fi
|
|
185
|
+
else
|
|
186
|
+
exec 3>&-
|
|
187
|
+
inbox_holder_open=0
|
|
188
|
+
rc="$runner_rc"
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
trap - USR1
|
|
23
192
|
exit "$rc"
|
|
@@ -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
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: omnilane
|
|
3
|
+
description: 'Universal model-routing table + cross-vendor dispatch for ANY harness (Claude Code, Codex, Grok Build, Antigravity). Use when delegating subtasks, choosing a model for work, planning multi-part tasks, or when asked about model routing, delegate, dispatch, which model, tier selection, escalate, 派工, 模型路由. One routing table; the main loop self-executes its own lane and shells out to every other vendor via dispatch.sh.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# omnilane — one routing table, every harness
|
|
7
|
+
|
|
8
|
+
You (the main loop) may be Claude, GPT, Grok, or Gemini. The procedure is identical:
|
|
9
|
+
|
|
10
|
+
1. **Identify your main model.** You know which model you are running as.
|
|
11
|
+
2. **Split the work into subtasks and classify each into a lane** (table below).
|
|
12
|
+
3. **If the lane's model is you, self-execute.** Otherwise dispatch:
|
|
13
|
+
`<repo>/scripts/dispatch.sh [--vendor V] [--mode work] [--workdir DIR] <lane> "<task>"`
|
|
14
|
+
Add `--background` for long tasks; poll with `scripts/jobs.sh status|result <id>`.
|
|
15
|
+
Before changing lane order from anecdotal outcomes, run
|
|
16
|
+
`scripts/jobs.sh recommend [--last N] [--lane L] [--min-samples N]` and report
|
|
17
|
+
its evidence threshold. The command is read-only and never changes routing.
|
|
18
|
+
Preview old completed-job cleanup with `scripts/jobs.sh prune --keep <N>`;
|
|
19
|
+
deletion requires the explicit `--apply` flag and never targets running jobs.
|
|
20
|
+
A deep task whose CLI call may outrun the 600s per-call watchdog can raise its
|
|
21
|
+
cap with `--timeout <seconds>` (e.g. `--timeout 1200` for hard-judgment /
|
|
22
|
+
long-context). It bounds each CLI call, not the whole dispatch.
|
|
23
|
+
For one aggregate fuse across lock wait, retries, voters, and rounds, add
|
|
24
|
+
`--job-timeout <seconds>`. It is disabled by default; deep full-repository
|
|
25
|
+
audits typically need 7200–14400 seconds, and expiry returns 124. The one
|
|
26
|
+
automatic exception is non-Git Codex `work`: without an explicit, lane, or
|
|
27
|
+
global job timeout, its resolved per-call timeout becomes the whole-job fuse,
|
|
28
|
+
capped at the supervisor's 999999999-second maximum. If the bundled Perl
|
|
29
|
+
supervisor is unavailable, it warns and continues through the existing
|
|
30
|
+
per-call watchdog path.
|
|
31
|
+
|
|
32
|
+
Run `scripts/dispatch.sh --list` to see the effective table (local overrides win).
|
|
33
|
+
When routing is unexpectedly unavailable, run `bin/omnilane doctor` before
|
|
34
|
+
changing configuration; it reports state and dependencies without repairing them.
|
|
35
|
+
Doctor remains offline unless the operator explicitly adds `--probe V`; that
|
|
36
|
+
bounded probe returns metadata only. Use `bin/omnilane benchmark` for a fixed
|
|
37
|
+
no-call route plan, and add `--run` only when actual advise-mode comparison calls
|
|
38
|
+
were explicitly requested. Neither command changes routing.
|
|
39
|
+
Lanes are fallback chains — dispatch uses the first vendor CLI actually installed,
|
|
40
|
+
so the same table works with any subset of subscriptions.
|
|
41
|
+
|
|
42
|
+
## Lanes (defaults; see routing.yaml for the live values)
|
|
43
|
+
|
|
44
|
+
Each lane's **backup** is the next candidate in its `routing.yaml` chain —
|
|
45
|
+
what dispatch picks when the first-choice vendor CLI is not installed.
|
|
46
|
+
|
|
47
|
+
| Lane | First choice | Backup | When |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| hardest-coding | GPT-5.6 Sol (xhigh) | Claude Opus 5 (xhigh) | Hardest implementation, deep root-cause debug, correctness-critical edits |
|
|
50
|
+
| bulk-mechanical | GPT-5.6 Terra (max) | Claude Sonnet 5 (high) | Refactors, migrations, tests, review sweeps — mechanical endurance |
|
|
51
|
+
| triage | GPT-5.6 Luna (medium) | Gemini 3.6 Flash (Low) | High-volume scans, first-pass filtering |
|
|
52
|
+
| hard-judgment | Claude Opus 5 (xhigh) | GPT-5.6 Sol (max) | Architecture arbitration, deep reasoning, second opinions |
|
|
53
|
+
| taste-final | Claude Opus 5 (high) | GPT-5.6 Sol (max) | User-facing prose, prompt/doc polish, Chinese phrasing, style arbitration |
|
|
54
|
+
| consult | Explicit named vendor/model | — (no fallback) | Direct natural-language consultation; always keep `--vendor` |
|
|
55
|
+
| ui-draft | GPT-5.6 Sol (xhigh) | Claude Opus 5 (high) | UI drafts only WITH a design system / reference images; open-ended visual taste goes to taste-final |
|
|
56
|
+
| long-context | Gemini 3.1 Pro (High) | GPT-5.6 Sol (high) | 1M-token synthesis; Pro is agentic-capable, while fast repeated loops prefer Flash on speed/cost |
|
|
57
|
+
| fast-agentic | GPT-5.6 Luna (max) | Gemini 3.6 Flash (High) | Fast multi-step agentic loops, multimodal checks |
|
|
58
|
+
| live-search | Grok 4.5 | — (off) | Realtime X/web search and social context |
|
|
59
|
+
| coding-overflow | Grok 4.5 | Kimi K3 → Qwen3 Coder Plus → OpenCode | Codex-quota relief valve for mid-tier coding; verify factual claims |
|
|
60
|
+
| arbitrate | off (opt-in vote panel) | — | Disabled by default. Enable with `arbitrate: vote codex,claude,grok -` in routing.local.yaml or via the configurator (any 1-4 voters). One quota hit PER VOTER PER ROUND; you chair: read the opinions and own the decision. Effort field 2 = debate round (voters rebut each other) |
|
|
61
|
+
|
|
62
|
+
Claude Fable 5 (`claude-fable-5`) is absent from the defaults on purpose: the
|
|
63
|
+
top Claude tier is usually the main loop itself, not a dispatched worker, and
|
|
64
|
+
it prices at twice Opus 5. This is a cost / guardrail / main-loop policy choice,
|
|
65
|
+
not a capability verdict — Artificial Analysis calls Opus 5 (61) and Fable 5 (60)
|
|
66
|
+
"effectively tied" on the Intelligence Index, but Opus 5 leads AA-Briefcase by
|
|
67
|
+
146 Elo at 20% lower cost per task. Fable 5 keeps the lead on factual breadth
|
|
68
|
+
(AA-Omniscience), so name it explicitly for recall-heavy consults. To route to
|
|
69
|
+
it anyway, select it in the configurator or override a lane in
|
|
70
|
+
`~/.omnilane/routing.local.yaml` (e.g. `taste-final: claude claude-fable-5 high`).
|
|
71
|
+
|
|
72
|
+
## Natural-language consultation
|
|
73
|
+
|
|
74
|
+
Users may speak normally; they do not need lane names.
|
|
75
|
+
|
|
76
|
+
1. Capability-only question (`which model`, `what can Claude do`, `哪個模型`,
|
|
77
|
+
`誰適合`) → classify the need, then answer with the first available model
|
|
78
|
+
shown for that lane by `dispatch.sh --list`; do not dispatch unless execution
|
|
79
|
+
is also requested.
|
|
80
|
+
2. Generic vendor name (`Claude`, `Codex`, `Grok`, `Gemini`, `OpenCode`) → run
|
|
81
|
+
`dispatch.sh --vendor <vendor> consult "<task>"`.
|
|
82
|
+
3. Canonical model alias → pass its vendor, model, and effort from the table
|
|
83
|
+
below. Never silently substitute another model family.
|
|
84
|
+
4. No named target → classify into an existing lane and dispatch normally.
|
|
85
|
+
5. Unknown or ambiguous nickname → ask for clarification; do not guess or run.
|
|
86
|
+
|
|
87
|
+
| Alias | Vendor | Model | Effort |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| Opus | claude | claude-opus-5 | high |
|
|
90
|
+
| Fable | claude | claude-fable-5 | high |
|
|
91
|
+
| Sonnet | claude | claude-sonnet-5 | high |
|
|
92
|
+
| Haiku | claude | claude-haiku-4-5 | - |
|
|
93
|
+
| Sol | codex | gpt-5.6-sol | max |
|
|
94
|
+
| Terra | codex | gpt-5.6-terra | max |
|
|
95
|
+
| Luna | codex | gpt-5.6-luna | medium |
|
|
96
|
+
| Grok 4.5 | grok | grok-4.5 | - |
|
|
97
|
+
| Gemini Pro | gemini | Gemini 3.1 Pro (High) | - |
|
|
98
|
+
| Gemini Flash | gemini | Gemini 3.6 Flash (High) | - |
|
|
99
|
+
| Kimi | kimi | kimi-k3 | - |
|
|
100
|
+
| Qwen | qwen | qwen3-coder-plus | - |
|
|
101
|
+
| OpenCode | opencode | provider/model form, or `-` for its own default | - |
|
|
102
|
+
| OpenRouter | openrouter | explicit OpenRouter slug (e.g. anthropic/claude-sonnet-5) | - |
|
|
103
|
+
|
|
104
|
+
OpenCode is the multi-provider aggregator CLI (75+ providers): work-capable,
|
|
105
|
+
last resort in coding-overflow. OpenRouter is direct-API — no CLI needed, only
|
|
106
|
+
`OPENROUTER_API_KEY` — and is **advise/consult only** (it cannot edit files);
|
|
107
|
+
its model slug is mandatory. "Ask <any hosted model> via OpenRouter" →
|
|
108
|
+
`dispatch.sh --vendor openrouter --model <slug> consult "<task>"`.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
|
|
112
|
+
- Ask Opus to challenge this architecture →
|
|
113
|
+
`dispatch.sh --vendor claude --model claude-opus-5 --effort high consult "challenge this architecture"`
|
|
114
|
+
- 請 Grok 查最新公開資訊 →
|
|
115
|
+
`dispatch.sh --vendor grok consult "查最新公開資訊"`
|
|
116
|
+
- 哪個模型適合檢查大型 repo? → answer only; do not dispatch.
|
|
117
|
+
|
|
118
|
+
Consultation defaults to `advise`. Use `--mode work --workdir <dir>` only for
|
|
119
|
+
an explicit edit request. Missing explicit targets fail clearly; never remove
|
|
120
|
+
`--vendor` to obtain a fallback.
|
|
121
|
+
|
|
122
|
+
## Live UI is observation only
|
|
123
|
+
|
|
124
|
+
The optional Live UI is a read-only observer, not a prompt or dispatch path.
|
|
125
|
+
It displays existing jobs' `task.txt` and public `out.txt`, but never raw logs;
|
|
126
|
+
its history search and state filters can export only the currently visible public
|
|
127
|
+
metadata as local JSON; tokens and task/result bodies are excluded from export.
|
|
128
|
+
it cannot interpret natural language, choose routes, dispatch, retry, cancel,
|
|
129
|
+
delete jobs, or edit configuration. Natural-language interpretation and
|
|
130
|
+
dispatch stay in this skill and the CLI. Manage the local board with
|
|
131
|
+
`omnilane ui start|status|url|stop`, and stop it when monitoring is finished.
|
|
132
|
+
|
|
133
|
+
## Rules
|
|
134
|
+
|
|
135
|
+
- **Dispatch in `advise` mode by default** (read-only worker). Use `--mode work`
|
|
136
|
+
only when the worker must edit files, and give it an explicit `--workdir`.
|
|
137
|
+
- **`--mode sysops`** is `work` minus the vendor sandbox, for service
|
|
138
|
+
operations the sandbox denies (launchctl, system daemons). Codex runs with
|
|
139
|
+
`-s danger-full-access`; other vendors treat it as `work`. Explicit
|
|
140
|
+
per-dispatch opt-in only — never a lane default, and the task text must
|
|
141
|
+
name the exact service commands the worker is authorized to run.
|
|
142
|
+
Codex `work`/`sysops` still needs a git-repo `--workdir` (non-git
|
|
143
|
+
directories trip the whole-job fuse).
|
|
144
|
+
- **Every dispatched task states acceptance criteria and the exact verification
|
|
145
|
+
command.** Do not accept "done" without evidence.
|
|
146
|
+
- **No nested dispatch**: workers must not fan out again (enforced via
|
|
147
|
+
`OMNILANE_DEPTH`). Escalate back to the main loop instead.
|
|
148
|
+
- **Same-directory codex dispatches are serialized automatically** (lock);
|
|
149
|
+
do not try to parallelize them yourself.
|
|
150
|
+
- Escalate without asking: two failed attempts on a lane → move one lane up
|
|
151
|
+
(triage → bulk-mechanical → hardest-coding).
|
|
152
|
+
- Vendor quota exhausted (429 / "stream disconnected" / usage-limit message):
|
|
153
|
+
send mid-tier coding through coding-overflow instead; never silently downgrade
|
|
154
|
+
hardest-coding — wait or escalate to the user.
|
|
155
|
+
|
|
156
|
+
## Per-model notes (apply the row matching YOUR main model)
|
|
157
|
+
|
|
158
|
+
- **Claude (Fable/Opus main)**: top judgment and taste are yours — self-execute;
|
|
159
|
+
push mechanical coding volume out to the codex lanes.
|
|
160
|
+
- **Claude Sonnet main**: coordination/tools/mid-tier coding only; never
|
|
161
|
+
self-assign top judgment or hardest implementation.
|
|
162
|
+
- **GPT Sol main**: hardest coding + hard judgment are yours (use max for
|
|
163
|
+
judgment turns, xhigh for coding); cross to taste-final for style calls.
|
|
164
|
+
- **GPT Terra main**: bulk work is yours at max; escalate the genuinely hardest
|
|
165
|
+
pieces to Sol instead of grinding.
|
|
166
|
+
- **Grok 4.5 main**: mid-tier coding + live-search are yours; verify every API
|
|
167
|
+
signature and cited fact before shipping (measured high hallucination rate).
|
|
168
|
+
- **Gemini Flash main**: fast agentic/multimodal loops are yours; never
|
|
169
|
+
self-assign top judgment.
|
|
170
|
+
- **Gemini 3.1 Pro main**: 1M-context synthesis and context-heavy agentic work
|
|
171
|
+
are yours. Prefer Gemini Flash for fast repeated tool loops on speed/cost;
|
|
172
|
+
route hardest coding and judgment to the stronger codex lanes.
|