omnilane 0.32.0 → 0.33.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.
@@ -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`
@@ -160,7 +196,7 @@ dispatch stay in this skill and the CLI. Manage the local board with
160
196
  - **Claude Fable 5.1 main**: hard judgment, taste finalization, and the hardest
161
197
  coding are yours. Dispatch bulk work to Sol high and long-context or fast
162
198
  loops to Gemini 3.7 Flash.
163
- - **Claude Opus 5 main**: judgment and taste remain strong self-execute lanes;
199
+ - **Claude Opus 5 main**: judgment and taste remain its strongest lanes, but the commander still dispatches them;
164
200
  use local overrides when its lower hallucination rate or price is preferred.
165
201
  - **Claude Sonnet main**: coordination/tools/mid-tier coding only; never
166
202
  self-assign top judgment or hardest implementation.