omnilane 0.15.0 → 0.21.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 +58 -1
- package/README.ja.md +46 -0
- package/README.ko.md +44 -0
- package/README.md +56 -1
- package/README.zh-CN.md +41 -0
- package/README.zh-TW.md +41 -0
- package/VERSION +1 -1
- package/bin/omnilane +5 -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 +65 -10
- package/scripts/doctor.sh +126 -0
- package/scripts/jobs.sh +164 -11
- package/scripts/lib/common.sh +88 -0
- package/scripts/lib/goal-loop.sh +776 -0
- package/scripts/lib/i18n.sh +8 -0
- package/scripts/lib/job-worker.sh +234 -4
- package/scripts/lib/live-protocol.sh +70 -0
- package/scripts/runners/run-claude.sh +106 -0
- package/scripts/runners/run-gemini.sh +109 -0
- package/skills/omnilane/SKILL.md +172 -0
package/scripts/jobs.sh
CHANGED
|
@@ -4,14 +4,21 @@ 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"
|
|
13
|
+
# shellcheck disable=SC1091
|
|
14
|
+
source "$OMNILANE_REPO/scripts/lib/live-protocol.sh"
|
|
10
15
|
JOBS="$OMNILANE_HOME/jobs"
|
|
11
16
|
JOB_ID_PATTERN='^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$'
|
|
12
17
|
JSON_MODE=0
|
|
13
18
|
COMMAND="unknown"
|
|
14
19
|
|
|
20
|
+
# The backslash case pattern is intentional.
|
|
21
|
+
# shellcheck disable=SC1003
|
|
15
22
|
json_escape() {
|
|
16
23
|
local s="$1" out="" ch escaped code i
|
|
17
24
|
for ((i = 0; i < ${#s}; i++)); do
|
|
@@ -49,7 +56,7 @@ die() {
|
|
|
49
56
|
exit "$rc"
|
|
50
57
|
}
|
|
51
58
|
|
|
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"
|
|
59
|
+
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
60
|
|
|
54
61
|
usage() {
|
|
55
62
|
die 2 "$USAGE_TEXT"
|
|
@@ -108,6 +115,14 @@ read_public_metadata() {
|
|
|
108
115
|
PUBLIC_METADATA="$value"
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
print_mode_notice() {
|
|
119
|
+
local path="$JOB_DIR/mode-notice.txt"
|
|
120
|
+
[[ -e "$path" || -L "$path" ]] || return 0
|
|
121
|
+
if read_public_metadata "$path"; then
|
|
122
|
+
printf '%s\n' "$PUBLIC_METADATA"
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
valid_utf8() {
|
|
112
127
|
command -v iconv >/dev/null 2>&1 || return 1
|
|
113
128
|
printf '%s' "$1" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1
|
|
@@ -131,6 +146,68 @@ read_job_pid() {
|
|
|
131
146
|
RECORDED_PID="$value"
|
|
132
147
|
}
|
|
133
148
|
|
|
149
|
+
load_job_vendor() {
|
|
150
|
+
local session_mode_re='"session_mode":"(live|single-shot)"'
|
|
151
|
+
if ! read_public_metadata "$JOB_DIR/meta.json" || ! parse_stats_metadata "$PUBLIC_METADATA"; then
|
|
152
|
+
die 1 "job metadata is not safely readable"
|
|
153
|
+
fi
|
|
154
|
+
JOB_VENDOR="$STATS_VENDOR"
|
|
155
|
+
JOB_SESSION_MODE=""
|
|
156
|
+
if [[ "$PUBLIC_METADATA" =~ $session_mode_re ]]; then
|
|
157
|
+
JOB_SESSION_MODE="${BASH_REMATCH[1]}"
|
|
158
|
+
elif [[ "$JOB_VENDOR" == "claude" ]]; then
|
|
159
|
+
# Compatibility with live jobs created before session_mode metadata.
|
|
160
|
+
JOB_SESSION_MODE="live"
|
|
161
|
+
else
|
|
162
|
+
JOB_SESSION_MODE="single-shot"
|
|
163
|
+
fi
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
require_live_job() {
|
|
167
|
+
load_job_vendor
|
|
168
|
+
live_vendor_capable "$JOB_VENDOR" ||
|
|
169
|
+
die 1 "job vendor '$JOB_VENDOR' is not live-capable (live vendors: $(live_capable_vendors)); it runs in single-shot mode"
|
|
170
|
+
[[ "$JOB_SESSION_MODE" == "live" ]] ||
|
|
171
|
+
die 1 "job vendor '$JOB_VENDOR' was dispatched in single-shot mode"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
wait_for_live_ready() {
|
|
175
|
+
local deadline=$((SECONDS + 5))
|
|
176
|
+
while [[ ! -f "$JOB_DIR/inbox.ready" ]]; do
|
|
177
|
+
if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
|
|
178
|
+
die 1 "job is no longer accepting live messages"
|
|
179
|
+
fi
|
|
180
|
+
if [[ "$SECONDS" -ge "$deadline" ]]; then
|
|
181
|
+
die 124 "live inbox did not become ready within 5s"
|
|
182
|
+
fi
|
|
183
|
+
sleep 0.1
|
|
184
|
+
done
|
|
185
|
+
[[ ! -L "$JOB_DIR/inbox.ready" ]] || die 1 "unsafe live inbox readiness path"
|
|
186
|
+
[[ -p "$JOB_DIR/inbox.fifo" && ! -L "$JOB_DIR/inbox.fifo" ]] || die 1 "live inbox FIFO is unavailable"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
write_fifo_with_timeout() {
|
|
190
|
+
local fifo="$1" payload="$2" writer_pid deadline rc
|
|
191
|
+
(
|
|
192
|
+
printf '%s\n' "$payload" > "$fifo"
|
|
193
|
+
) &
|
|
194
|
+
writer_pid=$!
|
|
195
|
+
deadline=$((SECONDS + 5))
|
|
196
|
+
while kill -0 "$writer_pid" 2>/dev/null; do
|
|
197
|
+
if [[ "$SECONDS" -ge "$deadline" ]]; then
|
|
198
|
+
kill -TERM "$writer_pid" 2>/dev/null || true
|
|
199
|
+
wait "$writer_pid" 2>/dev/null || true
|
|
200
|
+
return 124
|
|
201
|
+
fi
|
|
202
|
+
sleep 0.1
|
|
203
|
+
done
|
|
204
|
+
set +e
|
|
205
|
+
wait "$writer_pid"
|
|
206
|
+
rc=$?
|
|
207
|
+
set -e
|
|
208
|
+
return "$rc"
|
|
209
|
+
}
|
|
210
|
+
|
|
134
211
|
parse_stats_metadata() {
|
|
135
212
|
local value="$1"
|
|
136
213
|
local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"('"$OMNILANE_VENDOR_ALT"')"(,|})'
|
|
@@ -142,10 +219,11 @@ parse_stats_metadata() {
|
|
|
142
219
|
}
|
|
143
220
|
|
|
144
221
|
parse_audit_metadata() {
|
|
145
|
-
local value="$1"
|
|
222
|
+
local value="$1" current_re legacy_re
|
|
146
223
|
local json_string='([^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*'
|
|
147
|
-
|
|
148
|
-
[[
|
|
224
|
+
current_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"session_mode\":\"(live|single-shot)\",\"idle_timeout\":(0|[1-9][0-9]*),\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work|sysops)\",\"workdir\":\"${json_string}\",\"foreman_session\":\"${json_string}\",\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"\\}$"
|
|
225
|
+
legacy_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work|sysops)\",\"workdir\":\"${json_string}\",(\"foreman_session\":\"${json_string}\",)?\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"\\}$"
|
|
226
|
+
[[ "$value" =~ $current_re || "$value" =~ $legacy_re ]]
|
|
149
227
|
}
|
|
150
228
|
|
|
151
229
|
print_stat_counts() {
|
|
@@ -280,6 +358,71 @@ case "${1:-}" in
|
|
|
280
358
|
help|--help|-h)
|
|
281
359
|
[[ "$JSON_MODE" -eq 0 && $# -eq 1 ]] || usage
|
|
282
360
|
echo "$USAGE_TEXT" ;;
|
|
361
|
+
send)
|
|
362
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 3 ]] || usage
|
|
363
|
+
select_job "$2"
|
|
364
|
+
require_live_job
|
|
365
|
+
wait_for_live_ready
|
|
366
|
+
if [[ "${FOREMAN_SESSION+x}" == "x" ]]; then
|
|
367
|
+
foreman_value="$FOREMAN_SESSION"
|
|
368
|
+
else
|
|
369
|
+
foreman_value="${foreman_session-}"
|
|
370
|
+
fi
|
|
371
|
+
payload="$(live_encode_message "$JOB_VENDOR" "$2" "$foreman_value" "$3")"
|
|
372
|
+
set +e
|
|
373
|
+
write_fifo_with_timeout "$JOB_DIR/inbox.fifo" "$payload"
|
|
374
|
+
send_rc=$?
|
|
375
|
+
set -e
|
|
376
|
+
[[ "$send_rc" -eq 0 ]] || die "$send_rc" "send timed out or failed; the live worker may be gone"
|
|
377
|
+
echo "sent $2"
|
|
378
|
+
;;
|
|
379
|
+
watch)
|
|
380
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
|
|
381
|
+
select_job "$2"
|
|
382
|
+
require_live_job
|
|
383
|
+
events_path="$JOB_DIR/events.jsonl"
|
|
384
|
+
watch_deadline=$((SECONDS + 5))
|
|
385
|
+
while [[ ! -f "$events_path" ]]; do
|
|
386
|
+
if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
|
|
387
|
+
die 1 "job has no live event stream"
|
|
388
|
+
fi
|
|
389
|
+
[[ "$SECONDS" -lt "$watch_deadline" ]] || die 124 "live event stream did not appear within 5s"
|
|
390
|
+
sleep 0.1
|
|
391
|
+
done
|
|
392
|
+
[[ ! -L "$events_path" ]] || die 1 "unsafe live event path"
|
|
393
|
+
if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
|
|
394
|
+
cat "$events_path"
|
|
395
|
+
else
|
|
396
|
+
tail -n +1 -f -- "$events_path"
|
|
397
|
+
fi
|
|
398
|
+
;;
|
|
399
|
+
close)
|
|
400
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
|
|
401
|
+
select_job "$2"
|
|
402
|
+
require_live_job
|
|
403
|
+
if [[ -e "$JOB_DIR/exit" || -L "$JOB_DIR/exit" ]]; then
|
|
404
|
+
read_exit_code "$JOB_DIR/exit" || die 1 "invalid recorded exit status"
|
|
405
|
+
echo "already closed (exit $RECORDED_EXIT)"
|
|
406
|
+
exit "$RECORDED_EXIT"
|
|
407
|
+
fi
|
|
408
|
+
holder_path="$JOB_DIR/inbox.holder.pid"
|
|
409
|
+
read_job_pid "$holder_path" || die 1 "live inbox holder is unavailable"
|
|
410
|
+
holder_pid="$RECORDED_PID"
|
|
411
|
+
kill -0 "$holder_pid" 2>/dev/null || die 1 "live inbox holder is gone"
|
|
412
|
+
kill -USR1 "$holder_pid" 2>/dev/null || die 1 "could not signal live inbox holder"
|
|
413
|
+
close_deadline=$((SECONDS + 10))
|
|
414
|
+
while [[ ! -e "$JOB_DIR/exit" && ! -L "$JOB_DIR/exit" ]]; do
|
|
415
|
+
if [[ "$SECONDS" -ge "$close_deadline" ]]; then
|
|
416
|
+
die 124 "close signal sent, but the live worker did not terminate within 10s"
|
|
417
|
+
fi
|
|
418
|
+
sleep 0.1
|
|
419
|
+
done
|
|
420
|
+
read_exit_code "$JOB_DIR/exit" || die 1 "invalid recorded exit status"
|
|
421
|
+
events_path="$JOB_DIR/events.jsonl"
|
|
422
|
+
result_count="$(live_count_result_events "$JOB_VENDOR" "$events_path")"
|
|
423
|
+
echo "closed $2 (result_events=$result_count exit=$RECORDED_EXIT)"
|
|
424
|
+
exit "$RECORDED_EXIT"
|
|
425
|
+
;;
|
|
283
426
|
tail)
|
|
284
427
|
# Peek at the live public output stream of one job (running or done)
|
|
285
428
|
# without touching its exit contract. Only out.txt is shown; stderr and
|
|
@@ -427,13 +570,20 @@ case "${1:-}" in
|
|
|
427
570
|
fi
|
|
428
571
|
for artifact in "$job_dir"/*; do
|
|
429
572
|
[[ -e "$artifact" || -L "$artifact" ]] || continue
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
573
|
+
if [[ -L "$artifact" ]]; then
|
|
574
|
+
audit_emit "$id" symlink-artifact
|
|
575
|
+
findings=$((findings + 1)); job_failed=1
|
|
576
|
+
elif [[ -d "$artifact" ]]; then
|
|
577
|
+
audit_emit "$id" nested-directory
|
|
578
|
+
findings=$((findings + 1)); job_failed=1
|
|
579
|
+
elif [[ -p "$artifact" && ( "${artifact##*/}" == "inbox.fifo" ||
|
|
580
|
+
"${artifact##*/}" == "runner-inbox.fifo" ) ]]; then
|
|
581
|
+
mode="$(path_mode "$artifact" 2>/dev/null || true)"
|
|
582
|
+
if [[ "$mode" != "600" ]]; then
|
|
583
|
+
audit_emit "$id" unsafe-fifo-mode
|
|
584
|
+
findings=$((findings + 1)); job_failed=1
|
|
585
|
+
fi
|
|
586
|
+
elif [[ -f "$artifact" ]]; then
|
|
437
587
|
mode="$(path_mode "$artifact" 2>/dev/null || true)"
|
|
438
588
|
if [[ "$mode" != "600" ]]; then
|
|
439
589
|
audit_emit "$id" unsafe-file-mode
|
|
@@ -821,6 +971,9 @@ case "${1:-}" in
|
|
|
821
971
|
else
|
|
822
972
|
echo "running"
|
|
823
973
|
fi
|
|
974
|
+
fi
|
|
975
|
+
if [[ "$JSON_MODE" -eq 0 ]]; then
|
|
976
|
+
print_mode_notice
|
|
824
977
|
fi ;;
|
|
825
978
|
wait)
|
|
826
979
|
[[ "$JSON_MODE" -eq 0 && $# -ge 2 ]] || usage
|
package/scripts/lib/common.sh
CHANGED
|
@@ -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
|