omnilane 0.32.0 → 0.34.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.
@@ -9,6 +9,34 @@ export OMNILANE_REPO
9
9
  # Publishable default is plain CLIs on PATH; power users add ~/.omnilane/local.sh.
10
10
  [[ -f "$OMNILANE_HOME/local.sh" ]] && source "$OMNILANE_HOME/local.sh"
11
11
 
12
+ # The unquoted backslash case pattern intentionally matches one backslash.
13
+ # shellcheck disable=SC1003
14
+ json_escape() {
15
+ local s="$1" out="" ch escaped code i
16
+ for ((i = 0; i < ${#s}; i++)); do
17
+ ch="${s:i:1}"
18
+ case "$ch" in
19
+ '"') out="$out\\\"" ;;
20
+ \\) out="$out\\\\" ;;
21
+ $'\b') out="$out\\b" ;;
22
+ $'\f') out="$out\\f" ;;
23
+ $'\n') out="$out\\n" ;;
24
+ $'\r') out="$out\\r" ;;
25
+ $'\t') out="$out\\t" ;;
26
+ *)
27
+ LC_CTYPE=C printf -v code '%d' "'$ch"
28
+ if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
29
+ printf -v escaped '\\u%04x' "$code"
30
+ out="$out$escaped"
31
+ else
32
+ out="$out$ch"
33
+ fi
34
+ ;;
35
+ esac
36
+ done
37
+ printf '%s' "$out"
38
+ }
39
+
12
40
  resolve_timeout_cmd() {
13
41
  if command -v timeout &>/dev/null; then echo "timeout";
14
42
  elif command -v gtimeout &>/dev/null; then echo "gtimeout";
@@ -284,6 +312,66 @@ prepare_private_store() { # path, diagnostic label
284
312
  chmod 700 "$store_root" || return 1
285
313
  }
286
314
 
315
+ OMNILANE_THREAD_NAME_PATTERN='^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'
316
+
317
+ omnilane_valid_thread_name() {
318
+ [[ "${1:-}" =~ $OMNILANE_THREAD_NAME_PATTERN ]]
319
+ }
320
+
321
+ prepare_threads_store() {
322
+ prepare_private_store "$OMNILANE_HOME/threads" "thread store"
323
+ }
324
+
325
+ read_thread_state() { # path, expected name; populates THREAD_STATE_*
326
+ local path="$1" expected_name="$2" fields separator=$'\034'
327
+ THREAD_STATE_NAME=""
328
+ THREAD_STATE_VENDOR=""
329
+ THREAD_STATE_MODEL=""
330
+ THREAD_STATE_EFFORT=""
331
+ THREAD_STATE_WORKDIR=""
332
+ THREAD_STATE_SESSION_ID=""
333
+ THREAD_STATE_TURNS=""
334
+ THREAD_STATE_LAST_JOB_ID=""
335
+ THREAD_STATE_CREATED=""
336
+ THREAD_STATE_UPDATED=""
337
+
338
+ [[ -f "$path" && ! -L "$path" ]] || return 1
339
+ fields="$(perl -MJSON::PP -e '
340
+ use strict;
341
+ use warnings;
342
+ my ($path, $expected) = @ARGV;
343
+ my $size = -s $path;
344
+ die "invalid size\n" unless defined($size) && $size > 0 && $size <= 16384;
345
+ open my $fh, "<", $path or die $!;
346
+ local $/;
347
+ my $state = decode_json(<$fh>);
348
+ die "invalid state\n" unless ref($state) eq "HASH";
349
+ my @string_keys = qw(name vendor model effort workdir session_id last_job_id created updated);
350
+ for my $key (@string_keys) {
351
+ my $value = $state->{$key};
352
+ die "invalid $key\n" if !defined($value) || ref($value) || $value =~ /[\x00-\x1f\x7f]/;
353
+ }
354
+ die "wrong name\n" unless $state->{name} eq $expected;
355
+ die "invalid name\n" unless $state->{name} =~ /\A[A-Za-z0-9][A-Za-z0-9._-]{0,63}\z/;
356
+ die "invalid vendor\n" unless $state->{vendor} =~ /\A[a-z][a-z0-9-]*\z/;
357
+ die "invalid session\n" unless $state->{session_id} =~ /\A[A-Za-z0-9._:-]{1,256}\z/;
358
+ die "invalid turns\n" if ref($state->{turns}) || ($state->{turns} // "") !~ /\A[1-9][0-9]{0,8}\z/;
359
+ die "invalid job id\n" unless $state->{last_job_id} =~ /\A[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+\z/;
360
+ for my $key (qw(created updated)) {
361
+ die "invalid timestamp\n" unless $state->{$key} =~ /\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\z/;
362
+ }
363
+ die "field too long\n" if length($state->{model}) > 512 || length($state->{effort}) > 128 || length($state->{workdir}) > 4096;
364
+ print join(chr(28), map { $state->{$_} } qw(name vendor model effort workdir session_id turns last_job_id created updated));
365
+ ' "$path" "$expected_name" 2>/dev/null)" || return 1
366
+
367
+ # shellcheck disable=SC2034 # globals consumed by dispatch.sh and jobs.sh
368
+ IFS="$separator" read -r THREAD_STATE_NAME THREAD_STATE_VENDOR \
369
+ THREAD_STATE_MODEL THREAD_STATE_EFFORT THREAD_STATE_WORKDIR \
370
+ THREAD_STATE_SESSION_ID THREAD_STATE_TURNS THREAD_STATE_LAST_JOB_ID \
371
+ THREAD_STATE_CREATED THREAD_STATE_UPDATED <<< "$fields"
372
+ [[ -n "$THREAD_STATE_UPDATED" ]]
373
+ }
374
+
287
375
  prepare_inbox_store() {
288
376
  prepare_private_store "$OMNILANE_HOME/inbox" "inbox store"
289
377
  }
@@ -21,34 +21,6 @@ RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
21
21
  # Two concurrent codex execs in one target dir corrupt its job index — serialize.
22
22
  [[ "$VENDOR" == "codex" ]] && acquire_cwd_lock codex "$WORKDIR"
23
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
24
  emit_mode_notice() {
53
25
  local notice="$1" notice_file="${OUTPUT_FILE%/*}/mode-notice.txt"
54
26
  printf '%s\n' "$notice" >&2
@@ -23,7 +23,7 @@ json_escape() {
23
23
  ch="${s:i:1}"
24
24
  case "$ch" in
25
25
  '"') out="$out\\\"" ;;
26
- '\\') out="$out\\\\" ;;
26
+ \\) out="$out\\\\" ;;
27
27
  $'\b') out="$out\\b" ;;
28
28
  $'\f') out="$out\\f" ;;
