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/plugin.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
+ "name": "omnilane",
4
+ "version": "0.20.0",
5
+ "description": "One routing table, every harness: classify subtasks into lanes and auto-dispatch each lane to the best vendor CLI (Codex, Claude Code, Grok Build, Antigravity) with background jobs, depth guard, and serialized codex dispatch."
6
+ }
@@ -637,6 +637,10 @@ chmod 700 "$JOBS_ROOT"
637
637
  JOB_ID="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
638
638
  JOB_DIR="$JOBS_ROOT/$JOB_ID"
639
639
  mkdir -m 700 "$JOB_DIR"
640
+ FOREMAN_SESSION=""
641
+ if resolved_foreman_session="$(find_foreman_session "$$" 2>/dev/null)"; then
642
+ FOREMAN_SESSION="$resolved_foreman_session"
643
+ fi
640
644
 
641
645
  if [[ "$TASK" == "-" ]]; then
642
646
  (umask 077; cat > "$JOB_DIR/task.txt")
@@ -645,10 +649,11 @@ else
645
649
  fi
646
650
 
647
651
  # meta "timeout" is the resolved per-CLI-call watchdog cap, not a whole-job total.
648
- (umask 077; printf '{"lane":"%s","vendor":"%s","model":"%s","effort":"%s","timeout":%s,"job_timeout":%s,"mode":"%s","workdir":"%s","candidate":"%s/%s","started":"%s"}\n' \
652
+ (umask 077; printf '{"lane":"%s","vendor":"%s","model":"%s","effort":"%s","timeout":%s,"job_timeout":%s,"mode":"%s","workdir":"%s","foreman_session":"%s","candidate":"%s/%s","started":"%s"}\n' \
649
653
  "$(json_escape "$LANE")" "$(json_escape "$VENDOR")" "$(json_escape "$MODEL")" \
650
654
  "$(json_escape "$EFFORT")" "$TIMEOUT" "$JOB_TIMEOUT_JSON" "$(json_escape "$MODE")" \
651
- "$(json_escape "$WORKDIR")" "$RESOLVED_IDX" "$RESOLVED_TOTAL" \
655
+ "$(json_escape "$WORKDIR")" "$(json_escape "$FOREMAN_SESSION")" \
656
+ "$RESOLVED_IDX" "$RESOLVED_TOTAL" \
652
657
  "$(date -u +%FT%TZ)" > "$JOB_DIR/meta.json")
653
658
 
654
659
  secure_job_files() {
@@ -685,9 +690,10 @@ write_completion_record() {
685
690
  finished="$(date -u +%FT%TZ)" || return 1
686
691
  old_umask="$(umask)"
687
692
  umask 077
688
- printf '{"job_id":"%s","lane":"%s","vendor":"%s","model":"%s","mode":"%s","workdir":"%s","exit":%s,"finished":"%s","tail":"%s"}\n' \
693
+ printf '{"job_id":"%s","lane":"%s","vendor":"%s","model":"%s","mode":"%s","workdir":"%s","foreman_session":"%s","exit":%s,"finished":"%s","tail":"%s"}\n' \
689
694
  "$(json_escape "$JOB_ID")" "$(json_escape "$LANE")" "$(json_escape "$VENDOR")" \
690
695
  "$(json_escape "$MODEL")" "$(json_escape "$MODE")" "$(json_escape "$WORKDIR")" \
696
+ "$(json_escape "$FOREMAN_SESSION")" \
691
697
  "$rc" "$(json_escape "$finished")" "$(json_escape "$tail_value")" > "$tmp" || write_rc=$?
692
698
  if [[ "$write_rc" -eq 0 ]]; then
693
699
  chmod 600 "$tmp" || write_rc=$?
@@ -696,7 +702,8 @@ write_completion_record() {
696
702
  mv "$tmp" "$final" || write_rc=$?
697
703
  fi
698
704
  umask "$old_umask"
699
- rm -f "$tmp" 2>/dev/null || true
705
+ # no -f: force-flag rm is blocked by some environments' destructive guards
706
+ rm "$tmp" 2>/dev/null || true
700
707
  return "$write_rc"
701
708
  }
702
709
 
package/scripts/doctor.sh CHANGED
@@ -123,6 +123,132 @@ else
123
123
  report PASS state "$OMNILANE_HOME is accessible"
124
124
  fi
125
125
 
126
+ completion_plugin_state() {
127
+ local config_dir state
128
+ config_dir="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
129
+ state="$(perl -MJSON::PP -e '
130
+ use strict;
131
+ use warnings;
132
+ my ($repo, @paths) = @ARGV;
133
+ my ($seen, $enabled, $source) = (0, undef, undef);
134
+ for my $path (@paths) {
135
+ next unless -e $path;
136
+ $seen = 1;
137
+ open my $fh, "<", $path or do { print "unknown"; exit 0 };
138
+ local $/;
139
+ my $settings = eval { decode_json(<$fh>) };
140
+ close $fh;
141
+ if ($@ || ref($settings) ne "HASH") { print "unknown"; exit 0 }
142
+ my $plugins = $settings->{enabledPlugins};
143
+ if (ref($plugins) eq "HASH" && exists $plugins->{"omnilane\@omnilane"}) {
144
+ $enabled = $plugins->{"omnilane\@omnilane"};
145
+ }
146
+ my $markets = $settings->{extraKnownMarketplaces};
147
+ if (ref($markets) eq "HASH" && exists $markets->{omnilane}) {
148
+ my $market = $markets->{omnilane};
149
+ $source = ref($market) eq "HASH" ? $market->{source} : undef;
150
+ }
151
+ }
152
+ if (!$seen) { print "unknown"; exit 0 }
153
+ my $enabled_ok = ref($enabled) &&
154
+ eval { $enabled->isa("JSON::PP::Boolean") } && $enabled;
155
+ my $source_ok = ref($source) eq "HASH" &&
156
+ defined($source->{source}) && !ref($source->{source}) &&
157
+ defined($source->{path}) && !ref($source->{path}) &&
158
+ $source->{source} eq "directory" && $source->{path} eq $repo;
159
+ print $enabled_ok && $source_ok ? "active" : "inactive";
160
+ ' "$REPO" "$config_dir/settings.json" "$config_dir/settings.local.json" 2>/dev/null)" || state=unknown
161
+ case "$state" in
162
+ active|inactive|unknown) printf '%s\n' "$state" ;;
163
+ *) printf '%s\n' unknown ;;
164
+ esac
165
+ }
166
+
167
+ completion_manifest_registers_hook() {
168
+ local manifest="$1"
169
+ [[ -r "$manifest" ]] || return 1
170
+ awk '
171
+ { content = content $0 }
172
+ END {
173
+ if (content ~ /"UserPromptSubmit"[[:space:]]*:[[:space:]]*\[/ &&
174
+ content ~ /"UserPromptSubmit".*report-completions\.sh/) exit 0
175
+ exit 1
176
+ }
177
+ ' "$manifest"
178
+ }
179
+
180
+ completion_plugin="$(completion_plugin_state)"
181
+ completion_settings_dir="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"
182
+ completion_script="$REPO/hooks/report-completions.sh"
183
+ completion_manifest="$REPO/hooks/hooks.json"
184
+ completion_inbox="$OMNILANE_HOME/inbox"
185
+ completion_plugin_ok=0
186
+ completion_script_ok=0
187
+ completion_manifest_ok=0
188
+ completion_inbox_ok=1
189
+ completion_pending=0
190
+ completion_reason=""
191
+
192
+ case "$completion_plugin" in
193
+ active)
194
+ completion_plugin_ok=1
195
+ report PASS completion-plugin "omnilane@omnilane is enabled according to Claude Code settings"
196
+ ;;
197
+ inactive)
198
+ completion_reason="omnilane@omnilane is missing or disabled"
199
+ report WARN completion-plugin "$completion_reason according to Claude Code settings"
200
+ ;;
201
+ *)
202
+ completion_reason="Claude Code plugin state is unknown"
203
+ report WARN completion-plugin "$completion_reason; settings files were absent, unreadable, or invalid"
204
+ ;;
205
+ esac
206
+
207
+ if [[ -x "$completion_script" ]]; then
208
+ completion_script_ok=1
209
+ report PASS completion-hook "$completion_script exists and is executable"
210
+ elif [[ -e "$completion_script" ]]; then
211
+ completion_reason="${completion_reason:+$completion_reason; }$completion_script is not executable"
212
+ report WARN completion-hook "$completion_script exists but is not executable"
213
+ else
214
+ completion_reason="${completion_reason:+$completion_reason; }$completion_script is missing"
215
+ report WARN completion-hook "$completion_script is missing"
216
+ fi
217
+
218
+ if completion_manifest_registers_hook "$completion_manifest"; then
219
+ completion_manifest_ok=1
220
+ report PASS completion-manifest "$completion_manifest from the current checkout registers UserPromptSubmit"
221
+ else
222
+ completion_reason="${completion_reason:+$completion_reason; }$completion_manifest does not register UserPromptSubmit"
223
+ report WARN completion-manifest "$completion_manifest from the current checkout does not register UserPromptSubmit for report-completions.sh"
224
+ fi
225
+
226
+ if [[ -L "$completion_inbox" || ( -e "$completion_inbox" && ! -d "$completion_inbox" ) ]]; then
227
+ completion_inbox_ok=0
228
+ completion_reason="${completion_reason:+$completion_reason; }$completion_inbox is not a real directory"
229
+ report WARN completion-inbox "$completion_inbox must be a real directory"
230
+ elif [[ -d "$completion_inbox" ]]; then
231
+ for completion_record in "$completion_inbox"/*.json; do
232
+ [[ -f "$completion_record" && ! -L "$completion_record" ]] || continue
233
+ completion_pending=$((completion_pending + 1))
234
+ done
235
+ report PASS completion-inbox "$completion_pending pending records in $completion_inbox"
236
+ else
237
+ report PASS completion-inbox "0 pending records; $completion_inbox does not exist yet"
238
+ fi
239
+
240
+ if [[ "$completion_plugin_ok" -eq 1 && "$completion_script_ok" -eq 1 &&
241
+ "$completion_manifest_ok" -eq 1 && "$completion_inbox_ok" -eq 1 ]]; then
242
+ report PASS completion-notice "active; $completion_pending pending records; manifest read from $completion_manifest"
243
+ else
244
+ if [[ "$completion_plugin" == inactive ]]; then
245
+ completion_reason="$completion_reason; run: claude plugin marketplace add \"$REPO\"; claude plugin install omnilane@omnilane"
246
+ elif [[ "$completion_plugin" == unknown ]]; then
247
+ completion_reason="$completion_reason; inspect $completion_settings_dir/settings.json and settings.local.json"
248
+ fi
249
+ report WARN completion-notice "inactive: $completion_reason; manifest read from $completion_manifest"
250
+ fi
251
+
126
252
  if [[ -f "$OMNILANE_HOME/local.sh" ]]; then
127
253
  if /bin/bash -n "$OMNILANE_HOME/local.sh" 2>/dev/null; then
128
254
  report PASS local-config "$OMNILANE_HOME/local.sh syntax is valid"
package/scripts/jobs.sh CHANGED
@@ -4,14 +4,19 @@ set -euo pipefail
4
4
  # Usage: jobs.sh [--json] list | status JOB_ID | result JOB_ID | stats [--last N]
5
5
  # jobs.sh [--json] recommend [--last N] [--lane L] [--min-samples N]
6
6
  # jobs.sh wait JOB_ID [--timeout N]
7
+ # jobs.sh send JOB_ID TEXT | watch JOB_ID | close JOB_ID
7
8
  # jobs.sh audit [--last N] [--json] | prune [--keep N] [--apply]
8
9
 
10
+ # Runtime-relative shared library.
11
+ # shellcheck disable=SC1091
9
12
  source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
10
13
  JOBS="$OMNILANE_HOME/jobs"
11
14
  JOB_ID_PATTERN='^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$'
12
15
  JSON_MODE=0
13
16
  COMMAND="unknown"
14
17
 
18
+ # The backslash case pattern is intentional.
19
+ # shellcheck disable=SC1003
15
20
  json_escape() {
16
21
  local s="$1" out="" ch escaped code i
17
22
  for ((i = 0; i < ${#s}; i++)); do
@@ -49,7 +54,7 @@ die() {
49
54
  exit "$rc"
50
55
  }
51
56
 
52
- USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done]|status ID|result ID|tail ID [--lines N]|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|recommend [--last N] [--lane L] [--min-samples N]|wait ID [--timeout N]|cancel ID|rm ID|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
57
+ USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done]|status ID|result ID|tail ID [--lines N]|send ID TEXT|watch ID|close ID|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|recommend [--last N] [--lane L] [--min-samples N]|wait ID [--timeout N]|cancel ID|rm ID|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
53
58
 
54
59
  usage() {
55
60
  die 2 "$USAGE_TEXT"
@@ -108,6 +113,14 @@ read_public_metadata() {
108
113
  PUBLIC_METADATA="$value"
109
114
  }
110
115
 
116
+ print_mode_notice() {
117
+ local path="$JOB_DIR/mode-notice.txt"
118
+ [[ -e "$path" || -L "$path" ]] || return 0
119
+ if read_public_metadata "$path"; then
120
+ printf '%s\n' "$PUBLIC_METADATA"
121
+ fi
122
+ }
123
+
111
124
  valid_utf8() {
112
125
  command -v iconv >/dev/null 2>&1 || return 1
113
126
  printf '%s' "$1" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1
@@ -131,6 +144,55 @@ read_job_pid() {
131
144
  RECORDED_PID="$value"
132
145
  }
133
146
 
147
+ load_job_vendor() {
148
+ if ! read_public_metadata "$JOB_DIR/meta.json" || ! parse_stats_metadata "$PUBLIC_METADATA"; then
149
+ die 1 "job metadata is not safely readable"
150
+ fi
151
+ JOB_VENDOR="$STATS_VENDOR"
152
+ }
153
+
154
+ require_live_job() {
155
+ load_job_vendor
156
+ [[ "$JOB_VENDOR" == "claude" ]] || die 1 "job vendor '$JOB_VENDOR' is not live-capable; it runs in single-shot mode"
157
+ }
158
+
159
+ wait_for_live_ready() {
160
+ local deadline=$((SECONDS + 5))
161
+ while [[ ! -f "$JOB_DIR/inbox.ready" ]]; do
162
+ if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
163
+ die 1 "job is no longer accepting live messages"
164
+ fi
165
+ if [[ "$SECONDS" -ge "$deadline" ]]; then
166
+ die 124 "live inbox did not become ready within 5s"
167
+ fi
168
+ sleep 0.1
169
+ done
170
+ [[ ! -L "$JOB_DIR/inbox.ready" ]] || die 1 "unsafe live inbox readiness path"
171
+ [[ -p "$JOB_DIR/inbox.fifo" && ! -L "$JOB_DIR/inbox.fifo" ]] || die 1 "live inbox FIFO is unavailable"
172
+ }
173
+
174
+ write_fifo_with_timeout() {
175
+ local fifo="$1" payload="$2" writer_pid deadline rc
176
+ (
177
+ printf '%s\n' "$payload" > "$fifo"
178
+ ) &
179
+ writer_pid=$!
180
+ deadline=$((SECONDS + 5))
181
+ while kill -0 "$writer_pid" 2>/dev/null; do
182
+ if [[ "$SECONDS" -ge "$deadline" ]]; then
183
+ kill -TERM "$writer_pid" 2>/dev/null || true
184
+ wait "$writer_pid" 2>/dev/null || true
185
+ return 124
186
+ fi
187
+ sleep 0.1
188
+ done
189
+ set +e
190
+ wait "$writer_pid"
191
+ rc=$?
192
+ set -e
193
+ return "$rc"
194
+ }
195
+
134
196
  parse_stats_metadata() {
135
197
  local value="$1"
136
198
  local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"('"$OMNILANE_VENDOR_ALT"')"(,|})'
@@ -280,6 +342,76 @@ case "${1:-}" in
280
342
  help|--help|-h)
281
343
  [[ "$JSON_MODE" -eq 0 && $# -eq 1 ]] || usage
282
344
  echo "$USAGE_TEXT" ;;
345
+ send)
346
+ [[ "$JSON_MODE" -eq 0 && $# -eq 3 ]] || usage
347
+ select_job "$2"
348
+ require_live_job
349
+ wait_for_live_ready
350
+ if [[ "${FOREMAN_SESSION+x}" == "x" ]]; then
351
+ foreman_value="$FOREMAN_SESSION"
352
+ else
353
+ foreman_value="${foreman_session-}"
354
+ fi
355
+ payload="$(printf '{"type":"user","omnilane_job_id":"%s","foreman_session":"%s","message":{"role":"user","content":[{"type":"text","text":"%s"}]}}' \
356
+ "$(json_escape "$2")" "$(json_escape "$foreman_value")" "$(json_escape "$3")")"
357
+ set +e
358
+ write_fifo_with_timeout "$JOB_DIR/inbox.fifo" "$payload"
359
+ send_rc=$?
360
+ set -e
361
+ [[ "$send_rc" -eq 0 ]] || die "$send_rc" "send timed out or failed; the live worker may be gone"
362
+ echo "sent $2"
363
+ ;;
364
+ watch)
365
+ [[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
366
+ select_job "$2"
367
+ require_live_job
368
+ events_path="$JOB_DIR/events.jsonl"
369
+ watch_deadline=$((SECONDS + 5))
370
+ while [[ ! -f "$events_path" ]]; do
371
+ if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
372
+ die 1 "job has no live event stream"
373
+ fi
374
+ [[ "$SECONDS" -lt "$watch_deadline" ]] || die 124 "live event stream did not appear within 5s"
375
+ sleep 0.1
376
+ done
377
+ [[ ! -L "$events_path" ]] || die 1 "unsafe live event path"
378
+ if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
379
+ cat "$events_path"
380
+ else
381
+ tail -n +1 -f -- "$events_path"
382
+ fi
383
+ ;;
384
+ close)
385
+ [[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
386
+ select_job "$2"
387
+ require_live_job
388
+ if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
389
+ read_exit_code "$JOB_DIR/exit" || die 1 "invalid recorded exit status"
390
+ echo "already closed (exit $RECORDED_EXIT)"
391
+ exit "$RECORDED_EXIT"
392
+ fi
393
+ holder_path="$JOB_DIR/inbox.holder.pid"
394
+ read_job_pid "$holder_path" || die 1 "live inbox holder is unavailable"
395
+ holder_pid="$RECORDED_PID"
396
+ kill -0 "$holder_pid" 2>/dev/null || die 1 "live inbox holder is gone"
397
+ kill -USR1 "$holder_pid" 2>/dev/null || die 1 "could not signal live inbox holder"
398
+ close_deadline=$((SECONDS + 10))
399
+ while [[ ! -e "$JOB_DIR/exit" && ! -L "$JOB_DIR/exit" ]]; do
400
+ if [[ "$SECONDS" -ge "$close_deadline" ]]; then
401
+ die 124 "close signal sent, but the live worker did not terminate within 10s"
402
+ fi
403
+ sleep 0.1
404
+ done
405
+ read_exit_code "$JOB_DIR/exit" || die 1 "invalid recorded exit status"
406
+ events_path="$JOB_DIR/events.jsonl"
407
+ result_count=0
408
+ if [[ -f "$events_path" && ! -L "$events_path" ]]; then
409
+ result_count="$(grep -Ec '"type"[[:space:]]*:[[:space:]]*"result"' "$events_path" || true)"
410
+ [[ "$result_count" =~ ^[0-9]+$ ]] || result_count=0
411
+ fi
412
+ echo "closed $2 (result_events=$result_count exit=$RECORDED_EXIT)"
413
+ exit "$RECORDED_EXIT"
414
+ ;;
283
415
  tail)
284
416
  # Peek at the live public output stream of one job (running or done)
285
417
  # without touching its exit contract. Only out.txt is shown; stderr and
@@ -427,13 +559,19 @@ case "${1:-}" in
427
559
  fi
428
560
  for artifact in "$job_dir"/*; do
429
561
  [[ -e "$artifact" || -L "$artifact" ]] || continue
430
- if [[ -L "$artifact" ]]; then
431
- audit_emit "$id" symlink-artifact
432
- findings=$((findings + 1)); job_failed=1
433
- elif [[ -d "$artifact" ]]; then
434
- audit_emit "$id" nested-directory
435
- findings=$((findings + 1)); job_failed=1
436
- elif [[ -f "$artifact" ]]; then
562
+ if [[ -L "$artifact" ]]; then
563
+ audit_emit "$id" symlink-artifact
564
+ findings=$((findings + 1)); job_failed=1
565
+ elif [[ -d "$artifact" ]]; then
566
+ audit_emit "$id" nested-directory
567
+ findings=$((findings + 1)); job_failed=1
568
+ elif [[ -p "$artifact" && "${artifact##*/}" == "inbox.fifo" ]]; then
569
+ mode="$(path_mode "$artifact" 2>/dev/null || true)"
570
+ if [[ "$mode" != "600" ]]; then
571
+ audit_emit "$id" unsafe-fifo-mode
572
+ findings=$((findings + 1)); job_failed=1
573
+ fi
574
+ elif [[ -f "$artifact" ]]; then
437
575
  mode="$(path_mode "$artifact" 2>/dev/null || true)"
