aiur-cli 0.0.4 → 0.0.5-nightly.24dcc4d
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/README.md +89 -54
- package/bin/aiur.js +2 -2
- package/libexec/aiur-engine.sh +1299 -109
- package/libexec/guard-pr-deletions.sh +70 -0
- package/package.json +8 -7
package/libexec/aiur-engine.sh
CHANGED
|
@@ -35,19 +35,180 @@ elif [ -n "${__aiur_hard_nofile}" ]; then
|
|
|
35
35
|
fi
|
|
36
36
|
unset __aiur_hard_nofile
|
|
37
37
|
|
|
38
|
+
# Export the effective soft limit after the best-effort raise. The BEAM uses
|
|
39
|
+
# this inherited value for FD-headroom admission on hosts without procfs,
|
|
40
|
+
# avoiding a runtime `ulimit` subprocess precisely when descriptors are scarce.
|
|
41
|
+
__aiur_soft_nofile="$(ulimit -Sn 2>/dev/null || echo)"
|
|
42
|
+
if [[ "$__aiur_soft_nofile" =~ ^[0-9]+$ ]]; then
|
|
43
|
+
export AIUR_NOFILE_SOFT_LIMIT="$__aiur_soft_nofile"
|
|
44
|
+
else
|
|
45
|
+
unset AIUR_NOFILE_SOFT_LIMIT
|
|
46
|
+
fi
|
|
47
|
+
unset __aiur_soft_nofile
|
|
48
|
+
|
|
49
|
+
# Preserve the shell that initiated the run as a best-effort Executor root.
|
|
50
|
+
# An explicit positive override wins (service managers may know a better root);
|
|
51
|
+
# otherwise the engine's parent is the nearest identity available before tmux
|
|
52
|
+
# hands daemon ownership to its pane launcher.
|
|
53
|
+
if ! [[ "${AIUR_OPERATOR_PID:-}" =~ ^[1-9][0-9]*$ ]]; then
|
|
54
|
+
if [[ "${PPID:-}" =~ ^[1-9][0-9]*$ ]]; then
|
|
55
|
+
export AIUR_OPERATOR_PID="$PPID"
|
|
56
|
+
else
|
|
57
|
+
unset AIUR_OPERATOR_PID
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
38
61
|
die() {
|
|
39
62
|
echo "❌ $*" >&2
|
|
40
63
|
exit 1
|
|
41
64
|
}
|
|
42
65
|
|
|
66
|
+
legacy_config_path() {
|
|
67
|
+
while [ "$#" -gt 0 ]; do
|
|
68
|
+
case "$1" in
|
|
69
|
+
--logs-root | --host | --port | --max-agents)
|
|
70
|
+
if [ "$#" -ge 2 ]; then
|
|
71
|
+
shift 2
|
|
72
|
+
else
|
|
73
|
+
shift
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
--)
|
|
77
|
+
shift
|
|
78
|
+
[ "$#" -gt 0 ] || return
|
|
79
|
+
case "$1" in
|
|
80
|
+
*.aiurconfig) printf '%s' "$1" ;;
|
|
81
|
+
esac
|
|
82
|
+
return
|
|
83
|
+
;;
|
|
84
|
+
--logs-root=* | --host=* | --port=* | --max-agents=* | --*)
|
|
85
|
+
shift
|
|
86
|
+
;;
|
|
87
|
+
*.aiurconfig)
|
|
88
|
+
printf '%s' "$1"
|
|
89
|
+
return
|
|
90
|
+
;;
|
|
91
|
+
*)
|
|
92
|
+
# A positional non-legacy config is authoritative. Let the Elixir CLI
|
|
93
|
+
# validate it instead of rejecting an unrelated ambient legacy file.
|
|
94
|
+
return
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
local target_root="${AIUR_REPO_ROOT:-}" home_real
|
|
100
|
+
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
101
|
+
|
|
102
|
+
if [ -n "$target_root" ]; then
|
|
103
|
+
if [ -f "$target_root/.aiur/config" ]; then
|
|
104
|
+
return
|
|
105
|
+
fi
|
|
106
|
+
if [ -f "$target_root/.aiurconfig" ]; then
|
|
107
|
+
printf '%s' "$target_root/.aiurconfig"
|
|
108
|
+
return
|
|
109
|
+
fi
|
|
110
|
+
else
|
|
111
|
+
local pwd_real d
|
|
112
|
+
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
113
|
+
d="$pwd_real"
|
|
114
|
+
|
|
115
|
+
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
116
|
+
if [ -f "$d/.aiur/config" ]; then
|
|
117
|
+
return
|
|
118
|
+
fi
|
|
119
|
+
if [ -f "$d/.aiurconfig" ]; then
|
|
120
|
+
printf '%s' "$d/.aiurconfig"
|
|
121
|
+
return
|
|
122
|
+
fi
|
|
123
|
+
d="$(dirname "$d")"
|
|
124
|
+
done
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
if [ -n "$home_real" ] && [ ! -f "$home_real/.aiur/config" ] && [ -f "$home_real/.aiurconfig" ]; then
|
|
128
|
+
printf '%s' "$home_real/.aiurconfig"
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
reject_legacy_config() {
|
|
133
|
+
local legacy legacy_basename canonical
|
|
134
|
+
legacy="$(legacy_config_path "$@")"
|
|
135
|
+
[ -z "$legacy" ] && return
|
|
136
|
+
legacy_basename="${legacy##*/}"
|
|
137
|
+
|
|
138
|
+
if [ "$legacy_basename" = ".aiurconfig" ]; then
|
|
139
|
+
canonical="$(dirname "$legacy")/.aiur/config"
|
|
140
|
+
else
|
|
141
|
+
canonical="${legacy%.aiurconfig}.yaml"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
die "$legacy is no longer supported. Move it to $canonical. Keep relative prompt_file and hooks_file paths valid from the new config directory."
|
|
145
|
+
}
|
|
146
|
+
|
|
43
147
|
engine_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
148
|
|
|
149
|
+
package_version() {
|
|
150
|
+
local package_file="$1"
|
|
151
|
+
[ -r "$package_file" ] || return 1
|
|
152
|
+
sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$package_file" | head -n 1
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
cli_package_version() {
|
|
156
|
+
package_version "$engine_dir/../package.json"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
version_is_older() {
|
|
160
|
+
local installed="$1" available="$2"
|
|
161
|
+
local installed_major installed_minor installed_patch installed_pre
|
|
162
|
+
local available_major available_minor available_patch available_pre
|
|
163
|
+
|
|
164
|
+
if [[ ! "$installed" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z.-]+))?(\+[0-9A-Za-z.-]+)?$ ]]; then return 1; fi
|
|
165
|
+
installed_major="${BASH_REMATCH[1]}"
|
|
166
|
+
installed_minor="${BASH_REMATCH[2]}"
|
|
167
|
+
installed_patch="${BASH_REMATCH[3]}"
|
|
168
|
+
installed_pre="${BASH_REMATCH[5]}"
|
|
169
|
+
|
|
170
|
+
if [[ ! "$available" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z.-]+))?(\+[0-9A-Za-z.-]+)?$ ]]; then return 1; fi
|
|
171
|
+
available_major="${BASH_REMATCH[1]}"
|
|
172
|
+
available_minor="${BASH_REMATCH[2]}"
|
|
173
|
+
available_patch="${BASH_REMATCH[3]}"
|
|
174
|
+
available_pre="${BASH_REMATCH[5]}"
|
|
175
|
+
|
|
176
|
+
[ "$installed_major" -lt "$available_major" ] ||
|
|
177
|
+
{ [ "$installed_major" -eq "$available_major" ] && [ "$installed_minor" -lt "$available_minor" ]; } ||
|
|
178
|
+
{ [ "$installed_major" -eq "$available_major" ] && [ "$installed_minor" -eq "$available_minor" ] && [ "$installed_patch" -lt "$available_patch" ]; } ||
|
|
179
|
+
{
|
|
180
|
+
[ "$installed_major" -eq "$available_major" ] &&
|
|
181
|
+
[ "$installed_minor" -eq "$available_minor" ] &&
|
|
182
|
+
[ "$installed_patch" -eq "$available_patch" ] &&
|
|
183
|
+
{
|
|
184
|
+
{ [ -n "$installed_pre" ] && [ -z "$available_pre" ]; } ||
|
|
185
|
+
{ [ -n "$installed_pre" ] && [ -n "$available_pre" ] && [[ "$installed_pre" < "$available_pre" ]]; }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
warn_if_cli_behind_release_checkout() {
|
|
191
|
+
local stamp="${AIUR_RELEASE_DIR:-}/AIUR_BUILD_STAMP"
|
|
192
|
+
local repo_root installed_version checkout_version
|
|
193
|
+
|
|
194
|
+
[ -r "$stamp" ] || return 0
|
|
195
|
+
repo_root="$(sed -n 's/^repo_root=//p' "$stamp" | head -n 1)"
|
|
196
|
+
[ -n "$repo_root" ] || return 0
|
|
197
|
+
|
|
198
|
+
installed_version="$(cli_package_version || true)"
|
|
199
|
+
checkout_version="$(package_version "$repo_root/packaging/npm/aiur-cli/package.json" || true)"
|
|
200
|
+
|
|
201
|
+
if version_is_older "$installed_version" "$checkout_version"; then
|
|
202
|
+
echo "aiur: installed CLI $installed_version is older than checkout CLI $checkout_version; update aiur-cli before retrying" >&2
|
|
203
|
+
fi
|
|
204
|
+
}
|
|
205
|
+
|
|
45
206
|
# --- distribution identity (per-instance: keyed by the aiur project root) -----
|
|
46
207
|
|
|
47
208
|
# The aiur project root used to key this instance. AIUR_REPO_ROOT (set by the dev
|
|
48
209
|
# shim) wins. Otherwise walk up from $PWD to the first dir holding a REPO-LOCAL
|
|
49
|
-
# config — but the walk STOPS at $HOME: the global config at ~/.aiur/config
|
|
50
|
-
#
|
|
210
|
+
# config — but the walk STOPS at $HOME: the global config at ~/.aiur/config is
|
|
211
|
+
# not a repo root, and treating it as one would collapse every
|
|
51
212
|
# project under $HOME onto one key (#443). When no repo-local config is found, the
|
|
52
213
|
# BEAM serves this run via the global config (mirroring its discovery order in
|
|
53
214
|
# src/lib/aiur/workflow.ex) — or via none — and in both cases the project being
|
|
@@ -71,7 +232,7 @@ aiur_project_root() {
|
|
|
71
232
|
|
|
72
233
|
local d="$pwd_real"
|
|
73
234
|
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
74
|
-
if [ -f "$d/.aiur/config" ]
|
|
235
|
+
if [ -f "$d/.aiur/config" ]; then
|
|
75
236
|
printf '%s' "$d"
|
|
76
237
|
return
|
|
77
238
|
fi
|
|
@@ -90,7 +251,7 @@ aiur_project_root_source() {
|
|
|
90
251
|
|
|
91
252
|
local d="$pwd_real"
|
|
92
253
|
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
93
|
-
if [ -f "$d/.aiur/config" ]
|
|
254
|
+
if [ -f "$d/.aiur/config" ]; then
|
|
94
255
|
printf 'repo'
|
|
95
256
|
return
|
|
96
257
|
fi
|
|
@@ -213,16 +374,35 @@ release_dir=""
|
|
|
213
374
|
vsn_dir=""
|
|
214
375
|
release_bin=""
|
|
215
376
|
|
|
377
|
+
control_release_retry() {
|
|
378
|
+
[ -n "${AIUR_CONTROL_RELEASE_RETRY_SIGNAL:-}" ] && : >"$AIUR_CONTROL_RELEASE_RETRY_SIGNAL"
|
|
379
|
+
return 75
|
|
380
|
+
}
|
|
381
|
+
|
|
216
382
|
resolve_release() {
|
|
217
383
|
release_dir="${AIUR_RELEASE_DIR:-}"
|
|
218
384
|
[ -n "$release_dir" ] || die "AIUR_RELEASE_DIR is not set; the engine must be invoked via the aiur or aiurdev wrapper"
|
|
219
|
-
[ -d "$release_dir" ]
|
|
385
|
+
if [ ! -d "$release_dir" ]; then
|
|
386
|
+
# aiurdev uses EX_TEMPFAIL to wait out an in-place dev rebuild and retry
|
|
387
|
+
# exactly once. Product launches keep the existing fatal diagnostics.
|
|
388
|
+
if [ "${AIUR_CONTROL_RELEASE_RETRYABLE:-0}" = "1" ]; then
|
|
389
|
+
control_release_retry
|
|
390
|
+
return $?
|
|
391
|
+
fi
|
|
392
|
+
die "AIUR_RELEASE_DIR does not exist: $release_dir"
|
|
393
|
+
fi
|
|
220
394
|
|
|
221
395
|
local release_vsn
|
|
222
|
-
release_vsn="$(cut -d' ' -f2 "$release_dir/releases/start_erl.data")"
|
|
396
|
+
release_vsn="$(cut -d' ' -f2 "$release_dir/releases/start_erl.data" 2>/dev/null || true)"
|
|
223
397
|
vsn_dir="$release_dir/releases/$release_vsn"
|
|
224
398
|
release_bin="$release_dir/bin/aiur"
|
|
225
|
-
[ -
|
|
399
|
+
if [ -z "$release_vsn" ] || [ ! -x "$release_bin" ] || [ ! -x "$vsn_dir/elixir" ]; then
|
|
400
|
+
if [ "${AIUR_CONTROL_RELEASE_RETRYABLE:-0}" = "1" ]; then
|
|
401
|
+
control_release_retry
|
|
402
|
+
return $?
|
|
403
|
+
fi
|
|
404
|
+
die "release elixir launcher not found at $vsn_dir/elixir"
|
|
405
|
+
fi
|
|
226
406
|
}
|
|
227
407
|
|
|
228
408
|
# --- argv round-trip (System.argv is empty under `elixir --eval`) -------------
|
|
@@ -267,17 +447,39 @@ build_init_cmd() {
|
|
|
267
447
|
|
|
268
448
|
usage() {
|
|
269
449
|
cat <<'EOF'
|
|
270
|
-
Usage: aiur [--interactive] [--max-agents <n>] [--logs-root <path>] [--port <port>] [--host <host>] [path
|
|
271
|
-
aiur
|
|
272
|
-
aiur --
|
|
450
|
+
Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agents <n>] [--logs-root <path>] [--port <port>] [--host <host>] [config-path]
|
|
451
|
+
aiur run [--bg] [--no-dashboard] [--executor] [--debug] explicit launch form (foreground unless --bg)
|
|
452
|
+
aiur init [--force] scaffold .aiur/config (interactive setup wizard)
|
|
453
|
+
aiur --bg [--no-dashboard] [--executor] [--debug] start detached; dashboard on unless suppressed
|
|
273
454
|
aiur stop stop the running session
|
|
455
|
+
aiur restart [--no-build] [run flags] stop, refresh the build, start again (detached)
|
|
274
456
|
aiur status show agent status
|
|
275
457
|
aiur agents show each agent's state + current activity
|
|
458
|
+
aiur commands [<decision-id>] [--filter all|open|blocking|resolved] [--blocking] [--ticket <id>] [--search <text>] [--cursor <cursor>] [--limit <n>] [--json]
|
|
459
|
+
aiur executor-answer <decision-id> --expected-version <n> (--option <id>|--custom-response <text>) --rationale <text> --idempotency-key <key> [--executor-id <id>]
|
|
460
|
+
aiur executor-escalate <decision-id> --expected-version <n> --reason <text> [--executor-id <id>]
|
|
461
|
+
aiur units [--scope live|unfinished|all|none] [--condition active|alert|paused|queued|finished]... [--format auto|table|records] [--json]
|
|
462
|
+
aiur build-orders [<root>] [--json] show the Build Order catalog or one root
|
|
463
|
+
aiur analytics [--range run|full] [--since <ISO-8601>] [--until <ISO-8601>] [--build-order <id>] [--json]
|
|
276
464
|
aiur alerts [--needs-attention] show structured alert feed
|
|
277
465
|
aiur watch [--full|--changes] [--interval <secs>] server-side status board
|
|
466
|
+
aiur executor-listen [--topic <pattern>] stream Executor events as JSON lines
|
|
467
|
+
aiur executor-wait [--timeout <seconds>] [--json] block until Executor work arrives
|
|
468
|
+
aiur executor-emit <topic> --payload <json> publish an Executor event
|
|
469
|
+
aiur executor-subscribe|executor-unsubscribe <pattern>
|
|
470
|
+
aiur executor-subscriptions list persistent Executor bindings
|
|
278
471
|
aiur set max-agents <n> change the concurrent-agent cap at runtime
|
|
279
|
-
aiur pause
|
|
280
|
-
aiur
|
|
472
|
+
aiur pause | resume flip the global pause switch (whole daemon)
|
|
473
|
+
aiur pause <ids|--all> | resume <ids|--all> per-agent pause/resume
|
|
474
|
+
aiur message <id> <text> send Executor text to a running agent
|
|
475
|
+
aiur --todo <ids...> [--only] queue tickets; optionally dequeue all other pending tickets
|
|
476
|
+
aiur findings [--unfiled] [--slugs] [--scope aiur|repo] inspect host-local findings
|
|
477
|
+
aiur findings --record <json> --repo <owner/repo> append one validated finding
|
|
478
|
+
aiur findings --digest [--scope aiur|repo] generate the promoted Markdown digest
|
|
479
|
+
aiur guard-pr-deletions [base-branch] refuse PRs with excessive untouched deletions
|
|
480
|
+
aiur ask <title> [--body <text>|--body-file <path>] [--urgency low|normal|high] [--blocking]
|
|
481
|
+
aiur ask --done <id> [--note <text>] create or resolve an operator request
|
|
482
|
+
aiur asks [--open|--all] [--json] inspect current-repository operator requests
|
|
281
483
|
aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
|
|
282
484
|
aiur --version
|
|
283
485
|
EOF
|
|
@@ -287,6 +489,8 @@ EOF
|
|
|
287
489
|
|
|
288
490
|
run_version() {
|
|
289
491
|
resolve_release
|
|
492
|
+
AIUR_CLI_VERSION="$(cli_package_version || true)"
|
|
493
|
+
export AIUR_CLI_VERSION
|
|
290
494
|
# Distribution-free (like init): printing the version is a compile-time
|
|
291
495
|
# constant, so never claim the node name — otherwise `aiur --version` fails
|
|
292
496
|
# whenever an aiur session is already running.
|
|
@@ -308,16 +512,81 @@ run_init() {
|
|
|
308
512
|
exec "${release_cmd[@]}"
|
|
309
513
|
}
|
|
310
514
|
|
|
515
|
+
# --- one-shot: --todo (control RPC; requires a running daemon) ----------------
|
|
516
|
+
|
|
517
|
+
parsed_todo_only=0
|
|
518
|
+
|
|
519
|
+
parse_todo_args() {
|
|
520
|
+
parsed_targets=()
|
|
521
|
+
parsed_todo_only=0
|
|
522
|
+
|
|
523
|
+
local saw_todo=0 raw part parts
|
|
524
|
+
for raw in "$@"; do
|
|
525
|
+
case "$raw" in
|
|
526
|
+
--todo)
|
|
527
|
+
[ "$saw_todo" -eq 0 ] || return 1
|
|
528
|
+
saw_todo=1
|
|
529
|
+
;;
|
|
530
|
+
--only)
|
|
531
|
+
parsed_todo_only=1
|
|
532
|
+
;;
|
|
533
|
+
-*)
|
|
534
|
+
return 1
|
|
535
|
+
;;
|
|
536
|
+
*)
|
|
537
|
+
IFS=',' read -ra parts <<<"$raw"
|
|
538
|
+
for part in "${parts[@]}"; do
|
|
539
|
+
part="$(trim "$part")"
|
|
540
|
+
if [ -z "$part" ] || [[ ! "$part" =~ ^[0-9]+$ ]]; then return 1; fi
|
|
541
|
+
while [ "${#part}" -gt 1 ] && [ "${part#0}" != "$part" ]; do
|
|
542
|
+
part="${part#0}"
|
|
543
|
+
done
|
|
544
|
+
parsed_targets+=("$part")
|
|
545
|
+
done
|
|
546
|
+
;;
|
|
547
|
+
esac
|
|
548
|
+
done
|
|
549
|
+
|
|
550
|
+
[ "$saw_todo" -eq 1 ] && [ "${#parsed_targets[@]}" -gt 0 ]
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
run_todo() {
|
|
554
|
+
if ! parse_todo_args "$@"; then
|
|
555
|
+
echo "aiur: --todo expects one or more numeric issue IDs, optionally followed by --only" >&2
|
|
556
|
+
exit 64
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
local only_arg="false"
|
|
560
|
+
[ "$parsed_todo_only" -eq 1 ] && only_arg="true"
|
|
561
|
+
run_control_rpc "Aiur.AgentControlCLI.todo($(elixir_list_literal "${parsed_targets[@]}"), only: $only_arg, emit_exit_marker: true)"
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
# --- one-shot: findings (distribution-free, no daemon/tmux) -------------------
|
|
565
|
+
|
|
566
|
+
run_findings() {
|
|
567
|
+
run_init "$@"
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
# --- one-shot: ask / asks (distribution-free, no daemon/tmux) ----------------
|
|
571
|
+
|
|
572
|
+
run_asks() {
|
|
573
|
+
run_init "$@"
|
|
574
|
+
}
|
|
575
|
+
|
|
311
576
|
# --- interactive / background run -------------------------------------------
|
|
312
577
|
#
|
|
313
578
|
# mode=foreground attaches the UI and tears down on exit; mode=background leaves
|
|
314
579
|
# the detached tmux session running and returns.
|
|
315
580
|
|
|
316
|
-
# Load
|
|
317
|
-
#
|
|
318
|
-
# An already-exported variable always wins, so a shell export overrides the file.
|
|
581
|
+
# Load operator/machine credentials before repo-local settings. Since each file
|
|
582
|
+
# only fills unset names, shell exports win first, then ~/.aiur/.env, then ./.env.
|
|
319
583
|
load_dotenv() {
|
|
320
|
-
|
|
584
|
+
load_dotenv_file "$HOME/.aiur/.env"
|
|
585
|
+
load_dotenv_file ".env"
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
load_dotenv_file() {
|
|
589
|
+
local file="$1" line key val
|
|
321
590
|
[ -f "$file" ] || return 0
|
|
322
591
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
323
592
|
line="${line#"${line%%[![:space:]]*}"}"
|
|
@@ -337,6 +606,56 @@ load_dotenv() {
|
|
|
337
606
|
done <"$file"
|
|
338
607
|
}
|
|
339
608
|
|
|
609
|
+
# The readiness token grants the one-shot `aiur init` assessment access that a
|
|
610
|
+
# normal daemon and its child agents must never inherit. Keep dotenv loading
|
|
611
|
+
# generic, then remove this run-only secret before any session process starts.
|
|
612
|
+
scrub_run_only_env() {
|
|
613
|
+
unset AIUR_CI_READINESS_TOKEN
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
run_argv=()
|
|
617
|
+
# Default dashboard bind host. Prefer this machine's Tailscale IPv4 so the
|
|
618
|
+
# dashboard is reachable across the tailnet by default (when config omits it);
|
|
619
|
+
# fall back to loopback when Tailscale is absent, or when dashboard credentials
|
|
620
|
+
# are unset (a non-loopback bind requires them, so we stay on loopback rather
|
|
621
|
+
# than refuse to start). The BEAM applies this below an explicit `server.host`,
|
|
622
|
+
# while an explicit `--host` remains the highest-precedence value.
|
|
623
|
+
default_dashboard_host() {
|
|
624
|
+
local ip=""
|
|
625
|
+
if command -v tailscale >/dev/null 2>&1; then
|
|
626
|
+
ip="$(tailscale ip -4 2>/dev/null | grep -m1 -E '^100\.[0-9]+\.[0-9]+\.[0-9]+$' || true)"
|
|
627
|
+
fi
|
|
628
|
+
if [ -n "$ip" ] && [ -n "${AIUR_DASHBOARD_USERNAME:-}" ] && [ -n "${AIUR_DASHBOARD_PASSWORD:-}" ]; then
|
|
629
|
+
printf '%s' "$ip"
|
|
630
|
+
else
|
|
631
|
+
printf '127.0.0.1'
|
|
632
|
+
fi
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
build_run_argv() {
|
|
636
|
+
local mode="$1"
|
|
637
|
+
shift
|
|
638
|
+
|
|
639
|
+
local has_interactive=0 has_headless=0 has_ack=0 arg
|
|
640
|
+
for arg in "$@"; do
|
|
641
|
+
case "$arg" in
|
|
642
|
+
--interactive) has_interactive=1 ;;
|
|
643
|
+
--headless) has_headless=1 ;;
|
|
644
|
+
--i-understand-that-this-will-be-running-without-the-usual-guardrails) has_ack=1 ;;
|
|
645
|
+
esac
|
|
646
|
+
done
|
|
647
|
+
|
|
648
|
+
local injected=()
|
|
649
|
+
if [ "$mode" = "background" ] && [ "$has_interactive" -eq 0 ]; then
|
|
650
|
+
[ "$has_headless" -eq 1 ] || injected+=(--headless)
|
|
651
|
+
else
|
|
652
|
+
[ "$has_interactive" -eq 1 ] || injected+=(--interactive)
|
|
653
|
+
fi
|
|
654
|
+
[ "$has_ack" -eq 1 ] || injected+=(--i-understand-that-this-will-be-running-without-the-usual-guardrails)
|
|
655
|
+
|
|
656
|
+
run_argv=("${injected[@]}" "$@")
|
|
657
|
+
}
|
|
658
|
+
|
|
340
659
|
run_session() {
|
|
341
660
|
local mode="$1"
|
|
342
661
|
shift
|
|
@@ -363,34 +682,25 @@ run_session() {
|
|
|
363
682
|
# Pick up GITHUB_TOKEN / dashboard creds the wizard wrote to ./.env so the
|
|
364
683
|
# running tracker can authenticate. Shell exports still take precedence.
|
|
365
684
|
load_dotenv
|
|
685
|
+
scrub_run_only_env
|
|
686
|
+
export AIUR_DEFAULT_DASHBOARD_HOST="$(default_dashboard_host)"
|
|
366
687
|
|
|
367
688
|
init_argv_file
|
|
368
689
|
|
|
369
|
-
#
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
# (status/agents/message/pause/set).
|
|
374
|
-
#
|
|
375
|
-
|
|
376
|
-
for
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
--i-understand-that-this-will-be-running-without-the-usual-guardrails) has_ack=1 ;;
|
|
382
|
-
esac
|
|
690
|
+
# Supply the flags a bare `aiur` needs: UI mode and the no-guardrails ack.
|
|
691
|
+
# The exported dashboard host fills only a missing config value; an explicit
|
|
692
|
+
# server.host or --host remains authoritative. Foreground runs are
|
|
693
|
+
# interactive; `--bg` runs headless (no panes/chat backfill) and is driven
|
|
694
|
+
# over the control RPC (status/agents/message/pause/set). Dashboard binding is
|
|
695
|
+
# independent: it remains enabled in either mode unless `--no-dashboard` is
|
|
696
|
+
# supplied. `aiur --bg --interactive` opts back into the full terminal stack
|
|
697
|
+
# for an attachable background session.
|
|
698
|
+
build_run_argv "$mode" "$@"
|
|
699
|
+
local no_dashboard=0
|
|
700
|
+
for run_arg in "${run_argv[@]}"; do
|
|
701
|
+
[ "$run_arg" = "--no-dashboard" ] && no_dashboard=1
|
|
383
702
|
done
|
|
384
|
-
|
|
385
|
-
[ "$has_host" -eq 1 ] || injected+=(--host 127.0.0.1)
|
|
386
|
-
if [ "$mode" = "background" ] && [ "$has_interactive" -eq 0 ]; then
|
|
387
|
-
[ "$has_headless" -eq 1 ] || injected+=(--headless)
|
|
388
|
-
else
|
|
389
|
-
[ "$has_interactive" -eq 1 ] || injected+=(--interactive)
|
|
390
|
-
fi
|
|
391
|
-
[ "$has_ack" -eq 1 ] || injected+=(--i-understand-that-this-will-be-running-without-the-usual-guardrails)
|
|
392
|
-
|
|
393
|
-
write_argv "${injected[@]}" "$@"
|
|
703
|
+
write_argv "${run_argv[@]}"
|
|
394
704
|
export AIUR_ARGV_FILE="$argv_file"
|
|
395
705
|
|
|
396
706
|
prepare_distribution
|
|
@@ -441,6 +751,17 @@ run_session() {
|
|
|
441
751
|
export AIUR_LOGS_ROOT
|
|
442
752
|
fi
|
|
443
753
|
|
|
754
|
+
# Capture an erl_crash.dump on daemon BEAM death (#852) so a crash under load
|
|
755
|
+
# is diagnosable instead of vanishing. Written next to the run's aiur.log so it
|
|
756
|
+
# survives the launcher's tempfile cleanup; ERL_CRASH_DUMP_SECONDS bounds the
|
|
757
|
+
# write so a wedged BEAM can't hang the dump indefinitely. Nothing in the
|
|
758
|
+
# release boot disables dumps, and an Executor override of either var is kept.
|
|
759
|
+
# Requires a durable logs root (background run or agent IR sandbox).
|
|
760
|
+
if [ -n "${AIUR_LOGS_ROOT:-}" ]; then
|
|
761
|
+
export ERL_CRASH_DUMP="${ERL_CRASH_DUMP:-$AIUR_LOGS_ROOT/erl_crash.dump}"
|
|
762
|
+
export ERL_CRASH_DUMP_SECONDS="${ERL_CRASH_DUMP_SECONDS:-30}"
|
|
763
|
+
fi
|
|
764
|
+
|
|
444
765
|
# Capture sink for BEAM startup (and, in background mode, the whole run's
|
|
445
766
|
# boot stdout/stderr). Foreground uses a throwaway tempfile; background points
|
|
446
767
|
# at a durable file in the run log dir. The dir/file is created lazily just
|
|
@@ -460,13 +781,15 @@ run_session() {
|
|
|
460
781
|
{
|
|
461
782
|
printf '#!/usr/bin/env bash\n'
|
|
462
783
|
printf 'set -o pipefail\n'
|
|
784
|
+
printf 'unset AIUR_CI_READINESS_TOKEN\n'
|
|
463
785
|
printf 'cd %q || exit 1\n' "$PWD"
|
|
464
786
|
local v
|
|
465
787
|
for v in AIUR_RELEASE_DIR AIUR_ARGV_FILE RELEASE_DISTRIBUTION RELEASE_NODE \
|
|
466
788
|
RELEASE_COOKIE ERL_AFLAGS ERL_EPMD_ADDRESS AIUR_NODE AIUR_ERLANG_COOKIE \
|
|
467
789
|
AIUR_TMUX_SESSION AIUR_TMUX_SOCKET AIUR_TMUX_CONF AIUR_BIN \
|
|
468
790
|
AIUR_SESSION_TMPFILE AIUR_AGENT_TMPFILE AIUR_WORKSPACE_ROOT_FILE \
|
|
469
|
-
ELIXIR_ERL_OPTIONS AIUR_LOGS_ROOT AIUR_OPENCODE_BRIDGE_PORT AIUR_DEBUG
|
|
791
|
+
ELIXIR_ERL_OPTIONS AIUR_LOGS_ROOT AIUR_OPENCODE_BRIDGE_PORT AIUR_DEFAULT_DASHBOARD_HOST AIUR_DEBUG \
|
|
792
|
+
AIUR_OPERATOR_PID AIUR_NOFILE_SOFT_LIMIT ERL_CRASH_DUMP ERL_CRASH_DUMP_SECONDS; do
|
|
470
793
|
if [ -n "${!v:-}" ]; then printf 'export %s=%q\n' "$v" "${!v}"; fi
|
|
471
794
|
done
|
|
472
795
|
printf 'capture=%q\n' "$startup_capture"
|
|
@@ -493,7 +816,10 @@ run_session() {
|
|
|
493
816
|
# session whose BEAM/control plane is gone should be reclaimed before retry.
|
|
494
817
|
if [ "$mode" = "background" ] && "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
495
818
|
if [ "$(probe_control_liveness)" = "up" ]; then
|
|
496
|
-
|
|
819
|
+
# Write a record only when none exists. A live session's own start already
|
|
820
|
+
# wrote one — and it owns the BEAM-written AIUR_RECORD_WORKSPACE_ROOT_FILE
|
|
821
|
+
# path, which must not be overwritten by this invocation's temp file.
|
|
822
|
+
[ -f "$(aiur_instance_record_path)" ] || (AIUR_WORKSPACE_ROOT_FILE="" write_aiur_instance_record "$session" "$socket")
|
|
497
823
|
echo "aiur is already running in the background (tmux session ${session})." >&2
|
|
498
824
|
echo "Use: aiur status # inspect agents" >&2
|
|
499
825
|
echo "Use: aiur stop # stop it before starting a fresh session" >&2
|
|
@@ -561,6 +887,7 @@ run_session() {
|
|
|
561
887
|
fi
|
|
562
888
|
|
|
563
889
|
write_aiur_instance_record "$session" "$socket"
|
|
890
|
+
print_dashboard_status "$no_dashboard" "$startup_capture"
|
|
564
891
|
|
|
565
892
|
if [ "$mode" = "foreground" ]; then
|
|
566
893
|
echo "aiur foreground tmux socket ${socket}, session ${session}" >&2
|
|
@@ -902,7 +1229,7 @@ agent_pid_matches() {
|
|
|
902
1229
|
#
|
|
903
1230
|
# tmux runs on aiur's PRIVATE socket (-L aiur-$USER), so kill-server tears down
|
|
904
1231
|
# every REPL/chat pane agent in one shot AND leaves no live aiur tmux server —
|
|
905
|
-
# never touching the
|
|
1232
|
+
# never touching the Executor’s own default tmux. Headless agents (claude/codex
|
|
906
1233
|
# app-servers spawned via Port) are bare OS processes that reparent to init on a
|
|
907
1234
|
# BEAM crash; kill-server can't see them, so they're reaped from the pidfile by
|
|
908
1235
|
# process tree, comm-guarded against pid reuse. Idempotent.
|
|
@@ -969,6 +1296,7 @@ write_aiur_instance_record() {
|
|
|
969
1296
|
printf 'AIUR_RECORD_INSTANCE_KEY=%q\n' "$AIUR_INSTANCE_KEY"
|
|
970
1297
|
printf 'AIUR_RECORD_SESSION=%q\n' "$session"
|
|
971
1298
|
printf 'AIUR_RECORD_SOCKET=%q\n' "$socket"
|
|
1299
|
+
printf 'AIUR_RECORD_WORKSPACE_ROOT_FILE=%q\n' "${AIUR_WORKSPACE_ROOT_FILE:-}"
|
|
972
1300
|
printf 'AIUR_RECORD_PROJECT_ROOT=%q\n' "$root"
|
|
973
1301
|
printf 'AIUR_RECORD_PROJECT_ROOT_SOURCE=%q\n' "${AIUR_PROJECT_ROOT_SOURCE:-}"
|
|
974
1302
|
printf 'AIUR_RECORD_WRITTEN_AT=%q\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
@@ -1121,24 +1449,10 @@ reap_workspace_cwd_from_file() {
|
|
|
1121
1449
|
reap_workspace_cwd_agents "$root"
|
|
1122
1450
|
}
|
|
1123
1451
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
local output status line
|
|
1129
|
-
set +e
|
|
1130
|
-
output="$("$release_bin" rpc "IO.puts(Aiur.Config.workspace_root())" 2>/dev/null)"
|
|
1131
|
-
status=$?
|
|
1132
|
-
set -e
|
|
1133
|
-
[ "$status" -eq 0 ] || return 1
|
|
1134
|
-
|
|
1135
|
-
while IFS= read -r line; do
|
|
1136
|
-
case "$line" in
|
|
1137
|
-
"" | :ok) continue ;;
|
|
1138
|
-
*) printf '%s\n' "$line"; return 0 ;;
|
|
1139
|
-
esac
|
|
1140
|
-
done <<<"$output"
|
|
1141
|
-
return 1
|
|
1452
|
+
workspace_root_file_from_instance_record() {
|
|
1453
|
+
load_aiur_instance_record "$(aiur_instance_record_path)" || return 1
|
|
1454
|
+
[ -n "${AIUR_RECORD_WORKSPACE_ROOT_FILE:-}" ] || return 1
|
|
1455
|
+
printf '%s\n' "$AIUR_RECORD_WORKSPACE_ROOT_FILE"
|
|
1142
1456
|
}
|
|
1143
1457
|
|
|
1144
1458
|
# Background watchdog that survives the BEAM. Polls for the release BEAM by
|
|
@@ -1189,7 +1503,10 @@ start_beam_death_watchdog() {
|
|
|
1189
1503
|
|
|
1190
1504
|
probe_control_liveness() {
|
|
1191
1505
|
local expression output status
|
|
1192
|
-
|
|
1506
|
+
# The Orchestrator starts before the dashboard child, so an answering status
|
|
1507
|
+
# call alone does not prove startup (or the bind attempt) finished. Wait until
|
|
1508
|
+
# OTP marks :aiur started before printing the effective dashboard status.
|
|
1509
|
+
expression='aiur_started = Enum.any?(Application.started_applications(), fn {app, _description, _version} -> app == :aiur end); case {Process.whereis(Aiur.Orchestrator), aiur_started} do {pid, true} when is_pid(pid) -> case Aiur.Orchestrator.status(Aiur.Orchestrator, 100) do statuses when is_list(statuses) -> IO.puts("__AIUR_CONTROL_READY__"); _ -> IO.puts("__AIUR_CONTROL_NOT_READY__") end; _ -> IO.puts("__AIUR_CONTROL_NOT_READY__") end'
|
|
1193
1510
|
|
|
1194
1511
|
set +e
|
|
1195
1512
|
output="$("$release_bin" rpc "$expression" 2>&1)"
|
|
@@ -1203,11 +1520,51 @@ probe_control_liveness() {
|
|
|
1203
1520
|
fi
|
|
1204
1521
|
}
|
|
1205
1522
|
|
|
1523
|
+
# Return the useful URL and effective bind host/port only when the running node
|
|
1524
|
+
# confirms that Bandit actually bound a listener. An empty result means the
|
|
1525
|
+
# listener was suppressed or refused; configured server.port alone is never
|
|
1526
|
+
# proof of service.
|
|
1527
|
+
probe_dashboard_status() {
|
|
1528
|
+
local expression output status marker="__AIUR_DASHBOARD_STATUS__:"
|
|
1529
|
+
expression='case {Aiur.HttpServer.base_url(), Aiur.Config.server_host(), Aiur.HttpServer.bound_port()} do {url, host, port} when is_binary(url) and is_binary(host) and is_integer(port) -> IO.puts("__AIUR_DASHBOARD_STATUS__:Dashboard: " <> url <> " (bind host=" <> host <> ", port=" <> Integer.to_string(port) <> ")"); _ -> :ok end'
|
|
1530
|
+
|
|
1531
|
+
set +e
|
|
1532
|
+
run_release_rpc_with_timeout "$expression"
|
|
1533
|
+
status=$?
|
|
1534
|
+
set -e
|
|
1535
|
+
output="$AIUR_CONTROL_RPC_OUTPUT"
|
|
1536
|
+
|
|
1537
|
+
if [ "$status" -eq 0 ] && [[ "$output" == *"$marker"* ]]; then
|
|
1538
|
+
output="${output#*"$marker"}"
|
|
1539
|
+
printf '%s' "${output%%$'\n'*}"
|
|
1540
|
+
fi
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
print_dashboard_status() {
|
|
1544
|
+
local no_dashboard="$1" startup_capture="$2" dashboard_status
|
|
1545
|
+
|
|
1546
|
+
if [ "$no_dashboard" -eq 1 ]; then
|
|
1547
|
+
echo "Dashboard disabled by --no-dashboard." >&2
|
|
1548
|
+
return
|
|
1549
|
+
fi
|
|
1550
|
+
|
|
1551
|
+
dashboard_status="$(probe_dashboard_status)"
|
|
1552
|
+
if [ -n "$dashboard_status" ]; then
|
|
1553
|
+
echo "${dashboard_status}" >&2
|
|
1554
|
+
else
|
|
1555
|
+
echo "⚠️ dashboard listener unavailable; inspect ${startup_capture} for bind or authentication refusal." >&2
|
|
1556
|
+
fi
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1206
1559
|
wait_for_session_startup() {
|
|
1207
1560
|
local tmux_bin="$1" socket="$2" conf="$3" session="$4" startup_capture="$5" require_control="$6"
|
|
1208
1561
|
local max_ticks="${AIUR_TMUX_GRACE_TICKS:-30}" tick control_state
|
|
1209
1562
|
if [ "$require_control" = "1" ]; then
|
|
1210
|
-
|
|
1563
|
+
# 0.1s per tick. The old 100-tick (10s) budget was under a cold control
|
|
1564
|
+
# plane's real boot time, so a healthy daemon was killed mid-startup and
|
|
1565
|
+
# reported as a failure. Boot time grows with the module count; keep the
|
|
1566
|
+
# headroom generous, since the loop exits as soon as the probe says "up".
|
|
1567
|
+
max_ticks="${AIUR_NODE_GRACE_TICKS:-1200}"
|
|
1211
1568
|
fi
|
|
1212
1569
|
|
|
1213
1570
|
for ((tick = 0; tick < max_ticks; tick++)); do
|
|
@@ -1231,7 +1588,16 @@ wait_for_session_startup() {
|
|
|
1231
1588
|
done
|
|
1232
1589
|
|
|
1233
1590
|
if [ "$require_control" = "1" ]; then
|
|
1234
|
-
|
|
1591
|
+
# A live session at this point means the BEAM never died -- we ran out of
|
|
1592
|
+
# patience, not the daemon out of health. Saying so is the difference
|
|
1593
|
+
# between "your release is broken" and "give it longer", and the empty
|
|
1594
|
+
# capture below is evidence of the latter, not of a silent crash.
|
|
1595
|
+
if "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
1596
|
+
echo "❌ aiur control plane at ${RELEASE_NODE:-unknown} was still booting after $(awk "BEGIN{printf \"%.0f\", ${max_ticks} / 10}")s; the BEAM is alive but not yet answering." >&2
|
|
1597
|
+
echo " Raise the budget with AIUR_NODE_GRACE_TICKS (ticks of 0.1s) if this box is just slow." >&2
|
|
1598
|
+
else
|
|
1599
|
+
echo "❌ aiur control plane did not become ready at ${RELEASE_NODE:-unknown} during startup; captured output:" >&2
|
|
1600
|
+
fi
|
|
1235
1601
|
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
1236
1602
|
return 1
|
|
1237
1603
|
fi
|
|
@@ -1364,6 +1730,7 @@ load_aiur_instance_record() {
|
|
|
1364
1730
|
AIUR_RECORD_INSTANCE_KEY=""
|
|
1365
1731
|
AIUR_RECORD_SESSION=""
|
|
1366
1732
|
AIUR_RECORD_SOCKET=""
|
|
1733
|
+
AIUR_RECORD_WORKSPACE_ROOT_FILE=""
|
|
1367
1734
|
AIUR_RECORD_PROJECT_ROOT=""
|
|
1368
1735
|
AIUR_RECORD_PROJECT_ROOT_SOURCE=""
|
|
1369
1736
|
[ -r "$file" ] || return 1
|
|
@@ -1445,15 +1812,14 @@ $root
|
|
|
1445
1812
|
}
|
|
1446
1813
|
|
|
1447
1814
|
print_global_config_control_hint() {
|
|
1815
|
+
[ -n "${AIUR_CONTROL_HINT_ROOTS:-}" ] || return 0
|
|
1448
1816
|
[ "${AIUR_CONTROL_CALLER_ROOT_SOURCE:-}" = "cwd" ] || return 0
|
|
1449
1817
|
[ "${AIUR_CONTROL_CURRENT_NODE_STATE:-}" = "down" ] || return 0
|
|
1450
1818
|
|
|
1451
1819
|
echo "aiur: global-config control identity is keyed by cwd ${AIUR_CONTROL_CALLER_ROOT:-${AIUR_PROJECT_ROOT:-unknown}}" >&2
|
|
1452
1820
|
echo "aiur: run control commands from the launch directory, or from a subdirectory of that launch directory" >&2
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
printf '%s' "$AIUR_CONTROL_HINT_ROOTS" | sed '/^$/d; s/^/ /' >&2
|
|
1456
|
-
fi
|
|
1821
|
+
echo "aiur: live launch directory candidate(s):" >&2
|
|
1822
|
+
printf '%s' "$AIUR_CONTROL_HINT_ROOTS" | sed '/^$/d; s/^/ /' >&2
|
|
1457
1823
|
}
|
|
1458
1824
|
|
|
1459
1825
|
control_rpc_timeout_seconds() {
|
|
@@ -1464,20 +1830,41 @@ control_rpc_timeout_seconds() {
|
|
|
1464
1830
|
printf '%s' "$seconds"
|
|
1465
1831
|
}
|
|
1466
1832
|
|
|
1833
|
+
print_not_running_message() {
|
|
1834
|
+
echo "error: aiur is not running. Start it with \`aiurdev run\` (or \`aiurdev --bg\`), then retry." >&2
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
print_control_down_message() {
|
|
1838
|
+
local crash_marker
|
|
1839
|
+
crash_marker="$(aiur_crash_marker_path)"
|
|
1840
|
+
if [ -f "$crash_marker" ]; then
|
|
1841
|
+
echo "aiur: background daemon at ${RELEASE_NODE} is DOWN after an unexpected exit; agents may be orphaned" >&2
|
|
1842
|
+
sed 's/^/ /' "$crash_marker" >&2 2>/dev/null || true
|
|
1843
|
+
echo "aiur: run 'aiur stop' to reap any orphaned agents, then start aiur again" >&2
|
|
1844
|
+
else
|
|
1845
|
+
print_not_running_message
|
|
1846
|
+
print_global_config_control_hint
|
|
1847
|
+
fi
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1467
1850
|
kill_control_rpc_process() {
|
|
1468
1851
|
local pid="$1" grouped="$2" p tree=()
|
|
1469
1852
|
[ -n "$pid" ] || return 0
|
|
1470
1853
|
|
|
1854
|
+
# Snapshot descendants before signalling the wrapper. A release launcher may
|
|
1855
|
+
# fork its rpc BEAM into another process group and then exit; group signalling
|
|
1856
|
+
# alone would orphan that child and lose the only relationship we can reap.
|
|
1857
|
+
while IFS= read -r p; do tree+=("$p"); done < <(agent_pid_tree "$pid")
|
|
1858
|
+
|
|
1471
1859
|
if [ "$grouped" = "1" ]; then
|
|
1472
1860
|
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
1473
|
-
sleep 0.2
|
|
1474
|
-
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
|
1475
|
-
return 0
|
|
1476
1861
|
fi
|
|
1477
1862
|
|
|
1478
|
-
while IFS= read -r p; do tree+=("$p"); done < <(agent_pid_tree "$pid")
|
|
1479
1863
|
for p in "${tree[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
1480
1864
|
sleep 0.2
|
|
1865
|
+
if [ "$grouped" = "1" ]; then
|
|
1866
|
+
kill -KILL "-$pid" 2>/dev/null || true
|
|
1867
|
+
fi
|
|
1481
1868
|
for p in "${tree[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
1482
1869
|
}
|
|
1483
1870
|
|
|
@@ -1510,7 +1897,14 @@ run_release_rpc_with_timeout() {
|
|
|
1510
1897
|
else
|
|
1511
1898
|
status=$?
|
|
1512
1899
|
fi
|
|
1513
|
-
|
|
1900
|
+
if [ -f "$timeout_file" ]; then
|
|
1901
|
+
# The timeout watchdog owns descendant cleanup. Once it has marked the
|
|
1902
|
+
# timeout, let it finish its TERM/KILL sequence; cancelling it here can
|
|
1903
|
+
# strand descendants that ignored TERM after the root process exits.
|
|
1904
|
+
:
|
|
1905
|
+
else
|
|
1906
|
+
kill "$watchdog_pid" 2>/dev/null || true
|
|
1907
|
+
fi
|
|
1514
1908
|
wait "$watchdog_pid" 2>/dev/null || true
|
|
1515
1909
|
|
|
1516
1910
|
AIUR_CONTROL_RPC_OUTPUT="$(cat "$output_file" 2>/dev/null || true)"
|
|
@@ -1525,19 +1919,59 @@ run_release_rpc_with_timeout() {
|
|
|
1525
1919
|
return "$status"
|
|
1526
1920
|
}
|
|
1527
1921
|
|
|
1922
|
+
control_command_label() {
|
|
1923
|
+
printf '%s' "${AIUR_CONTROL_COMMAND:-control rpc}"
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
# Every line a control RPC surfaces to the operator routes through one of these
|
|
1927
|
+
# two helpers, so `run_control_rpc` can prove it never returns non-zero while
|
|
1928
|
+
# saying nothing (#1684): a silent failure is indistinguishable from a healthy
|
|
1929
|
+
# idle fleet, and nobody reads exit codes interactively.
|
|
1930
|
+
control_rpc_say() {
|
|
1931
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
1932
|
+
printf '%s\n' "$1" >&2
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
control_rpc_echo_output() {
|
|
1936
|
+
[ -n "$1" ] || return 0
|
|
1937
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
1938
|
+
printf '%s\n' "$1" >&2
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1528
1941
|
# RPC an expression into the running node. The control CLI prints a trailing
|
|
1529
1942
|
# `__AIUR_CONTROL_EXIT__:<code>` marker we translate into the process exit code.
|
|
1943
|
+
#
|
|
1944
|
+
# Wrapper contract: a non-zero return ALWAYS carries at least one stderr line
|
|
1945
|
+
# naming the command that failed. EX_TEMPFAIL (75) is exempt — it is the dev
|
|
1946
|
+
# shim's internal rebuild-and-retry signal, never an operator-visible outcome.
|
|
1530
1947
|
run_control_rpc() {
|
|
1948
|
+
local status=0
|
|
1949
|
+
|
|
1950
|
+
AIUR_CONTROL_RPC_DIAGNOSED=0
|
|
1951
|
+
run_control_rpc_dispatch "$@" || status=$?
|
|
1952
|
+
|
|
1953
|
+
if [ "$status" -ne 0 ] && [ "$status" -ne 75 ] && [ "${AIUR_CONTROL_RPC_DIAGNOSED:-0}" -ne 1 ]; then
|
|
1954
|
+
if [ "$status" -eq 124 ]; then
|
|
1955
|
+
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE:-the daemon} timed out after $(control_rpc_timeout_seconds)s; outcome is unknown. The daemon did not reply within the budget - commonly one blocked process inside it, not host load. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
1956
|
+
else
|
|
1957
|
+
control_rpc_say "aiur: $(control_command_label) failed (exit ${status}) and produced no diagnostic output; outcome is unknown. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
1958
|
+
fi
|
|
1959
|
+
fi
|
|
1960
|
+
|
|
1961
|
+
return "$status"
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
run_control_rpc_dispatch() {
|
|
1531
1965
|
local expression="$1"
|
|
1532
|
-
resolve_release
|
|
1966
|
+
resolve_release || return $?
|
|
1533
1967
|
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
1534
1968
|
resolve_control_identity_from_records
|
|
1535
1969
|
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -eq 1 ]; then
|
|
1536
1970
|
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
1537
1971
|
fi
|
|
1538
1972
|
|
|
1539
|
-
local marker="__AIUR_CONTROL_EXIT__:"
|
|
1540
|
-
local output status exit_code=0 saw_marker=0 line
|
|
1973
|
+
local marker="__AIUR_CONTROL_EXIT__:" error_marker="__AIUR_CONTROL_ERROR__:"
|
|
1974
|
+
local output status exit_code=0 saw_marker=0 saw_error=0 saw_output=0 line partial_suffix=""
|
|
1541
1975
|
|
|
1542
1976
|
set +e
|
|
1543
1977
|
run_release_rpc_with_timeout "$expression"
|
|
@@ -1545,9 +1979,15 @@ run_control_rpc() {
|
|
|
1545
1979
|
output="$AIUR_CONTROL_RPC_OUTPUT"
|
|
1546
1980
|
set -e
|
|
1547
1981
|
|
|
1982
|
+
if [ "$status" -ne 0 ] && [ "${AIUR_CONTROL_RELEASE_RETRYABLE:-0}" = "1" ] && \
|
|
1983
|
+
{ [ ! -x "$release_bin" ] || [ ! -x "$vsn_dir/elixir" ] || [ ! -r "$release_dir/releases/start_erl.data" ]; }; then
|
|
1984
|
+
control_release_retry
|
|
1985
|
+
return $?
|
|
1986
|
+
fi
|
|
1987
|
+
|
|
1548
1988
|
if [ "${AIUR_CONTROL_RPC_TIMED_OUT:-0}" -eq 1 ]; then
|
|
1549
|
-
[ -n "$output" ] &&
|
|
1550
|
-
|
|
1989
|
+
[ -n "$output" ] && partial_suffix="; partial output was discarded"
|
|
1990
|
+
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE} timed out after $(control_rpc_timeout_seconds)s; outcome is unknown${partial_suffix}. Commonly one blocked process inside the daemon, not host load."
|
|
1551
1991
|
return 124
|
|
1552
1992
|
fi
|
|
1553
1993
|
|
|
@@ -1560,26 +2000,31 @@ run_control_rpc() {
|
|
|
1560
2000
|
# node epmd confirms is down earns the friendly "start aiur" hint; an `up`
|
|
1561
2001
|
# node failed for a real reason, and an `unknown` probe must not be assumed
|
|
1562
2002
|
# down — in both of those cases surface the actual rpc output, never mask it.
|
|
2003
|
+
#
|
|
2004
|
+
# An empty buffer is its own case: `--rpc-eval` kills itself without a word
|
|
2005
|
+
# when the evaluated expression exits (a GenServer call timing out against a
|
|
2006
|
+
# saturated daemon does exactly that), so "see the error above" would point
|
|
2007
|
+
# at nothing (#1684). Name the failure instead.
|
|
1563
2008
|
case "$(probe_node_liveness)" in
|
|
1564
2009
|
down)
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
if [ -f "$crash_marker" ]; then
|
|
1568
|
-
echo "aiur: background daemon at ${RELEASE_NODE} is DOWN after an unexpected exit; agents may be orphaned" >&2
|
|
1569
|
-
sed 's/^/ /' "$crash_marker" >&2 2>/dev/null || true
|
|
1570
|
-
echo "aiur: run 'aiur stop' to reap any orphaned agents, then start aiur again" >&2
|
|
1571
|
-
else
|
|
1572
|
-
echo "aiur: no running aiur node at ${RELEASE_NODE}; start aiur and try again" >&2
|
|
1573
|
-
print_global_config_control_hint
|
|
1574
|
-
fi
|
|
2010
|
+
print_control_down_message
|
|
2011
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
1575
2012
|
;;
|
|
1576
2013
|
up)
|
|
1577
|
-
|
|
1578
|
-
|
|
2014
|
+
control_rpc_echo_output "$output"
|
|
2015
|
+
if [ -n "$output" ]; then
|
|
2016
|
+
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} (node is running); see the error above"
|
|
2017
|
+
else
|
|
2018
|
+
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} with no diagnostic output (node is running); outcome is unknown. 'aiur alerts' is answered by a different process and can confirm the daemon is alive."
|
|
2019
|
+
fi
|
|
1579
2020
|
;;
|
|
1580
2021
|
*)
|
|
1581
|
-
|
|
1582
|
-
|
|
2022
|
+
control_rpc_echo_output "$output"
|
|
2023
|
+
if [ -n "$output" ]; then
|
|
2024
|
+
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} (could not query epmd to confirm node state); see the error above"
|
|
2025
|
+
else
|
|
2026
|
+
control_rpc_say "aiur: $(control_command_label) failed against ${RELEASE_NODE} with no output (could not query epmd to confirm node state)"
|
|
2027
|
+
fi
|
|
1583
2028
|
;;
|
|
1584
2029
|
esac
|
|
1585
2030
|
return 1
|
|
@@ -1591,19 +2036,55 @@ run_control_rpc() {
|
|
|
1591
2036
|
saw_marker=1
|
|
1592
2037
|
exit_code="${line#"$marker"}"
|
|
1593
2038
|
;;
|
|
2039
|
+
"$error_marker"*)
|
|
2040
|
+
saw_error=1
|
|
2041
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2042
|
+
printf '%s\n' "${line#"$error_marker"}" >&2
|
|
2043
|
+
;;
|
|
1594
2044
|
:ok | "") ;;
|
|
1595
|
-
*)
|
|
2045
|
+
*)
|
|
2046
|
+
saw_output=1
|
|
2047
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2048
|
+
printf '%s\n' "$line"
|
|
2049
|
+
;;
|
|
1596
2050
|
esac
|
|
1597
2051
|
done <<<"$output"
|
|
1598
2052
|
|
|
1599
2053
|
if [ "$saw_marker" -ne 1 ]; then
|
|
1600
|
-
|
|
2054
|
+
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE} returned no exit marker; command output may be incomplete"
|
|
1601
2055
|
return 1
|
|
1602
2056
|
fi
|
|
1603
2057
|
|
|
2058
|
+
if [ "$exit_code" -ne 0 ] && [ "$saw_error" -ne 1 ] && [ "$saw_output" -ne 1 ]; then
|
|
2059
|
+
control_rpc_say "aiur: $(control_command_label) failed with exit ${exit_code} and returned no diagnostic output"
|
|
2060
|
+
fi
|
|
2061
|
+
|
|
1604
2062
|
return "$exit_code"
|
|
1605
2063
|
}
|
|
1606
2064
|
|
|
2065
|
+
# A streaming Executor listener must retain the RPC process and its stdout. It
|
|
2066
|
+
# intentionally bypasses the one-shot control timeout and exit-marker wrapper.
|
|
2067
|
+
run_control_stream() {
|
|
2068
|
+
local expression="$1"
|
|
2069
|
+
resolve_release || return $?
|
|
2070
|
+
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
2071
|
+
resolve_control_identity_from_records
|
|
2072
|
+
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -eq 1 ]; then
|
|
2073
|
+
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
2074
|
+
fi
|
|
2075
|
+
if [ "$(probe_node_liveness)" = "down" ]; then
|
|
2076
|
+
print_control_down_message
|
|
2077
|
+
return 1
|
|
2078
|
+
fi
|
|
2079
|
+
if "$release_bin" rpc "$expression"; then
|
|
2080
|
+
return 0
|
|
2081
|
+
else
|
|
2082
|
+
local status=$?
|
|
2083
|
+
echo "aiur: streaming control RPC failed with exit ${status}" >&2
|
|
2084
|
+
return "$status"
|
|
2085
|
+
fi
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1607
2088
|
elixir_list_literal() {
|
|
1608
2089
|
local first=1 item
|
|
1609
2090
|
printf '['
|
|
@@ -1647,11 +2128,26 @@ cmd_status() {
|
|
|
1647
2128
|
run_control_rpc "Aiur.AgentControlCLI.status()"
|
|
1648
2129
|
}
|
|
1649
2130
|
|
|
2131
|
+
# `aiur usage` — Codex/Claude limit headroom from the daemon's meter
|
|
2132
|
+
# projection, each value carrying the age of its observation.
|
|
2133
|
+
cmd_usage() {
|
|
2134
|
+
[ "$#" -eq 0 ] || die "usage does not accept arguments"
|
|
2135
|
+
run_control_rpc "Aiur.AgentControlCLI.usage()"
|
|
2136
|
+
}
|
|
2137
|
+
|
|
1650
2138
|
cmd_pause_resume() {
|
|
1651
2139
|
local command="$1"
|
|
1652
2140
|
shift
|
|
2141
|
+
|
|
2142
|
+
# Bare `aiur pause` / `aiur resume` (no IDs, no --all) flips the single
|
|
2143
|
+
# global pause switch: a daemon-wide halt distinct from per-agent pause.
|
|
2144
|
+
if [ "$#" -eq 0 ]; then
|
|
2145
|
+
run_control_rpc "Aiur.AgentControlCLI.${command}_global()"
|
|
2146
|
+
return
|
|
2147
|
+
fi
|
|
2148
|
+
|
|
1653
2149
|
if ! parse_issue_targets "$@"; then
|
|
1654
|
-
echo "aiur: $command expects issue IDs or --all (e.g. aiur $command 44 45,46; aiur $command --all)" >&2
|
|
2150
|
+
echo "aiur: $command expects issue IDs or --all (e.g. aiur $command 44 45,46; aiur $command --all; or bare aiur $command for the global switch)" >&2
|
|
1655
2151
|
exit 64
|
|
1656
2152
|
fi
|
|
1657
2153
|
|
|
@@ -1665,7 +2161,28 @@ cmd_pause_resume() {
|
|
|
1665
2161
|
run_control_rpc "$expression"
|
|
1666
2162
|
}
|
|
1667
2163
|
|
|
1668
|
-
# `aiur
|
|
2164
|
+
# `aiur reset-budget <id>...` — clear the lifetime dispatch latch for one or
|
|
2165
|
+
# more tickets (the supported exit from the #1453 latch; no JSON hand-editing).
|
|
2166
|
+
cmd_reset_budget() {
|
|
2167
|
+
if ! parse_issue_targets "$@"; then
|
|
2168
|
+
echo "aiur: reset-budget expects issue IDs (e.g. aiur reset-budget 44 45,46)" >&2
|
|
2169
|
+
exit 64
|
|
2170
|
+
fi
|
|
2171
|
+
|
|
2172
|
+
# --all is rejected (exit 64 with guidance) rather than silently no-opping:
|
|
2173
|
+
# clearing every ticket's latch at once is not a documented operation and
|
|
2174
|
+
# would mask which tickets are structurally stuck (#1453 review P2d).
|
|
2175
|
+
if [ "$parsed_all" -eq 1 ]; then
|
|
2176
|
+
echo "aiur: reset-budget does not accept --all; name ticket IDs explicitly (e.g. aiur reset-budget 44 45,46)" >&2
|
|
2177
|
+
exit 64
|
|
2178
|
+
fi
|
|
2179
|
+
|
|
2180
|
+
local expression
|
|
2181
|
+
expression="Aiur.AgentControlCLI.reset_budget($(elixir_list_literal "${parsed_targets[@]}"))"
|
|
2182
|
+
run_control_rpc "$expression"
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
# `aiur message <issue> <text>` — deliver Executor text to one running agent.
|
|
1669
2186
|
# The text is base64-encoded for the RPC hop so arbitrary content (quotes,
|
|
1670
2187
|
# backslashes, `#{}`, newlines) survives without Elixir-string escaping.
|
|
1671
2188
|
cmd_message() {
|
|
@@ -1696,8 +2213,268 @@ cmd_agents() {
|
|
|
1696
2213
|
run_control_rpc "Aiur.AgentControlCLI.agents()"
|
|
1697
2214
|
}
|
|
1698
2215
|
|
|
2216
|
+
# `aiur commands` — read the dashboard's retained Decision projection without
|
|
2217
|
+
# exposing any dispatch or answer mutation path.
|
|
2218
|
+
cmd_commands() {
|
|
2219
|
+
local filter="all" blocking=0 json=0 decision_id="" ticket="" search="" cursor="" limit="" arg
|
|
2220
|
+
|
|
2221
|
+
while [ "$#" -gt 0 ]; do
|
|
2222
|
+
arg="$1"
|
|
2223
|
+
case "$arg" in
|
|
2224
|
+
--filter) [ "$#" -gt 1 ] || { echo "aiur: commands --filter requires a value" >&2; exit 64; }; shift; filter="$1" ;;
|
|
2225
|
+
--filter=*) filter="${arg#--filter=}" ;;
|
|
2226
|
+
--blocking) blocking=1 ;;
|
|
2227
|
+
--ticket) [ "$#" -gt 1 ] || { echo "aiur: commands --ticket requires a value" >&2; exit 64; }; shift; ticket="$1" ;;
|
|
2228
|
+
--ticket=*) ticket="${arg#--ticket=}" ;;
|
|
2229
|
+
--search) [ "$#" -gt 1 ] || { echo "aiur: commands --search requires a value" >&2; exit 64; }; shift; search="$1" ;;
|
|
2230
|
+
--search=*) search="${arg#--search=}" ;;
|
|
2231
|
+
--cursor) [ "$#" -gt 1 ] || { echo "aiur: commands --cursor requires a value" >&2; exit 64; }; shift; cursor="$1" ;;
|
|
2232
|
+
--cursor=*) cursor="${arg#--cursor=}" ;;
|
|
2233
|
+
--limit) [ "$#" -gt 1 ] || { echo "aiur: commands --limit requires a value" >&2; exit 64; }; shift; limit="$1" ;;
|
|
2234
|
+
--limit=*) limit="${arg#--limit=}" ;;
|
|
2235
|
+
--json) json=1 ;;
|
|
2236
|
+
-*) echo "aiur: commands received an unknown option: $arg" >&2; exit 64 ;;
|
|
2237
|
+
*)
|
|
2238
|
+
if [ -n "$decision_id" ]; then
|
|
2239
|
+
echo "aiur: commands accepts at most one decision ID" >&2
|
|
2240
|
+
exit 64
|
|
2241
|
+
fi
|
|
2242
|
+
decision_id="$arg"
|
|
2243
|
+
;;
|
|
2244
|
+
esac
|
|
2245
|
+
shift
|
|
2246
|
+
done
|
|
2247
|
+
|
|
2248
|
+
case "$filter" in all|open|blocking|resolved) ;; *) echo "aiur: commands --filter accepts all, open, blocking, or resolved" >&2; exit 64 ;; esac
|
|
2249
|
+
[ -z "$limit" ] || [[ "$limit" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: commands --limit expects a positive integer" >&2; exit 64; }
|
|
2250
|
+
[ -z "$ticket" ] || [ "$filter" = "all" ] || { echo "aiur: commands --ticket requires --filter all" >&2; exit 64; }
|
|
2251
|
+
[ -z "$search" ] || [ "$filter" = "all" ] || { echo "aiur: commands --search requires --filter all" >&2; exit 64; }
|
|
2252
|
+
|
|
2253
|
+
local opts="filter: :$filter"
|
|
2254
|
+
[ "$blocking" -eq 1 ] && opts="$opts, blocking: true"
|
|
2255
|
+
[ "$json" -eq 1 ] && opts="$opts, json: true"
|
|
2256
|
+
[ -n "$limit" ] && opts="$opts, limit: $limit"
|
|
2257
|
+
local key raw encoded
|
|
2258
|
+
for key in decision_id ticket search cursor; do
|
|
2259
|
+
raw="${!key}"
|
|
2260
|
+
[ -n "$raw" ] || continue
|
|
2261
|
+
encoded="$(printf '%s' "$raw" | base64 | tr -d '\n')"
|
|
2262
|
+
opts="$opts, $key: Base.decode64!(\"$encoded\")"
|
|
2263
|
+
done
|
|
2264
|
+
|
|
2265
|
+
run_control_rpc "Aiur.AgentControlCLI.commands([$opts])"
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
encode_control_value() {
|
|
2269
|
+
printf '%s' "$1" | base64 | tr -d '\n'
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
# These are explicit trusted-Executor mutations, deliberately separate from
|
|
2273
|
+
# the read-only `commands` catalog. Revisions remain dashboard-owned.
|
|
2274
|
+
cmd_executor_answer() {
|
|
2275
|
+
local decision_id="${1:-}" expected_version="" option_id="" custom_response="" rationale="" idempotency_key="" executor_id="aiur-cli" arg
|
|
2276
|
+
if [ -z "$decision_id" ] || [[ "$decision_id" = -* ]]; then
|
|
2277
|
+
echo "aiur: executor-answer expects exactly one decision ID" >&2
|
|
2278
|
+
exit 64
|
|
2279
|
+
fi
|
|
2280
|
+
shift
|
|
2281
|
+
|
|
2282
|
+
while [ "$#" -gt 0 ]; do
|
|
2283
|
+
arg="$1"
|
|
2284
|
+
case "$arg" in
|
|
2285
|
+
--expected-version) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --expected-version requires a value" >&2; exit 64; }; shift; expected_version="$1" ;;
|
|
2286
|
+
--expected-version=*) expected_version="${arg#--expected-version=}" ;;
|
|
2287
|
+
--option) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --option requires a value" >&2; exit 64; }; shift; option_id="$1" ;;
|
|
2288
|
+
--option=*) option_id="${arg#--option=}" ;;
|
|
2289
|
+
--custom-response) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --custom-response requires a value" >&2; exit 64; }; shift; custom_response="$1" ;;
|
|
2290
|
+
--custom-response=*) custom_response="${arg#--custom-response=}" ;;
|
|
2291
|
+
--rationale) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --rationale requires a value" >&2; exit 64; }; shift; rationale="$1" ;;
|
|
2292
|
+
--rationale=*) rationale="${arg#--rationale=}" ;;
|
|
2293
|
+
--idempotency-key) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --idempotency-key requires a value" >&2; exit 64; }; shift; idempotency_key="$1" ;;
|
|
2294
|
+
--idempotency-key=*) idempotency_key="${arg#--idempotency-key=}" ;;
|
|
2295
|
+
--executor-id) [ "$#" -gt 1 ] || { echo "aiur: executor-answer --executor-id requires a value" >&2; exit 64; }; shift; executor_id="$1" ;;
|
|
2296
|
+
--executor-id=*) executor_id="${arg#--executor-id=}" ;;
|
|
2297
|
+
-*) echo "aiur: executor-answer received an unknown option: $arg" >&2; exit 64 ;;
|
|
2298
|
+
*) echo "aiur: executor-answer expects exactly one decision ID" >&2; exit 64 ;;
|
|
2299
|
+
esac
|
|
2300
|
+
shift
|
|
2301
|
+
done
|
|
2302
|
+
|
|
2303
|
+
[[ "$expected_version" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-answer --expected-version expects a positive integer" >&2; exit 64; }
|
|
2304
|
+
if { [ -n "$option_id" ] && [ -n "$custom_response" ]; } || { [ -z "$option_id" ] && [ -z "$custom_response" ]; }; then
|
|
2305
|
+
echo "aiur: executor-answer requires exactly one of --option or --custom-response" >&2
|
|
2306
|
+
exit 64
|
|
2307
|
+
fi
|
|
2308
|
+
[ -n "$rationale" ] || { echo "aiur: executor-answer --rationale is required" >&2; exit 64; }
|
|
2309
|
+
[ -n "$idempotency_key" ] || { echo "aiur: executor-answer --idempotency-key is required" >&2; exit 64; }
|
|
2310
|
+
[ -n "$executor_id" ] || { echo "aiur: executor-answer --executor-id must not be empty" >&2; exit 64; }
|
|
2311
|
+
|
|
2312
|
+
local opts="decision_id: Base.decode64!(\"$(encode_control_value "$decision_id")\"), expected_version: $expected_version"
|
|
2313
|
+
if [ -n "$option_id" ]; then
|
|
2314
|
+
opts="$opts, option_id: Base.decode64!(\"$(encode_control_value "$option_id")\")"
|
|
2315
|
+
else
|
|
2316
|
+
opts="$opts, custom_response: Base.decode64!(\"$(encode_control_value "$custom_response")\")"
|
|
2317
|
+
fi
|
|
2318
|
+
opts="$opts, rationale: Base.decode64!(\"$(encode_control_value "$rationale")\")"
|
|
2319
|
+
opts="$opts, idempotency_key: Base.decode64!(\"$(encode_control_value "$idempotency_key")\")"
|
|
2320
|
+
opts="$opts, executor_id: Base.decode64!(\"$(encode_control_value "$executor_id")\")"
|
|
2321
|
+
run_control_rpc "Aiur.AgentControlCLI.executor_answer([$opts])"
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
cmd_executor_escalate() {
|
|
2325
|
+
local decision_id="${1:-}" expected_version="" reason="" executor_id="aiur-cli" arg
|
|
2326
|
+
if [ -z "$decision_id" ] || [[ "$decision_id" = -* ]]; then
|
|
2327
|
+
echo "aiur: executor-escalate expects exactly one decision ID" >&2
|
|
2328
|
+
exit 64
|
|
2329
|
+
fi
|
|
2330
|
+
shift
|
|
2331
|
+
|
|
2332
|
+
while [ "$#" -gt 0 ]; do
|
|
2333
|
+
arg="$1"
|
|
2334
|
+
case "$arg" in
|
|
2335
|
+
--expected-version) [ "$#" -gt 1 ] || { echo "aiur: executor-escalate --expected-version requires a value" >&2; exit 64; }; shift; expected_version="$1" ;;
|
|
2336
|
+
--expected-version=*) expected_version="${arg#--expected-version=}" ;;
|
|
2337
|
+
--reason) [ "$#" -gt 1 ] || { echo "aiur: executor-escalate --reason requires a value" >&2; exit 64; }; shift; reason="$1" ;;
|
|
2338
|
+
--reason=*) reason="${arg#--reason=}" ;;
|
|
2339
|
+
--executor-id) [ "$#" -gt 1 ] || { echo "aiur: executor-escalate --executor-id requires a value" >&2; exit 64; }; shift; executor_id="$1" ;;
|
|
2340
|
+
--executor-id=*) executor_id="${arg#--executor-id=}" ;;
|
|
2341
|
+
-*) echo "aiur: executor-escalate received an unknown option: $arg" >&2; exit 64 ;;
|
|
2342
|
+
*) echo "aiur: executor-escalate expects exactly one decision ID" >&2; exit 64 ;;
|
|
2343
|
+
esac
|
|
2344
|
+
shift
|
|
2345
|
+
done
|
|
2346
|
+
|
|
2347
|
+
[[ "$expected_version" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-escalate --expected-version expects a positive integer" >&2; exit 64; }
|
|
2348
|
+
[ -n "$reason" ] || { echo "aiur: executor-escalate --reason is required" >&2; exit 64; }
|
|
2349
|
+
[ -n "$executor_id" ] || { echo "aiur: executor-escalate --executor-id must not be empty" >&2; exit 64; }
|
|
2350
|
+
|
|
2351
|
+
local opts="decision_id: Base.decode64!(\"$(encode_control_value "$decision_id")\"), expected_version: $expected_version"
|
|
2352
|
+
opts="$opts, reason: Base.decode64!(\"$(encode_control_value "$reason")\")"
|
|
2353
|
+
opts="$opts, executor_id: Base.decode64!(\"$(encode_control_value "$executor_id")\")"
|
|
2354
|
+
run_control_rpc "Aiur.AgentControlCLI.executor_escalate([$opts])"
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
# `aiur units` — read the dashboard Units catalog through its own projection,
|
|
2358
|
+
# including non-running tickets in current-run membership.
|
|
2359
|
+
cmd_units() {
|
|
2360
|
+
local scope="live" format="" json=0 arg condition condition_value condition_encoded conditions_literal=""
|
|
2361
|
+
local -a conditions=() condition_values=()
|
|
2362
|
+
while [ "$#" -gt 0 ]; do
|
|
2363
|
+
arg="$1"
|
|
2364
|
+
case "$arg" in
|
|
2365
|
+
--scope) [ "$#" -gt 1 ] || { echo "aiur: units --scope requires a value" >&2; exit 64; }; shift; scope="$1" ;;
|
|
2366
|
+
--scope=*) scope="${arg#--scope=}" ;;
|
|
2367
|
+
--condition) [ "$#" -gt 1 ] || { echo "aiur: units --condition requires a value" >&2; exit 64; }; shift; conditions+=("$1") ;;
|
|
2368
|
+
--condition=*) conditions+=("${arg#--condition=}") ;;
|
|
2369
|
+
--format) [ "$#" -gt 1 ] || { echo "aiur: units --format requires a value" >&2; exit 64; }; shift; format="$1" ;;
|
|
2370
|
+
--format=*) format="${arg#--format=}" ;;
|
|
2371
|
+
--json) json=1 ;;
|
|
2372
|
+
-*) echo "aiur: units received an unknown option: $arg" >&2; exit 64 ;;
|
|
2373
|
+
*) echo "aiur: units does not accept positional arguments" >&2; exit 64 ;;
|
|
2374
|
+
esac
|
|
2375
|
+
shift
|
|
2376
|
+
done
|
|
2377
|
+
|
|
2378
|
+
for condition in "${conditions[@]+"${conditions[@]}"}"; do
|
|
2379
|
+
IFS=',' read -r -a condition_values <<< "$condition"
|
|
2380
|
+
for condition_value in "${condition_values[@]+"${condition_values[@]}"}"; do
|
|
2381
|
+
if [ -n "$conditions_literal" ]; then conditions_literal="$conditions_literal, "; fi
|
|
2382
|
+
condition_encoded="$(printf '%s' "$condition_value" | base64 | tr -d '\n')"
|
|
2383
|
+
conditions_literal="${conditions_literal}Base.decode64!(\"${condition_encoded}\")"
|
|
2384
|
+
done
|
|
2385
|
+
done
|
|
2386
|
+
|
|
2387
|
+
local scope_encoded
|
|
2388
|
+
scope_encoded="$(printf '%s' "$scope" | base64 | tr -d '\n')"
|
|
2389
|
+
local opts="scope: Base.decode64!(\"$scope_encoded\")"
|
|
2390
|
+
[ -z "$conditions_literal" ] || opts="$opts, conditions: [$conditions_literal]"
|
|
2391
|
+
|
|
2392
|
+
if [ -n "$format" ]; then
|
|
2393
|
+
local format_encoded
|
|
2394
|
+
format_encoded="$(printf '%s' "$format" | base64 | tr -d '\n')"
|
|
2395
|
+
opts="$opts, format: Base.decode64!(\"$format_encoded\")"
|
|
2396
|
+
fi
|
|
2397
|
+
|
|
2398
|
+
[ "$json" -eq 1 ] && opts="$opts, json: true"
|
|
2399
|
+
run_control_rpc "Aiur.AgentControlCLI.units([$opts])"
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
# `aiur build-orders` — read the dashboard Build Order projection without a
|
|
2403
|
+
# second GitHub or API derivation. A root selector switches from catalog to the
|
|
2404
|
+
# selected-root graph view.
|
|
2405
|
+
cmd_build_orders() {
|
|
2406
|
+
local json=0 root="" arg
|
|
2407
|
+
|
|
2408
|
+
while [ "$#" -gt 0 ]; do
|
|
2409
|
+
arg="$1"
|
|
2410
|
+
case "$arg" in
|
|
2411
|
+
--json) json=1 ;;
|
|
2412
|
+
-*) echo "aiur: build-orders received an unknown option: $arg" >&2; exit 64 ;;
|
|
2413
|
+
*)
|
|
2414
|
+
if [ -n "$root" ]; then
|
|
2415
|
+
echo "aiur: build-orders accepts at most one root" >&2
|
|
2416
|
+
exit 64
|
|
2417
|
+
fi
|
|
2418
|
+
root="$arg"
|
|
2419
|
+
;;
|
|
2420
|
+
esac
|
|
2421
|
+
shift
|
|
2422
|
+
done
|
|
2423
|
+
|
|
2424
|
+
local opts=""
|
|
2425
|
+
[ "$json" -eq 1 ] && opts="json: true"
|
|
2426
|
+
|
|
2427
|
+
if [ -n "$root" ]; then
|
|
2428
|
+
local encoded
|
|
2429
|
+
encoded="$(printf '%s' "$root" | base64 | tr -d '\n')"
|
|
2430
|
+
[ -n "$opts" ] && opts="$opts, "
|
|
2431
|
+
opts="${opts}root: Base.decode64!(\"$encoded\")"
|
|
2432
|
+
fi
|
|
2433
|
+
|
|
2434
|
+
run_control_rpc "Aiur.AgentControlCLI.build_orders([$opts])"
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
# `aiur analytics` — render the dashboard analytics projection for an explicit
|
|
2438
|
+
# time window. This is read-only and obtains the same durable telemetry snapshot
|
|
2439
|
+
# the page uses through the running node.
|
|
2440
|
+
cmd_analytics() {
|
|
2441
|
+
local range="run" json=0 since="" until="" build_order="" has_build_order=0 arg
|
|
2442
|
+
|
|
2443
|
+
while [ "$#" -gt 0 ]; do
|
|
2444
|
+
arg="$1"
|
|
2445
|
+
case "$arg" in
|
|
2446
|
+
--range) [ "$#" -gt 1 ] || { echo "aiur: analytics --range requires a value" >&2; exit 64; }; shift; range="$1" ;;
|
|
2447
|
+
--range=*) range="${arg#--range=}" ;;
|
|
2448
|
+
--since) [ "$#" -gt 1 ] || { echo "aiur: analytics --since requires a value" >&2; exit 64; }; shift; since="$1" ;;
|
|
2449
|
+
--since=*) since="${arg#--since=}" ;;
|
|
2450
|
+
--until) [ "$#" -gt 1 ] || { echo "aiur: analytics --until requires a value" >&2; exit 64; }; shift; until="$1" ;;
|
|
2451
|
+
--until=*) until="${arg#--until=}" ;;
|
|
2452
|
+
--build-order) [ "$#" -gt 1 ] || { echo "aiur: analytics --build-order requires a value" >&2; exit 64; }; shift; build_order="$1"; has_build_order=1 ;;
|
|
2453
|
+
--build-order=*) build_order="${arg#--build-order=}"; has_build_order=1 ;;
|
|
2454
|
+
--json) json=1 ;;
|
|
2455
|
+
-*) echo "aiur: analytics received an unknown option: $arg" >&2; exit 64 ;;
|
|
2456
|
+
*) echo "aiur: analytics does not accept positional arguments" >&2; exit 64 ;;
|
|
2457
|
+
esac
|
|
2458
|
+
shift
|
|
2459
|
+
done
|
|
2460
|
+
|
|
2461
|
+
case "$range" in run|full) ;; *) echo "aiur: analytics --range accepts run or full" >&2; exit 64 ;; esac
|
|
2462
|
+
[ "$has_build_order" -eq 0 ] || [[ "$build_order" =~ ^[0-9]+$ ]] || { echo "aiur: analytics --build-order expects a numeric ticket ID" >&2; exit 64; }
|
|
2463
|
+
|
|
2464
|
+
local opts="range: :$range" key raw encoded
|
|
2465
|
+
[ "$json" -eq 1 ] && opts="$opts, json: true"
|
|
2466
|
+
for key in since until build_order; do
|
|
2467
|
+
raw="${!key}"
|
|
2468
|
+
[ -n "$raw" ] || continue
|
|
2469
|
+
encoded="$(printf '%s' "$raw" | base64 | tr -d '\n')"
|
|
2470
|
+
opts="$opts, $key: Base.decode64!(\"$encoded\")"
|
|
2471
|
+
done
|
|
2472
|
+
|
|
2473
|
+
run_control_rpc "Aiur.AgentControlCLI.analytics([$opts])"
|
|
2474
|
+
}
|
|
2475
|
+
|
|
1699
2476
|
# `aiur alerts` — newline-delimited structured alert feed from persisted
|
|
1700
|
-
# per-agent logs. `--needs-attention` filters to
|
|
2477
|
+
# per-agent logs. `--needs-attention` filters to Executor-actionable alerts.
|
|
1701
2478
|
cmd_alerts() {
|
|
1702
2479
|
local needs_attention=0 arg
|
|
1703
2480
|
for arg in "$@"; do
|
|
@@ -1761,6 +2538,78 @@ cmd_watch() {
|
|
|
1761
2538
|
fi
|
|
1762
2539
|
}
|
|
1763
2540
|
|
|
2541
|
+
cmd_executor_listen() {
|
|
2542
|
+
local topic="executor.#" arg
|
|
2543
|
+
while [ "$#" -gt 0 ]; do
|
|
2544
|
+
arg="$1"
|
|
2545
|
+
case "$arg" in
|
|
2546
|
+
--topic) shift; topic="${1:-}" ;;
|
|
2547
|
+
--topic=*) topic="${arg#--topic=}" ;;
|
|
2548
|
+
*) echo "aiur: executor-listen accepts --topic <pattern>" >&2; exit 64 ;;
|
|
2549
|
+
esac
|
|
2550
|
+
[ -n "$topic" ] || { echo "aiur: executor-listen requires a topic" >&2; exit 64; }
|
|
2551
|
+
shift
|
|
2552
|
+
done
|
|
2553
|
+
local encoded
|
|
2554
|
+
encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
|
|
2555
|
+
run_control_stream "Aiur.AgentControlCLI.executor_listen(topic: Base.decode64!(\"$encoded\"))"
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
cmd_executor_wait() {
|
|
2559
|
+
local timeout=300 json=0 arg
|
|
2560
|
+
while [ "$#" -gt 0 ]; do
|
|
2561
|
+
arg="$1"
|
|
2562
|
+
case "$arg" in
|
|
2563
|
+
--timeout) shift; timeout="${1:-}" ;;
|
|
2564
|
+
--timeout=*) timeout="${arg#--timeout=}" ;;
|
|
2565
|
+
--json) json=1 ;;
|
|
2566
|
+
*) echo "aiur: executor-wait accepts --timeout <seconds> and --json" >&2; exit 64 ;;
|
|
2567
|
+
esac
|
|
2568
|
+
shift
|
|
2569
|
+
done
|
|
2570
|
+
[[ "$timeout" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: executor-wait --timeout expects a positive integer" >&2; exit 64; }
|
|
2571
|
+
local json_arg=false
|
|
2572
|
+
if [ "$json" -eq 1 ]; then json_arg=true; fi
|
|
2573
|
+
AIUR_CONTROL_COMMAND="executor-wait"
|
|
2574
|
+
AIUR_CONTROL_RPC_TIMEOUT_SECONDS=$((timeout + 10)) run_control_rpc "Aiur.AgentControlCLI.executor_wait(timeout_ms: $((timeout * 1000)), json: $json_arg)"
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
cmd_executor_emit() {
|
|
2578
|
+
local topic="${1:-}" payload="" arg
|
|
2579
|
+
shift 2>/dev/null || true
|
|
2580
|
+
[ -n "$topic" ] || { echo "aiur: executor-emit requires a topic" >&2; exit 64; }
|
|
2581
|
+
while [ "$#" -gt 0 ]; do
|
|
2582
|
+
arg="$1"
|
|
2583
|
+
case "$arg" in
|
|
2584
|
+
--payload) shift; payload="${1:-}" ;;
|
|
2585
|
+
--payload=*) payload="${arg#--payload=}" ;;
|
|
2586
|
+
*) echo "aiur: executor-emit accepts only --payload <json>" >&2; exit 64 ;;
|
|
2587
|
+
esac
|
|
2588
|
+
shift
|
|
2589
|
+
done
|
|
2590
|
+
[ -n "$payload" ] || { echo "aiur: executor-emit requires --payload <json>" >&2; exit 64; }
|
|
2591
|
+
local topic_encoded payload_encoded
|
|
2592
|
+
topic_encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
|
|
2593
|
+
payload_encoded="$(printf '%s' "$payload" | base64 | tr -d '\n')"
|
|
2594
|
+
run_control_rpc "Aiur.AgentControlCLI.executor_emit(Base.decode64!(\"$topic_encoded\"), Base.decode64!(\"$payload_encoded\"))"
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
cmd_executor_subscription() {
|
|
2598
|
+
local action="$1" topic="${2:-}"
|
|
2599
|
+
[ -n "$topic" ] && [ "$#" -eq 2 ] || { echo "aiur: $action requires one topic pattern" >&2; exit 64; }
|
|
2600
|
+
local encoded
|
|
2601
|
+
encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
|
|
2602
|
+
case "$action" in
|
|
2603
|
+
executor-subscribe) run_control_rpc "Aiur.AgentControlCLI.executor_subscribe(Base.decode64!(\"$encoded\"))" ;;
|
|
2604
|
+
executor-unsubscribe) run_control_rpc "Aiur.AgentControlCLI.executor_unsubscribe(Base.decode64!(\"$encoded\"))" ;;
|
|
2605
|
+
esac
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
cmd_executor_subscriptions() {
|
|
2609
|
+
[ "$#" -eq 0 ] || { echo "aiur: executor-subscriptions does not accept arguments" >&2; exit 64; }
|
|
2610
|
+
run_control_rpc "Aiur.AgentControlCLI.executor_subscriptions()"
|
|
2611
|
+
}
|
|
2612
|
+
|
|
1764
2613
|
watch_validate_interval() {
|
|
1765
2614
|
if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -le 0 ]; then
|
|
1766
2615
|
echo "aiur: watch --interval expects a positive integer (seconds)" >&2
|
|
@@ -1841,7 +2690,7 @@ sweep_dead_tmux_sockets() {
|
|
|
1841
2690
|
# foreground teardown (session_cleanup) and on `aiur stop`. Bounded and safe:
|
|
1842
2691
|
#
|
|
1843
2692
|
# * Only exact top-level Aiur artifact families under the temp roots the engine
|
|
1844
|
-
# and BEAM write to. Arbitrary
|
|
2693
|
+
# and BEAM write to. Arbitrary Executor worktrees/checkouts like
|
|
1845
2694
|
# `/tmp/aiur-pr490` are not candidates.
|
|
1846
2695
|
# * Age-gated by AIUR_TMP_REAP_MINUTES (default 1440 = 24h). An entry is removed
|
|
1847
2696
|
# only when NOTHING in its subtree was modified within the window, so a live
|
|
@@ -1935,8 +2784,41 @@ sweep_stale_tmp_artifacts() {
|
|
|
1935
2784
|
return 0
|
|
1936
2785
|
}
|
|
1937
2786
|
|
|
2787
|
+
# A stop only reaps the daemon whose node name matches this project root's
|
|
2788
|
+
# instance key (keys are sha256(project_root), so instances can't reap each
|
|
2789
|
+
# other). A daemon launched from a different directory therefore survives a stop
|
|
2790
|
+
# invoked elsewhere — the silent orphan that keeps holding the dashboard port and
|
|
2791
|
+
# serving stale code/credentials. Surface it loudly (we warn, not reap, to
|
|
2792
|
+
# respect the deliberate isolation) so the operator can stop it explicitly.
|
|
2793
|
+
warn_other_aiur_daemons() {
|
|
2794
|
+
local self_node="$1" me pids pid cmd node port found=0
|
|
2795
|
+
me="${USER:-$(id -un 2>/dev/null)}"
|
|
2796
|
+
pids="$(pgrep -u "$me" -f -- "-name aiur-${me}" 2>/dev/null || true)"
|
|
2797
|
+
for pid in $pids; do
|
|
2798
|
+
cmd="$(pid_command "$pid")"
|
|
2799
|
+
case "$cmd" in *beam.smp*) : ;; *) continue ;; esac
|
|
2800
|
+
node="$(printf '%s' "$cmd" | grep -oE -- '-name [^ ]+' | awk '{print $2}' | head -1)"
|
|
2801
|
+
[ -z "$node" ] && continue
|
|
2802
|
+
[ "$node" = "$self_node" ] && continue
|
|
2803
|
+
port="$(ss -tlnpH 2>/dev/null | awk -v p="pid=$pid," '$0 ~ p {print $4}' \
|
|
2804
|
+
| grep -oE '[0-9]+$' | head -1 || true)"
|
|
2805
|
+
if [ "$found" -eq 0 ]; then
|
|
2806
|
+
echo "aiur: heads up — another aiur daemon is still running that this stop did not touch" >&2
|
|
2807
|
+
echo " (launched from a different directory, so it has a different instance key):" >&2
|
|
2808
|
+
found=1
|
|
2809
|
+
fi
|
|
2810
|
+
echo " pid=$pid node=$node${port:+ dashboard-port=$port} — stop it with: kill $pid" >&2
|
|
2811
|
+
done
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
# Set when cmd_stop returns nonzero only because there was nothing to stop. It
|
|
2815
|
+
# lets a caller (cmd_restart) tell that benign outcome apart from a stop that
|
|
2816
|
+
# failed for a real reason, without changing `aiur stop`'s own exit code.
|
|
2817
|
+
AIUR_STOP_FOUND_NOTHING=0
|
|
2818
|
+
|
|
1938
2819
|
# Stop the running session: kill this instance's tmux + BEAM, then sweep.
|
|
1939
2820
|
cmd_stop() {
|
|
2821
|
+
AIUR_STOP_FOUND_NOTHING=0
|
|
1940
2822
|
resolve_release
|
|
1941
2823
|
aiur_resolve_identity
|
|
1942
2824
|
RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
@@ -1944,8 +2826,8 @@ cmd_stop() {
|
|
|
1944
2826
|
export RELEASE_NODE ERL_EPMD_ADDRESS
|
|
1945
2827
|
resolve_control_identity_from_records
|
|
1946
2828
|
|
|
1947
|
-
local
|
|
1948
|
-
|
|
2829
|
+
local workspace_root_file
|
|
2830
|
+
workspace_root_file="$(workspace_root_file_from_instance_record 2>/dev/null || true)"
|
|
1949
2831
|
|
|
1950
2832
|
local tmux_bin
|
|
1951
2833
|
tmux_bin="$(command -v tmux || true)"
|
|
@@ -1963,7 +2845,9 @@ cmd_stop() {
|
|
|
1963
2845
|
[ "$has_session" -eq 0 ] && \
|
|
1964
2846
|
[ ! -f "$(aiur_crash_marker_path)" ]; then
|
|
1965
2847
|
echo "aiur: no running aiur node at ${AIUR_RELEASE_NODE}; nothing stopped" >&2
|
|
2848
|
+
warn_other_aiur_daemons "$AIUR_RELEASE_NODE"
|
|
1966
2849
|
print_global_config_control_hint
|
|
2850
|
+
AIUR_STOP_FOUND_NOTHING=1
|
|
1967
2851
|
return 1
|
|
1968
2852
|
fi
|
|
1969
2853
|
|
|
@@ -1984,9 +2868,10 @@ cmd_stop() {
|
|
|
1984
2868
|
# it. Do not sweep by release dir: sibling instances share dev releases.
|
|
1985
2869
|
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
1986
2870
|
|
|
1987
|
-
# Final cwd-sweep backstop after the BEAM is dead.
|
|
1988
|
-
#
|
|
1989
|
-
|
|
2871
|
+
# Final cwd-sweep backstop after the BEAM is dead. The launcher's instance
|
|
2872
|
+
# record points to the BEAM-written root handoff, so degraded stop never has
|
|
2873
|
+
# to wait for an overloaded daemon before it starts tearing workers down.
|
|
2874
|
+
reap_workspace_cwd_from_file "$workspace_root_file"
|
|
1990
2875
|
|
|
1991
2876
|
# The BEAM (alive until the TERM above) reaped its own headless agents through
|
|
1992
2877
|
# ProcessReaper on Application.stop. kill-server is the guarantee the earlier
|
|
@@ -2011,12 +2896,244 @@ cmd_stop() {
|
|
|
2011
2896
|
sweep_dead_tmux_sockets
|
|
2012
2897
|
sweep_stale_tmp_artifacts
|
|
2013
2898
|
rm -f "$(aiur_instance_record_path)" 2>/dev/null || true
|
|
2899
|
+
warn_other_aiur_daemons "$AIUR_RELEASE_NODE"
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
# Stop, refresh the release, start again — `aiur restart`.
|
|
2903
|
+
#
|
|
2904
|
+
# The refresh deliberately runs BETWEEN the stop and the start. A dev rebuild
|
|
2905
|
+
# rewrites the release directory in place, and a BEAM booted from that directory
|
|
2906
|
+
# is exactly what the control-retry path (EX_TEMPFAIL) exists to survive; doing
|
|
2907
|
+
# it while the daemon is down means the new daemon boots from a release nothing
|
|
2908
|
+
# else is holding half-written.
|
|
2909
|
+
#
|
|
2910
|
+
# The engine itself never knows how to build: the installed CLI runs a pinned
|
|
2911
|
+
# platform release with no source tree beside it, so its restart is a plain
|
|
2912
|
+
# bounce. The dev shim supplies its build-if-stale step through
|
|
2913
|
+
# AIUR_RESTART_BUILD_CMD, a shell command line run via `bash -c` so the wrapper
|
|
2914
|
+
# (not this parser) owns quoting.
|
|
2915
|
+
aiur_restart_daemon_state="untouched"
|
|
2916
|
+
|
|
2917
|
+
# Everything between the stop and a confirmed start runs with this armed, so the
|
|
2918
|
+
# operator hears about a fleet that is down no matter which step failed — a
|
|
2919
|
+
# rebuild, a missing tmux, a BEAM that would not boot, or a Ctrl-C during the
|
|
2920
|
+
# minutes-long rebuild. Naming only the step that failed would let "aiur failed
|
|
2921
|
+
# to start" read as "nothing changed", which is the silently stopped fleet.
|
|
2922
|
+
aiur_restart_report_state() {
|
|
2923
|
+
[ "$aiur_restart_daemon_state" = "stopped" ] || return 0
|
|
2924
|
+
|
|
2925
|
+
# release_dir is resolved by the stop; fall back to the caller's declared dir
|
|
2926
|
+
# for the paths that fail before that.
|
|
2927
|
+
local dir="${release_dir:-${AIUR_RELEASE_DIR:-}}"
|
|
2928
|
+
|
|
2929
|
+
echo "aiur: the daemon is STOPPED and was NOT restarted" >&2
|
|
2930
|
+
if [ -n "$dir" ] && [ -x "$dir/bin/aiur" ]; then
|
|
2931
|
+
echo "aiur: fix the cause and run 'aiur restart' again, or 'aiur restart --no-build' to start" >&2
|
|
2932
|
+
echo " on the release already on disk" >&2
|
|
2933
|
+
else
|
|
2934
|
+
# The dev builder removes an incomplete release, so after a failed rebuild
|
|
2935
|
+
# there is nothing for --no-build to start. Advertising it anyway would cost
|
|
2936
|
+
# the operator a second failed cycle while the fleet stays down.
|
|
2937
|
+
echo "aiur: no complete release is left on disk, so --no-build has nothing to start —" >&2
|
|
2938
|
+
echo " fix the build and run 'aiur restart' again" >&2
|
|
2939
|
+
fi
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
# The rebuild runs in a process this command cannot see into, so "the builder
|
|
2943
|
+
# exited 0" is not the same claim as "the release I am about to boot is the one
|
|
2944
|
+
# it just built" — a wrapper pointed at another checkout satisfies the first and
|
|
2945
|
+
# not the second, which is exactly how a bounce ships a stale release while every
|
|
2946
|
+
# step reports success. The builder leaves a receipt naming the release dir it
|
|
2947
|
+
# vouches for and the commit it built from; this checks both against the release
|
|
2948
|
+
# on disk. Anything unverifiable is reported as unverifiable, never as success.
|
|
2949
|
+
verify_restart_build() {
|
|
2950
|
+
local receipt="$1"
|
|
2951
|
+
local built_dir built_sha stamped_sha stamp target_dir
|
|
2952
|
+
|
|
2953
|
+
if [ ! -s "$receipt" ]; then
|
|
2954
|
+
echo "aiur: the rebuild left no build receipt, so restart cannot confirm which" >&2
|
|
2955
|
+
echo " release it produced" >&2
|
|
2956
|
+
return 1
|
|
2957
|
+
fi
|
|
2958
|
+
|
|
2959
|
+
built_dir="$(sed -n 's/^release_dir=//p' "$receipt" | head -n 1)"
|
|
2960
|
+
built_sha="$(sed -n 's/^source_sha=//p' "$receipt" | head -n 1)"
|
|
2961
|
+
|
|
2962
|
+
# Fail on the missing field rather than on whatever an empty one happens to
|
|
2963
|
+
# compare equal to further down.
|
|
2964
|
+
if [ -z "$built_dir" ] || [ -z "$built_sha" ]; then
|
|
2965
|
+
echo "aiur: the build receipt is malformed; restart cannot confirm what was built" >&2
|
|
2966
|
+
return 1
|
|
2967
|
+
fi
|
|
2968
|
+
|
|
2969
|
+
# Compare canonical paths: the builder and this engine can reach the same
|
|
2970
|
+
# release through different symlinked parents.
|
|
2971
|
+
built_dir="$(cd -P "$built_dir" 2>/dev/null && pwd || printf '%s' "$built_dir")"
|
|
2972
|
+
target_dir="$(cd -P "${AIUR_RELEASE_DIR:-}" 2>/dev/null && pwd || printf '%s' "${AIUR_RELEASE_DIR:-}")"
|
|
2973
|
+
|
|
2974
|
+
if [ "$built_dir" != "$target_dir" ]; then
|
|
2975
|
+
echo "aiur: the rebuild targeted a different release than the one about to boot" >&2
|
|
2976
|
+
echo " rebuilt : $built_dir" >&2
|
|
2977
|
+
echo " booting : $target_dir" >&2
|
|
2978
|
+
return 1
|
|
2979
|
+
fi
|
|
2980
|
+
|
|
2981
|
+
stamp="$target_dir/AIUR_BUILD_STAMP"
|
|
2982
|
+
if [ ! -r "$stamp" ]; then
|
|
2983
|
+
echo "aiur: the release carries no build stamp, so restart cannot confirm it is" >&2
|
|
2984
|
+
echo " the one just built ($stamp)" >&2
|
|
2985
|
+
return 1
|
|
2986
|
+
fi
|
|
2987
|
+
|
|
2988
|
+
stamped_sha="$(sed -n 's/^source_sha=//p' "$stamp" | head -n 1)"
|
|
2989
|
+
if [ "$stamped_sha" != "$built_sha" ]; then
|
|
2990
|
+
echo "aiur: the release on disk was not built from the commit the rebuild reported" >&2
|
|
2991
|
+
echo " rebuild reported : $built_sha" >&2
|
|
2992
|
+
echo " release stamped : $stamped_sha" >&2
|
|
2993
|
+
return 1
|
|
2994
|
+
fi
|
|
2995
|
+
|
|
2996
|
+
# Say what was actually established. "verified ... built from unknown" would be
|
|
2997
|
+
# a silent-pass guard reporting success over a release it cannot identify, and
|
|
2998
|
+
# a dirty tree means the SHA is a floor rather than an identity.
|
|
2999
|
+
if [ "$stamped_sha" = "unknown" ]; then
|
|
3000
|
+
echo "aiur: release $target_dir matches the rebuild, but its source commit is" >&2
|
|
3001
|
+
echo " unknown (built outside a git work tree) — provenance unverified" >&2
|
|
3002
|
+
return 0
|
|
3003
|
+
fi
|
|
3004
|
+
|
|
3005
|
+
if [ "$(sed -n 's/^dirty=//p' "$stamp" | head -n 1)" = "yes" ]; then
|
|
3006
|
+
echo "aiur: release $target_dir built from $stamped_sha with uncommitted changes" >&2
|
|
3007
|
+
return 0
|
|
3008
|
+
fi
|
|
3009
|
+
|
|
3010
|
+
echo "aiur: verified release $target_dir built from $stamped_sha" >&2
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
cmd_restart() {
|
|
3014
|
+
local skip_build=0 arg
|
|
3015
|
+
local run_args=()
|
|
3016
|
+
|
|
3017
|
+
# Refuse a stale config before stopping a healthy daemon. dispatch_run also
|
|
3018
|
+
# checks this for ordinary launches, but restart mutates state before it gets
|
|
3019
|
+
# that far.
|
|
3020
|
+
reject_legacy_config "$@"
|
|
3021
|
+
|
|
3022
|
+
for arg in "$@"; do
|
|
3023
|
+
case "$arg" in
|
|
3024
|
+
--no-build) skip_build=1 ;;
|
|
3025
|
+
*) run_args+=("$arg") ;;
|
|
3026
|
+
esac
|
|
3027
|
+
done
|
|
3028
|
+
|
|
3029
|
+
# Restarting something that is already down is a start, not an error: the
|
|
3030
|
+
# operator asked for a running daemon on current code, and that is what they
|
|
3031
|
+
# get. Any OTHER stop failure is fatal — building and starting on top of a
|
|
3032
|
+
# half-stopped instance is the in-place rewrite this ordering exists to avoid.
|
|
3033
|
+
local stop_status=0
|
|
3034
|
+
cmd_stop || stop_status=$?
|
|
3035
|
+
if [ "$stop_status" -ne 0 ]; then
|
|
3036
|
+
if [ "${AIUR_STOP_FOUND_NOTHING:-0}" -eq 1 ]; then
|
|
3037
|
+
echo "aiur: nothing was running; restart will start a fresh session" >&2
|
|
3038
|
+
else
|
|
3039
|
+
die "restart aborted: stop failed (exit $stop_status); the daemon may still be running"
|
|
3040
|
+
fi
|
|
3041
|
+
fi
|
|
3042
|
+
|
|
3043
|
+
# cmd_stop's tmux teardown is best-effort (`|| true`), so a stop can report
|
|
3044
|
+
# success while the session survives. Starting from there would rebuild the
|
|
3045
|
+
# release under a live BEAM and then hit run_session's idempotent "already
|
|
3046
|
+
# running" return — exit 0, stale release, no bounce. Refuse instead.
|
|
3047
|
+
# release_bin is unset only when the stop was stubbed out, i.e. in tests.
|
|
3048
|
+
if [ -n "${release_bin:-}" ] && [ "$(probe_control_liveness)" = "up" ]; then
|
|
3049
|
+
die "restart aborted: the previous session still answers after the stop; nothing was rebuilt or restarted"
|
|
3050
|
+
fi
|
|
3051
|
+
|
|
3052
|
+
aiur_restart_daemon_state="stopped"
|
|
3053
|
+
trap aiur_restart_report_state EXIT INT TERM
|
|
3054
|
+
|
|
3055
|
+
if [ -n "${AIUR_RESTART_BUILD_CMD:-}" ] && [ "$skip_build" -eq 0 ]; then
|
|
3056
|
+
local build_status=0 receipt
|
|
3057
|
+
receipt="$(mktemp "${TMPDIR:-/tmp}/aiur-restart-receipt.XXXXXX")"
|
|
3058
|
+
AIUR_RESTART_BUILD_RECEIPT="$receipt" bash -c "$AIUR_RESTART_BUILD_CMD" || build_status=$?
|
|
3059
|
+
if [ "$build_status" -ne 0 ]; then
|
|
3060
|
+
# Exit with the builder's own status rather than a flat 1, so a caller can
|
|
3061
|
+
# still tell a failed rebuild from any other abort.
|
|
3062
|
+
rm -f "$receipt"
|
|
3063
|
+
echo "❌ restart aborted before start: the rebuild failed" >&2
|
|
3064
|
+
aiur_restart_report_state
|
|
3065
|
+
trap - EXIT INT TERM
|
|
3066
|
+
exit "$build_status"
|
|
3067
|
+
fi
|
|
3068
|
+
|
|
3069
|
+
# A builder that declares AIUR_RESTART_BUILD_VERIFIES has promised a receipt,
|
|
3070
|
+
# so a missing or contradicted one is a real failure and an unverifiable
|
|
3071
|
+
# rebuild is not a lesser success — starting anyway would hand back the
|
|
3072
|
+
# stale-release outcome this ordering exists to prevent, with a "restarted"
|
|
3073
|
+
# line on top of it. A builder that makes no such promise cannot be held to
|
|
3074
|
+
# it; say plainly that the guarantee did not apply rather than convert an
|
|
3075
|
+
# unknown wrapper into a stopped fleet.
|
|
3076
|
+
if [ "${AIUR_RESTART_BUILD_VERIFIES:-0}" = "1" ]; then
|
|
3077
|
+
local verify_status=0
|
|
3078
|
+
verify_restart_build "$receipt" || verify_status=$?
|
|
3079
|
+
rm -f "$receipt"
|
|
3080
|
+
if [ "$verify_status" -ne 0 ]; then
|
|
3081
|
+
echo "❌ restart aborted before start: the rebuild could not be verified" >&2
|
|
3082
|
+
# Naming the builder makes an inherited AIUR_RESTART_BUILD_CMD — a dev
|
|
3083
|
+
# shim leaking into another shell — diagnosable from this output alone,
|
|
3084
|
+
# instead of an identical refusal on every retry.
|
|
3085
|
+
echo " rebuild command: ${AIUR_RESTART_BUILD_CMD}" >&2
|
|
3086
|
+
echo " Rebuild from the checkout you mean and retry, or 'aiur restart --no-build'" >&2
|
|
3087
|
+
echo " to start the release on disk without the guarantee." >&2
|
|
3088
|
+
aiur_restart_report_state
|
|
3089
|
+
trap - EXIT INT TERM
|
|
3090
|
+
exit 70
|
|
3091
|
+
fi
|
|
3092
|
+
else
|
|
3093
|
+
rm -f "$receipt"
|
|
3094
|
+
echo "aiur: UNVERIFIED rebuild — this builder reports no receipt, so restart" >&2
|
|
3095
|
+
echo " cannot confirm the release it is about to boot is the one just built" >&2
|
|
3096
|
+
fi
|
|
3097
|
+
elif [ "$skip_build" -eq 1 ]; then
|
|
3098
|
+
echo "aiur: --no-build — restarting on the release already on disk" >&2
|
|
3099
|
+
fi
|
|
3100
|
+
|
|
3101
|
+
# Always detached: the daemon this stopped was itself detached, and a restart
|
|
3102
|
+
# that captured the terminal would be a surprise. `--interactive` still opts
|
|
3103
|
+
# into the attachable session (`aiur --bg --interactive`), and a purely
|
|
3104
|
+
# foreground restart remains `aiur stop && aiur run`.
|
|
3105
|
+
dispatch_run --bg "${run_args[@]+"${run_args[@]}"}"
|
|
3106
|
+
|
|
3107
|
+
aiur_restart_daemon_state="running"
|
|
3108
|
+
trap - EXIT INT TERM
|
|
2014
3109
|
}
|
|
2015
3110
|
|
|
2016
3111
|
# --- dispatch ----------------------------------------------------------------
|
|
2017
3112
|
|
|
3113
|
+
dispatch_run() {
|
|
3114
|
+
local mode="foreground" arg
|
|
3115
|
+
local args=()
|
|
3116
|
+
|
|
3117
|
+
reject_legacy_config "$@"
|
|
3118
|
+
|
|
3119
|
+
for arg in "$@"; do
|
|
3120
|
+
if [ "$arg" = "--bg" ]; then
|
|
3121
|
+
mode="background"
|
|
3122
|
+
else
|
|
3123
|
+
args+=("$arg")
|
|
3124
|
+
fi
|
|
3125
|
+
done
|
|
3126
|
+
|
|
3127
|
+
# bash 3.2 (macOS default) errors on "${args[@]}" when args is empty under
|
|
3128
|
+
# `set -u` — happens for a bare `--bg` run. Guard the expansion.
|
|
3129
|
+
run_session "$mode" "${args[@]+"${args[@]}"}"
|
|
3130
|
+
}
|
|
3131
|
+
|
|
2018
3132
|
aiur_engine_main() {
|
|
2019
3133
|
local cmd="${1:-}"
|
|
3134
|
+
# Names the running subcommand in control-RPC diagnostics so a failure says
|
|
3135
|
+
# which command failed instead of a generic "control rpc" (#1684).
|
|
3136
|
+
AIUR_CONTROL_COMMAND="${cmd:-run}"
|
|
2020
3137
|
case "$cmd" in
|
|
2021
3138
|
__identity)
|
|
2022
3139
|
aiur_print_identity
|
|
@@ -2027,25 +3144,69 @@ aiur_engine_main() {
|
|
|
2027
3144
|
--version)
|
|
2028
3145
|
run_version "$@"
|
|
2029
3146
|
;;
|
|
3147
|
+
--todo)
|
|
3148
|
+
run_todo "$@"
|
|
3149
|
+
;;
|
|
3150
|
+
--only)
|
|
3151
|
+
echo "aiur: --only is valid only with --todo" >&2
|
|
3152
|
+
exit 64
|
|
3153
|
+
;;
|
|
2030
3154
|
init)
|
|
2031
3155
|
run_init "$@"
|
|
2032
3156
|
;;
|
|
2033
|
-
|
|
3157
|
+
findings)
|
|
3158
|
+
run_findings "$@"
|
|
3159
|
+
;;
|
|
3160
|
+
guard-pr-deletions)
|
|
2034
3161
|
shift
|
|
2035
|
-
|
|
3162
|
+
"$engine_dir/guard-pr-deletions.sh" "$@"
|
|
3163
|
+
;;
|
|
3164
|
+
ask | asks)
|
|
3165
|
+
run_asks "$@"
|
|
3166
|
+
;;
|
|
3167
|
+
--bg)
|
|
3168
|
+
dispatch_run "$@"
|
|
2036
3169
|
;;
|
|
2037
3170
|
run)
|
|
2038
3171
|
shift
|
|
2039
|
-
|
|
3172
|
+
dispatch_run "$@"
|
|
2040
3173
|
;;
|
|
2041
3174
|
status)
|
|
2042
3175
|
shift
|
|
2043
3176
|
cmd_status "$@"
|
|
2044
3177
|
;;
|
|
3178
|
+
usage)
|
|
3179
|
+
shift
|
|
3180
|
+
cmd_usage "$@"
|
|
3181
|
+
;;
|
|
2045
3182
|
agents)
|
|
2046
3183
|
shift
|
|
2047
3184
|
cmd_agents "$@"
|
|
2048
3185
|
;;
|
|
3186
|
+
commands)
|
|
3187
|
+
shift
|
|
3188
|
+
cmd_commands "$@"
|
|
3189
|
+
;;
|
|
3190
|
+
executor-answer)
|
|
3191
|
+
shift
|
|
3192
|
+
cmd_executor_answer "$@"
|
|
3193
|
+
;;
|
|
3194
|
+
executor-escalate)
|
|
3195
|
+
shift
|
|
3196
|
+
cmd_executor_escalate "$@"
|
|
3197
|
+
;;
|
|
3198
|
+
units)
|
|
3199
|
+
shift
|
|
3200
|
+
cmd_units "$@"
|
|
3201
|
+
;;
|
|
3202
|
+
build-orders)
|
|
3203
|
+
shift
|
|
3204
|
+
cmd_build_orders "$@"
|
|
3205
|
+
;;
|
|
3206
|
+
analytics)
|
|
3207
|
+
shift
|
|
3208
|
+
cmd_analytics "$@"
|
|
3209
|
+
;;
|
|
2049
3210
|
alerts)
|
|
2050
3211
|
shift
|
|
2051
3212
|
cmd_alerts "$@"
|
|
@@ -2054,6 +3215,26 @@ aiur_engine_main() {
|
|
|
2054
3215
|
shift
|
|
2055
3216
|
cmd_watch "$@"
|
|
2056
3217
|
;;
|
|
3218
|
+
executor-listen)
|
|
3219
|
+
shift
|
|
3220
|
+
cmd_executor_listen "$@"
|
|
3221
|
+
;;
|
|
3222
|
+
executor-wait)
|
|
3223
|
+
shift
|
|
3224
|
+
cmd_executor_wait "$@"
|
|
3225
|
+
;;
|
|
3226
|
+
executor-emit)
|
|
3227
|
+
shift
|
|
3228
|
+
cmd_executor_emit "$@"
|
|
3229
|
+
;;
|
|
3230
|
+
executor-subscribe | executor-unsubscribe)
|
|
3231
|
+
shift
|
|
3232
|
+
cmd_executor_subscription "$cmd" "$@"
|
|
3233
|
+
;;
|
|
3234
|
+
executor-subscriptions)
|
|
3235
|
+
shift
|
|
3236
|
+
cmd_executor_subscriptions "$@"
|
|
3237
|
+
;;
|
|
2057
3238
|
set)
|
|
2058
3239
|
shift
|
|
2059
3240
|
cmd_set "$@"
|
|
@@ -2062,6 +3243,10 @@ aiur_engine_main() {
|
|
|
2062
3243
|
shift
|
|
2063
3244
|
cmd_pause_resume "$cmd" "$@"
|
|
2064
3245
|
;;
|
|
3246
|
+
reset-budget)
|
|
3247
|
+
shift
|
|
3248
|
+
cmd_reset_budget "$@"
|
|
3249
|
+
;;
|
|
2065
3250
|
message)
|
|
2066
3251
|
shift
|
|
2067
3252
|
cmd_message "$@"
|
|
@@ -2073,19 +3258,24 @@ aiur_engine_main() {
|
|
|
2073
3258
|
stop)
|
|
2074
3259
|
cmd_stop
|
|
2075
3260
|
;;
|
|
3261
|
+
restart)
|
|
3262
|
+
shift
|
|
3263
|
+
cmd_restart "$@"
|
|
3264
|
+
;;
|
|
2076
3265
|
"")
|
|
2077
|
-
|
|
3266
|
+
dispatch_run
|
|
2078
3267
|
;;
|
|
2079
3268
|
-*)
|
|
2080
3269
|
# leading-flag forms (e.g. `aiur --interactive <config>`) are a run
|
|
2081
|
-
|
|
3270
|
+
dispatch_run "$@"
|
|
2082
3271
|
;;
|
|
2083
3272
|
*)
|
|
2084
3273
|
# a path/config argument is a run; anything else is a usage error
|
|
2085
3274
|
if [ -e "$cmd" ]; then
|
|
2086
|
-
|
|
3275
|
+
dispatch_run "$@"
|
|
2087
3276
|
else
|
|
2088
3277
|
echo "aiur: unknown command: $cmd" >&2
|
|
3278
|
+
warn_if_cli_behind_release_checkout
|
|
2089
3279
|
usage >&2
|
|
2090
3280
|
exit 64
|
|
2091
3281
|
fi
|