29
29
  $'\n') out="$out\\n" ;;
@@ -57,7 +57,7 @@ json_escape() {
57
57
  ch="${value:i:1}"
58
58
  case "$ch" in
59
59
  '"') out="$out\\\"" ;;
60
- '\\') out="$out\\\\" ;;
60
+ \\) out="$out\\\\" ;;
61
61
  $'\b') out="$out\\b" ;;
62
62
  $'\f') out="$out\\f" ;;
63
63
  $'\n') out="$out\\n" ;;
@@ -14,6 +14,21 @@ RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
14
14
 
15
15
  truncate_payload "$PROMPT_FILE" 102400
16
16
 
17
+ THREAD_MODE="${OMNILANE_THREAD_MODE:-}"
18
+ THREAD_ID="${OMNILANE_THREAD_ID:-}"
19
+ THREAD_ARGS=()
20
+ if [[ -n "$THREAD_MODE" || -n "$THREAD_ID" ]]; then
21
+ [[ "$THREAD_ID" =~ ^[A-Za-z0-9._:-]+$ && "${#THREAD_ID}" -le 256 ]] || {
22
+ echo "omnilane: invalid Claude thread session id" >&2
23
+ exit 2
24
+ }
25
+ case "$THREAD_MODE" in
26
+ new) THREAD_ARGS=(--session-id "$THREAD_ID") ;;
27
+ resume) THREAD_ARGS=(--resume "$THREAD_ID") ;;
28
+ *) echo "omnilane: invalid Claude thread mode" >&2; exit 2 ;;
29
+ esac
30
+ fi
31
+
17
32
  LIVE_INBOX="${OMNILANE_INBOX:-}"
18
33
  if [[ -n "$LIVE_INBOX" && -p "$LIVE_INBOX" ]]; then
19
34
  EVENTS_FILE="${OUTPUT_FILE}.events.jsonl"
@@ -118,7 +133,7 @@ PY
118
133
  exit "$RC"
119
134
  fi
120
135
 