438
576
  if [[ "$mode" != "600" ]]; then
439
577
  audit_emit "$id" unsafe-file-mode
@@ -821,6 +959,9 @@ case "${1:-}" in
821
959
  else
822
960
  echo "running"
823
961
  fi
962
+ fi
963
+ if [[ "$JSON_MODE" -eq 0 ]]; then
964
+ print_mode_notice
824
965
  fi ;;
825
966
  wait)
826
967
  [[ "$JSON_MODE" -eq 0 && $# -ge 2 ]] || usage
@@ -64,6 +64,94 @@ write_current_pid_file() {
64
64
  mv "$tmp" "$path"
65
65
  }
66
66
 
67
+ process_start_time() { # pid -> stable ps(1) start timestamp
68
+ local pid="$1" value
69
+ [[ "$pid" =~ ^[1-9][0-9]{0,9}$ ]] || return 1
70
+ value="$(LC_ALL=C ps -o lstart= -p "$pid" 2>/dev/null)" || return 1
71
+ value="${value#"${value%%[![:space:]]*}"}"
72
+ value="${value%"${value##*[![:space:]]}"}"
73
+ [[ -n "$value" ]] || return 1
74
+ printf '%s\n' "$value"
75
+ }
76
+
77
+ process_parent_pid() {
78
+ local pid="$1" value
79
+ [[ "$pid" =~ ^[1-9][0-9]{0,9}$ ]] || return 1
80
+ value="$(LC_ALL=C ps -o ppid= -p "$pid" 2>/dev/null)" || return 1
81
+ value="${value//[[:space:]]/}"
82
+ [[ "$value" =~ ^[1-9][0-9]{0,9}$ ]] || return 1
83
+ printf '%s\n' "$value"
84
+ }
85
+
86
+ read_session_entry() { # path -> SESSION_ENTRY_{PID,START,ID}
87
+ local path="$1" fields rest
88
+ SESSION_ENTRY_PID=""
89
+ SESSION_ENTRY_START=""
90
+ SESSION_ENTRY_ID=""
91
+ [[ -f "$path" && ! -L "$path" ]] || return 1
92
+ fields="$(perl -MJSON::PP -e '
93
+ use strict;
94
+ use warnings;
95
+ my ($path) = @ARGV;
96
+ my $size = -s $path;
97
+ die "invalid size\n" unless defined($size) && $size <= 4096;
98
+ open my $fh, "<", $path or die $!;
99
+ local $/;
100
+ my $entry = decode_json(<$fh>);
101
+ close $fh or die $!;
102
+ die "invalid entry\n" unless ref($entry) eq "HASH";
103
+ my ($pid, $start, $id) = @{$entry}{qw(pid start_time session_id)};
104
+ die "invalid pid\n" if ref($pid) || !defined($pid) || $pid !~ /\A[1-9][0-9]{0,9}\z/;
105
+ die "invalid start\n" if ref($start) || !defined($start) ||
106
+ $start !~ /\A[ -~]{1,128}\z/;
107
+ die "invalid session\n" if ref($id) || !defined($id) ||
108
+ $id !~ /\A[A-Za-z0-9._:-]{1,256}\z/;
109
+ print "$pid\t$start\t$id";
110
+ ' "$path" 2>/dev/null)" || return 1
111
+ SESSION_ENTRY_PID="${fields%%$'\t'*}"
112
+ rest="${fields#*$'\t'}"
113
+ [[ "$rest" != "$fields" ]] || return 1
114
+ SESSION_ENTRY_START="${rest%%$'\t'*}"
115
+ SESSION_ENTRY_ID="${rest#*$'\t'}"
116
+ [[ "$SESSION_ENTRY_ID" != "$rest" ]] || return 1
117
+ }
118
+
119
+ prune_stale_session_entries() {
120
+ local sessions="$OMNILANE_HOME/sessions" entry live_start
121
+ [[ -d "$sessions" && ! -L "$sessions" ]] || return 0
122
+ for entry in "$sessions"/*.json; do
123
+ [[ -f "$entry" && ! -L "$entry" ]] || continue
124
+ read_session_entry "$entry" || continue
125
+ if ! live_start="$(process_start_time "$SESSION_ENTRY_PID")" ||
126
+ [[ "$live_start" != "$SESSION_ENTRY_START" ]]; then
127
+ rm "$entry" 2>/dev/null || true
128
+ fi
129
+ done
130
+ }
131
+
132
+ find_foreman_session() { # dispatch pid -> nearest live Claude foreman session
133
+ local pid="$1" sessions="$OMNILANE_HOME/sessions"
134
+ local parent start entry hops=0
135
+ [[ "$pid" =~ ^[1-9][0-9]{0,9}$ ]] || return 1
136
+ [[ -d "$sessions" && ! -L "$sessions" ]] || return 1
137
+ prune_stale_session_entries
138
+
139
+ while [[ "$hops" -lt 128 ]]; do
140
+ parent="$(process_parent_pid "$pid")" || return 1
141
+ [[ "$parent" =~ ^[1-9][0-9]{0,9}$ && "$parent" != "$pid" ]] || return 1
142
+ start="$(process_start_time "$parent")" || return 1
143
+ entry="$sessions/$parent.json"
144
+ if read_session_entry "$entry" &&
145
+ [[ "$SESSION_ENTRY_PID" == "$parent" && "$SESSION_ENTRY_START" == "$start" ]]; then
146
+ printf '%s\n' "$SESSION_ENTRY_ID"
147
+ return 0
148
+ fi
149
+ pid="$parent"
150
+ hops=$((hops + 1))
151
+ done
152
+ return 1
153
+ }
154
+
67
155
  # ---- Vendor registry (single source of truth) ---------------------------
68
156
  # Two vendor kinds. CLI-agent vendors run a local binary (see vendor_bin).
69
157
  # Direct-API vendors are OpenAI-compatible /chat/completions endpoints driven
@@ -28,6 +28,14 @@ msg() { # key -> localized string ({} = placeholder, see msgf)
28
28
  case "$L" in zh-TW) echo "已偵測到——請自行執行:";; zh-CN) echo "已检测到——请自行执行:";; ja) echo "検出——次を手動で実行:";; ko) echo "감지됨——직접 실행하세요:";; *) echo "found — run this yourself:";; esac ;;
29
29
  plugin_hint)
30
30
  case "$L" in zh-TW) echo "(選配:把本 repo 裝成 Claude Code 外掛,可用 /route 指令)";; zh-CN) echo "(可选:把本仓库装成 Claude Code 插件,获得 /route 命令)";; ja) echo "(任意:このリポジトリを Claude Code プラグインとして追加すると /route コマンドが使えます)";; ko) echo "(선택: 이 저장소를 Claude Code 플러그인으로 추가하면 /route 명령 사용 가능)";; *) echo "(optional: add this repo as a Claude Code plugin for /route commands)";; esac ;;
31
+ completion_notice_active)
32
+ case "$L" in zh-TW) echo "Claude Code 完成通知:已啟用(omnilane@omnilane 外掛已啟用)。";; zh-CN) echo "Claude Code 完成通知:已启用(omnilane@omnilane 插件已启用)。";; ja) echo "Claude Code 完了通知: 有効です(omnilane@omnilane プラグインが有効)。";; ko) echo "Claude Code 완료 알림: 활성 상태입니다(omnilane@omnilane 플러그인 활성화됨).";; *) echo "Claude Code completion notice: active (omnilane@omnilane is enabled).";; esac ;;
33
+ completion_notice_unknown)
34
+ case "$L" in zh-TW) echo "Claude Code 完成通知:狀態未知(查不到外掛狀態)。";; zh-CN) echo "Claude Code 完成通知:状态未知(无法查询插件状态)。";; ja) echo "Claude Code 完了通知: 状態不明(プラグイン状態を確認できません)。";; ko) echo "Claude Code 완료 알림: 상태를 알 수 없습니다(플러그인 상태 조회 실패).";; *) echo "Claude Code completion notice: unknown (plugin state could not be queried).";; esac ;;
35
+ completion_notice_inactive)
36
+ case "$L" in zh-TW) echo "Claude Code 完成通知:未啟用。這項功能需要安裝外掛;只有 skill symlink 不會送達通知。";; zh-CN) echo "Claude Code 完成通知:未启用。此功能需要安装插件;只有 skill symlink 不会送达通知。";; ja) echo "Claude Code 完了通知: 無効です。この機能にはプラグインのインストールが必要で、skill symlink だけでは通知されません。";; ko) echo "Claude Code 완료 알림: 비활성 상태입니다. 이 기능은 플러그인 설치가 필요하며 skill symlink만으로는 알림이 전달되지 않습니다.";; *) echo "Claude Code completion notice: inactive. This feature requires the plugin install; the skill symlink alone does not deliver notices.";; esac ;;
37
+ completion_notice_install)
38
+ case "$L" in zh-TW) echo "請執行:";; zh-CN) echo "请执行:";; ja) echo "次を実行してください:";; ko) echo "다음을 실행하세요:";; *) echo "Run:";; esac ;;
31
39
  linked)
32
40
  case "$L" in zh-TW) echo "已連結";; zh-CN) echo "已链接";; ja) echo "リンクしました";; ko) echo "링크됨";; *) echo "linked";; esac ;;
33
41
  removed)