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
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
|
+
}
|
package/scripts/dispatch.sh
CHANGED
|
@@ -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")" "$
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
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
|
package/scripts/lib/i18n.sh
CHANGED
|
@@ -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)
|