121
- ARGS=(--disable-slash-commands --model "$MODEL" --output-format text)
136
+ ARGS=(--disable-slash-commands --model "$MODEL")
122
137
  [[ -n "$EFFORT" && "$EFFORT" != "-" ]] && ARGS+=(--effort "$EFFORT")
123
138
  if [[ "$MODE" == "advise" ]]; then
124
139
  # Read-only surface: the worker can inspect the repo but not change or run anything.
@@ -126,18 +141,62 @@ if [[ "$MODE" == "advise" ]]; then
126
141
  else
127
142
  ARGS+=(--permission-mode acceptEdits)
128
143
  fi
144
+ if [[ -n "$THREAD_MODE" ]]; then
145
+ ARGS+=(--verbose --output-format stream-json)
146
+ ARGS+=("${THREAD_ARGS[@]}")
147
+ else
148
+ ARGS+=(--output-format text)
149
+ fi
129
150
  ARGS+=(-p "$(cat "$PROMPT_FILE")")
130
151
 
131
152
  set +e
132
153
  (
133
154
  cd "$WORKDIR" || exit 127
134
- run_with_timeout "$RUN_TIMEOUT" env \
135
- OMNILANE_DEPTH=1 \
136
- "$CLAUDE_BIN" "${ARGS[@]}" > "${OUTPUT_FILE}.tmp" 2> "${OUTPUT_FILE}.stderr.log"
155
+ if [[ -n "$THREAD_MODE" ]]; then
156
+ run_with_timeout "$RUN_TIMEOUT" env \
157
+ OMNILANE_DEPTH=1 \
158
+ "$CLAUDE_BIN" "${ARGS[@]}" > "${OUTPUT_FILE}.events.jsonl" 2> "${OUTPUT_FILE}.stderr.log"
159
+ else
160
+ run_with_timeout "$RUN_TIMEOUT" env \
161
+ OMNILANE_DEPTH=1 \
162
+ "$CLAUDE_BIN" "${ARGS[@]}" > "${OUTPUT_FILE}.tmp" 2> "${OUTPUT_FILE}.stderr.log"
163
+ fi
137
164
  )
138
165
  RC=$?
139
166
  set -e
140
167
 
168
+ if [[ -n "$THREAD_MODE" ]]; then
169
+ if [[ "$RC" -eq 0 ]]; then
170
+ if ! python3 - "$OUTPUT_FILE.events.jsonl" "${OUTPUT_FILE}.tmp" <<'PY'
171
+ import json
172
+ import pathlib
173
+ import sys
174
+
175
+ events_path = pathlib.Path(sys.argv[1])
176
+ output_path = pathlib.Path(sys.argv[2])
177
+ last_result = None
178
+ with events_path.open(encoding="utf-8") as events:
179
+ for raw_line in events:
180
+ try:
181
+ event = json.loads(raw_line)
182
+ except json.JSONDecodeError:
183
+ continue
184
+ if event.get("type") == "result" and isinstance(event.get("result"), str):
185
+ last_result = event["result"]
186
+ if last_result is None:
187
+ raise SystemExit(1)
188
+ output_path.write_text(last_result.rstrip("\n") + "\n", encoding="utf-8")
189
+ PY
190
+ then
191
+ echo "omnilane: Claude thread stream ended without readable result event" >> "${OUTPUT_FILE}.stderr.log"
192
+ RC=1
193
+ fi
194
+ fi
195
+ if [[ "$RC" -ne 0 && -s "${OUTPUT_FILE}.stderr.log" ]]; then
196
+ cat "${OUTPUT_FILE}.stderr.log" >&2
197
+ cat "${OUTPUT_FILE}.stderr.log" > "${OUTPUT_FILE}.tmp"
198
+ fi
199
+ fi
141
200
  [[ -f "${OUTPUT_FILE}.tmp" ]] && mv "${OUTPUT_FILE}.tmp" "$OUTPUT_FILE"
142
201
  [[ -s "${OUTPUT_FILE}.stderr.log" ]] || rm "${OUTPUT_FILE}.stderr.log" 2>/dev/null || true
143
202
  exit "$RC"
@@ -14,19 +14,46 @@ MODE="$1"; WORKDIR="$2"; MODEL="$3"; EFFORT="$4"; PROMPT_FILE="$5"; OUTPUT_FILE=
14
14
  CODEX_BIN="${CODEX_BIN:-codex}"
15
15
  RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
16
16
 
17
+ THREAD_MODE="${OMNILANE_THREAD_MODE:-}"
18
+ THREAD_ID="${OMNILANE_THREAD_ID:-}"
19
+ if [[ -n "$THREAD_MODE" || -n "$THREAD_ID" ]]; then
20
+ [[ "$THREAD_ID" =~ ^[A-Za-z0-9._:-]+$ && "${#THREAD_ID}" -le 256 ]] || {
21
+ echo "omnilane: invalid Codex thread session id" >&2
22
+ exit 2
23
+ }
24
+ case "$THREAD_MODE" in
25
+ new) ;;
26
+ resume) ;;
27
+ *) echo "omnilane: invalid Codex thread mode" >&2; exit 2 ;;
28
+ esac
29
+ fi
30
+
17
31
  # --skip-git-repo-check: the operator chose WORKDIR explicitly; codex would
18
32
  # otherwise refuse any directory that is not a trusted git repo.
19
33
  # --json: without it codex writes nothing until it exits, so a watchdog kill
20
34
  # leaves an empty progress log that looks identical to a run that never started.
21
- ARGS=(exec --json -m "$MODEL" -o "${OUTPUT_FILE}.tmp" --skip-git-repo-check)
35
+ if [[ "$THREAD_MODE" == "resume" ]]; then
36
+ ARGS=(exec resume --json -m "$MODEL" -o "${OUTPUT_FILE}.tmp" --skip-git-repo-check)
37
+ else
38
+ ARGS=(exec --json -m "$MODEL" -o "${OUTPUT_FILE}.tmp" --skip-git-repo-check)
39
+ fi
22
40
  [[ -n "$EFFORT" && "$EFFORT" != "-" ]] && ARGS+=(-c "model_reasoning_effort=\"$EFFORT\"")
23
41
  if [[ "$MODE" == "advise" ]]; then
24
- ARGS+=(--ephemeral -s read-only)
42
+ [[ -z "$THREAD_MODE" ]] && ARGS+=(--ephemeral)
43
+ SANDBOX=read-only
25
44
  elif [[ "$MODE" == "sysops" ]]; then
26
- ARGS+=(-s danger-full-access)
45
+ SANDBOX=danger-full-access
46
+ else
47
+ SANDBOX=workspace-write
48
+ fi
49
+ # `codex exec resume` has no -s/--sandbox flag (rejects it with exit 2); the
50
+ # same policy is only reachable there through the sandbox_mode config override.
51
+ if [[ "$THREAD_MODE" == "resume" ]]; then
52
+ ARGS+=(-c "sandbox_mode=\"$SANDBOX\"")
27
53
  else
28
- ARGS+=(-s workspace-write)
54
+ ARGS+=(-s "$SANDBOX")
29
55
  fi
56
+ [[ "$THREAD_MODE" == "resume" ]] && ARGS+=("$THREAD_ID" -)
30
57
 
31
58
  truncate_payload "$PROMPT_FILE" 140000
32
59
 
@@ -14,6 +14,21 @@ AGY_BIN="${AGY_BIN:-agy}"
14
14
  RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
15
15
  CAPACITY_PATTERN='MODEL_CAPACITY_EXHAUSTED|No capacity available for model|rateLimitExceeded|RESOURCE_EXHAUSTED'
16
16
 
17
+ THREAD_MODE="${OMNILANE_THREAD_MODE:-}"
18
+ THREAD_ID="${OMNILANE_THREAD_ID:-}"
19
+ THREAD_ARGS=()
20
+ if [[ -n "$THREAD_MODE" || -n "$THREAD_ID" ]]; then
21
+ [[ "$THREAD_ID" =~ ^[A-Za-z0-9._:-]+$ && "${#THREAD_ID}" -le 256 ]] || {
22
+ echo "omnilane: invalid Gemini thread session id" >&2
23
+ exit 2
24
+ }
25
+ case "$THREAD_MODE" in
26
+ new) ;;
27
+ resume) THREAD_ARGS=(--conversation "$THREAD_ID") ;;
28
+ *) echo "omnilane: invalid Gemini thread mode" >&2; exit 2 ;;
29
+ esac
30
+ fi
31
+
17
32
  truncate_payload "$PROMPT_FILE" 140000
18
33
 
19
34
  # Both modes run inside the target WORKDIR so the worker can actually see the
@@ -142,6 +157,38 @@ PY
142
157
  exit "$RC"
143
158
  fi
144
159
 
160
+ if [[ -n "$THREAD_MODE" ]]; then
161
+ set +e
162
+ (
163
+ cd "$RUN_DIR" || exit 127
164
+ env -u GEMINI_API_KEY -u GOOGLE_API_KEY -u GOOGLE_AI_API_KEY \
165
+ NO_BROWSER=1 OMNILANE_DEPTH=1 \
166
+ "$AGY_BIN" --dangerously-skip-permissions --add-dir "$RUN_DIR" \
167
+ "${MODE_ARGS[@]}" ${MODEL_ARGS[@]+"${MODEL_ARGS[@]}"} \
168
+ --print-timeout "${RUN_TIMEOUT}s" --output-format json \
169
+ "${THREAD_ARGS[@]}" -p "$(cat "$PROMPT_FILE")" \
170
+ > "${OUTPUT_FILE}.result.json" 2> "${OUTPUT_FILE}.stderr.log"
171
+ )
172
+ RC=$?
173
+ set -e
174
+ if [[ "$RC" -eq 0 ]]; then
175
+ if ! python3 - "${OUTPUT_FILE}.result.json" "${OUTPUT_FILE}.tmp" <<'PY'
176
+ import json
177
+ import pathlib
178
+ import sys
179
+
180
+ result = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
181
+ response = result.get("response")
182
+ if result.get("status") != "SUCCESS" or not isinstance(response, str):
183
+ raise SystemExit(1)
184
+ pathlib.Path(sys.argv[2]).write_text(response.rstrip("\n") + "\n", encoding="utf-8")
185
+ PY
186
+ then
187
+ echo "omnilane: Gemini thread result was not readable SUCCESS JSON" >> "${OUTPUT_FILE}.stderr.log"
188
+ RC=1
189
+ fi
190
+ fi
191
+ else
145
192
  set +e
146
193
  (
147
194
  cd "$RUN_DIR" || exit 127
@@ -158,8 +205,9 @@ set +e
158
205
  )
159
206
  RC=$?
160
207
  set -e
208
+ fi
161
209
 
162
- if grep -Eiq "$CAPACITY_PATTERN" "${OUTPUT_FILE}.tmp" "${OUTPUT_FILE}.stderr.log" 2>/dev/null; then
210
+ if grep -Eiq "$CAPACITY_PATTERN" "${OUTPUT_FILE}.tmp" "${OUTPUT_FILE}.result.json" "${OUTPUT_FILE}.stderr.log" 2>/dev/null; then
163
211
  echo "omnilane: gemini capacity exhausted" >> "${OUTPUT_FILE}.stderr.log"
164
212
  RC=126
165
213
  fi
@@ -17,14 +17,30 @@ MAX_ATTEMPTS="${OMNILANE_GROK_MAX_ATTEMPTS:-5}"
17
17
  exit 2
18
18
  }
19
19
 
20
+ THREAD_MODE="${OMNILANE_THREAD_MODE:-}"
21
+ THREAD_ID="${OMNILANE_THREAD_ID:-}"
22
+ THREAD_ARGS=()
23
+ if [[ -n "$THREAD_MODE" || -n "$THREAD_ID" ]]; then
24
+ [[ "$THREAD_ID" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]] || {
25
+ echo "omnilane: invalid Grok thread session id" >&2
26
+ exit 2
27
+ }
28
+ case "$THREAD_MODE" in
29
+ new) THREAD_ARGS=(--session-id "$THREAD_ID") ;;
30
+ resume) THREAD_ARGS=(--resume "$THREAD_ID") ;;
31
+ *) echo "omnilane: invalid Grok thread mode" >&2; exit 2 ;;
32
+ esac
33
+ fi
34
+
20
35
  # Subscription OAuth path: an exhausted API key in env causes 403s.
21
36
  unset XAI_API_KEY 2>/dev/null || true
22
37
 
23
38
  truncate_payload "$PROMPT_FILE" 140000
24
39
 
25
40
  ARGS=(--cwd "$WORKDIR" --model "$MODEL"
26
- --no-memory --no-subagents --no-plan --no-alt-screen
27
- --output-format plain --verbatim --prompt-file "$PROMPT_FILE")
41
+ --no-memory --no-subagents --no-plan --no-alt-screen
42
+ --output-format plain --verbatim --prompt-file "$PROMPT_FILE")
43
+ ARGS+=("${THREAD_ARGS[@]}")
28
44
  [[ "$MODE" == "advise" ]] && ARGS+=(--permission-mode plan)
29
45
  # Web/X search stays ON by default — it is this vendor's signature lane.
30
46
  [[ "${OMNILANE_GROK_NO_WEB:-0}" == "1" ]] && ARGS+=(--disable-web-search)
@@ -1,6 +1,6 @@
1
1
  ---
2
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; implementation work dispatches by default via dispatch.sh — the main loop self-executes only reserved commander items.'
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; every task dispatches by default via dispatch.sh — the main loop self-executes only reserved commander items.'
4
4
  ---
5
5
 
6
6
  # omnilane — one routing table, every harness
@@ -9,13 +9,31 @@ You (the main loop) may be Claude, GPT, Grok, or Gemini. The procedure is identi
9
9
 
10
10
  1. **Identify your main model.** You know which model you are running as.
11
11
  2. **Split the work into subtasks and classify each into a lane** (table below).
12
- 3. **Dispatch implementation work by default — even when the lane's model is
13
- you.** Self-execute only reserved commander items: planning and
14
- decomposition, writing task briefs, reviewing reports, acceptance checks,
15
- replies to the operator, git commit/push, read-only verification, and
16
- fixes of one line or less. Dispatch:
12
+ 3. **Dispatch every task by default — even when the lane's model is you:**
13
+ implementation, search, investigation, file reads, verification, tests,
14
+ builds, deploys. The commander self-executes only: planning and
15
+ decomposition, writing task briefs, reading worker reports and job files
16
+ (`out.txt`, `events.jsonl`, inbox records), acceptance judgment, replies to
17
+ the operator, git commit/push, and edits to governance files. Read-only
18
+ work goes out in advise mode: `triage` for high-volume scans, `long-context`
19
+ for large documents, `live-search` for web or X, `hard-judgment` for second
20
+ opinions. Editing work uses `--mode work --workdir <repo> --timeout 3600`
21
+ or more. Re-verify a worker's claim by reading its attached evidence or by
22
+ dispatching a second worker (change `--vendor`); the commander runs no
23
+ commands itself. Invalid reasons to skip dispatch: "this lane is mine",
24
+ "I am not dispatching so the rule does not apply", "it is only a file
25
+ read", "dispatch is slower", "it is one line". Dispatch:
17
26
  `<repo>/scripts/dispatch.sh [--vendor V] [--mode work] [--workdir DIR] <lane> "<task>"`
18
27
  Add `--background` for long tasks; poll with `scripts/jobs.sh status|result <id>`.
28
+ Use `--thread NAME` when later claude, codex, grok or gemini dispatches
29
+ must retain earlier context. Threads in 0.33.0 pin vendor, model, effort and
30
+ physical workdir; inspect or remove local state with `scripts/jobs.sh threads`,
31
+ `threads show NAME`, and `threads rm NAME` (removal leaves the vendor session).
32
+ Implementation dispatches (code edits, new files, tests, builds, deploys)
33
+ must carry `--mode work --workdir <repo>` and a `--timeout` of at least
34
+ 3600 seconds. The advise default is a read-only worker under a 600 s
35
+ per-call watchdog, and on an implementation task it yields zero output.
36
+ Advise stays the default for reviews, questions, and second opinions.
19
37
  Before changing lane order from anecdotal outcomes, run
20
38
  `scripts/jobs.sh recommend [--last N] [--lane L] [--min-samples N]` and report
21
39
  its evidence threshold. The command is read-only and never changes routing.
@@ -50,26 +68,26 @@ what dispatch picks when the first-choice vendor CLI is not installed.
50
68
 
51
69
  | Lane | First choice | Backup | When |
52
70
  |---|---|---|---|
53
- | hardest-coding | Claude Fable 5.1 (xhigh) | GPT-5.6 Sol (xhigh) | Hardest implementation, deep root-cause debug, correctness-critical edits |
71
+ | hardest-coding | Claude Fable 5.1 (xhigh) | GPT-5.6 Sol (xhigh) → Grok 4.6 → Gemini 3.7 Flash (High) | Hardest implementation, deep root-cause debug, correctness-critical edits |
54
72
  | bulk-mechanical | GPT-5.6 Sol (high) | Gemini 3.7 Flash (High) → Claude Sonnet 5 (high) | Refactors, migrations, tests, review sweeps — mechanical endurance |
55
73
  | triage | GPT-5.6 Luna (high) | Gemini 3.7 Flash (Low) → Claude Haiku 4.5 | High-volume scans, first-pass filtering |
56
- | hard-judgment | Claude Fable 5.1 (xhigh) | GPT-5.6 Sol (max) → Grok 4.6 | Architecture arbitration, deep reasoning, second opinions |
57
- | taste-final | Claude Fable 5.1 (high) | GPT-5.6 Sol (max) | User-facing prose, prompt/doc polish, Chinese phrasing, style arbitration |
74
+ | hard-judgment | Claude Opus 5 (xhigh) | GPT-5.6 Sol (max) → Grok 4.6 | Architecture arbitration, deep reasoning, second opinions |
75
+ | taste-final | Claude Fable 5.1 (high) | GPT-5.6 Sol (max) → Grok 4.6 → Gemini 3.7 Flash (High) | User-facing prose, prompt/doc polish, Chinese phrasing, style arbitration |
58
76
  | consult | GPT-5.6 Sol (max) | Claude Fable 5.1 (high) → Grok 4.6 → Gemini 3.7 Flash (High) | Direct named-model consultation; always keep `--vendor` |
59
- | ui-draft | GPT-5.6 Sol (xhigh) | Claude Fable 5.1 (high) | UI drafts only WITH a design system / reference images; open-ended visual taste goes to taste-final |
77
+ | ui-draft | GPT-5.6 Sol (xhigh) | Claude Fable 5.1 (high) → Gemini 3.7 Flash (High) | UI drafts only WITH a design system / reference images; open-ended visual taste goes to taste-final |
60
78
  | long-context | Gemini 3.7 Flash (Medium) | GPT-5.6 Terra (max) → Claude Opus 5 (medium) | Long-context synthesis ordered on AA-LCR, then cost and throughput |
61
- | fast-agentic | Gemini 3.7 Flash (Medium) | GPT-5.6 Luna (high) | Fast multi-step agentic loops, multimodal checks |
62
- | live-search | Grok 4.6 | (off) | Realtime X/web search and social context |
79
+ | fast-agentic | Gemini 3.7 Flash (Medium) | GPT-5.6 Luna (high) → Claude Haiku 4.5 | Fast multi-step agentic loops, multimodal checks |
80
+ | live-search | Grok 4.6 | Gemini 3.7 Flash (High) → Claude Sonnet 5 (high) → off | Realtime X/web search and social context; Flash/Sonnet fall back to their own web-search tools |
63
81
  | coding-overflow | Grok 4.6 | Gemini 3.7 Flash (High) → Kimi K3 → Qwen3 Coder Plus → OpenCode | Codex-quota relief valve for mid-tier coding; verify factual claims |
64
82
  | 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) |
65
83
 
66
- Claude Fable 5.1 (`claude-fable-5-1`) is in the judgment, taste, and
67
- hardest-coding defaults because it leads Opus 5 on every Artificial Analysis
68
- axis at the same effort. It is not in bulk or triage because it prices at twice
69
- Opus 5 per token and consumes the most subscription quota per turn. Opus 5
70
- remains the lower-hallucination, lower-price Claude choice and can return to any
71
- lane via `~/.omnilane/routing.local.yaml`, for example:
72
- `hard-judgment: claude claude-opus-5 xhigh`.
84
+ Claude Fable 5.1 (`claude-fable-5-1`) is in the taste and hardest-coding
85
+ defaults because it leads Opus 5 on every Artificial Analysis axis at the same
86
+ effort. It is not in bulk or triage because it prices at twice Opus 5 per token
87
+ and consumes the most subscription quota per turn. Opus 5 now leads
88
+ hard-judgment on a per-cost, lower-hallucination basis and remains selectable
89
+ everywhere via `~/.omnilane/routing.local.yaml`, for example, to bring Fable
90
+ back: `hard-judgment: claude claude-fable-5-1 xhigh`.
73
91
 
74
92
  ## Natural-language consultation
75
93
 
@@ -132,6 +150,24 @@ delete jobs, or edit configuration. Natural-language interpretation and
132
150
  dispatch stay in this skill and the CLI. Manage the local board with
133
151
  `omnilane ui start|status|url|stop`, and stop it when monitoring is finished.
134
152
 
153
+ ## Job lifecycle defaults
154
+
155
+ - **Completion inbox**: with the Claude Code plugin's hooks installed, a
156
+ finished `--background` job is delivered into the foreman's next prompt by
157
+ the bundled `UserPromptSubmit` hook, so do not poll for it. Outside Claude
158
+ Code, block on `scripts/jobs.sh wait <id> [--timeout N]` instead.
159
+ - **Live mailbox**: a `--background` dispatch to Claude or Gemini is a
160
+ resident worker. Send follow-up instructions with `scripts/jobs.sh send <id>
161
+ "<text>"` and end it with `scripts/jobs.sh close <id>`; other vendors (and
162
+ `--single-shot`) run one-shot. Do not use a mailbox for fire-and-forget work.
163
+ - **Goal orchestration**: when the next step depends on the previous result,
164
+ wrap the dispatches in `omnilane goal open "<objective>" --workdir DIR`, then
165
+ `goal dispatch <goal-id> ...`, `goal note`, `goal status`, `goal close --summary`.
166
+ Budgets are unlimited unless `--budget-jobs` / `--budget-seconds` is passed.
167
+ A single obvious task is dispatched directly, never through a goal.
168
+ - **Job hygiene**: `scripts/jobs.sh cancel <id>` stops a runaway job.
169
+ `stats`, `recommend`, and `audit` are read-only and never change routing.
170
+
135
171
  ## Rules
136
172
 
137
173
  - **Dispatch in `advise` mode by default** (read-only worker). Use `--mode work`
@@ -157,24 +193,29 @@ dispatch stay in this skill and the CLI. Manage the local board with
157
193
 
158
194
  ## Per-model notes (apply the row matching YOUR main model)
159
195
 
160
- - **Claude Fable 5.1 main**: hard judgment, taste finalization, and the hardest
161
- coding are yours. Dispatch bulk work to Sol high and long-context or fast
162
- loops to Gemini 3.7 Flash.
163
- - **Claude Opus 5 main**: judgment and taste remain strong self-execute lanes;
164
- use local overrides when its lower hallucination rate or price is preferred.
165
- - **Claude Sonnet main**: coordination/tools/mid-tier coding only; never
166
- self-assign top judgment or hardest implementation.
196
+ - **Claude Fable 5.1 main**: taste finalization and the hardest coding are
197
+ yours; hard judgment now defaults to Opus 5. Dispatch bulk work to Sol high,
198
+ hard judgment to Opus 5 xhigh, and long-context or fast loops to Gemini 3.7
199
+ Flash.
200
+ - **Claude Opus 5 main**: hard judgment is now yours by default. Taste-final
201
+ remains Fable's; use local overrides when Opus's lower hallucination rate or
202
+ price is preferred there.
203
+ - **Claude Sonnet main**: coordination/tools/mid-tier coding only, plus
204
+ fallback duty in bulk-mechanical and live-search; never self-assign top
205
+ judgment or hardest implementation.
167
206
  - **GPT Sol main**: hardest coding + hard judgment are yours (use max for
168
207
  judgment turns, xhigh for coding); cross to taste-final for style calls.
169
208
  - **GPT Terra main**: long-context Codex fallback work is yours at max;
170
209
  bulk-mechanical now defaults to Sol high, and genuinely hardest pieces
171
210
  escalate to Sol xhigh.
172
- - **Grok 4.6 main**: live-search and coding overflow are yours; its measured
211
+ - **Grok 4.6 main**: live-search and coding overflow are yours, plus fallback
212
+ duty in hardest-coding, hard-judgment, and taste-final; its measured
173
213
  hallucination rate is the lowest among the frontier rows, but still verify
174
214
  every API signature and cited fact before shipping.
175
215
  - **Gemini 3.7 Flash main**: long-context and fast agentic/multimodal loops
176
- are yours at the lane's configured effort; bulk and overflow use the high row.
177
- Never self-assign top judgment.
216
+ are yours at the lane's configured effort; bulk and overflow use the high
217
+ row, plus fallback duty (High) in hardest-coding, taste-final, ui-draft, and
218
+ live-search. Never self-assign top judgment.
178
219
  - **Gemini 3.1 Pro main**: it remains directly selectable, but the default
179
220
  long-context lane now prefers Gemini 3.7 Flash on LCR, cost, and throughput;
180
221
  route hardest coding and judgment to the stronger Codex and Claude lanes.