aiur-cli 0.0.3 → 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 -53
- package/bin/aiur.js +5 -4
- package/libexec/aiur-engine.sh +2085 -112
- package/libexec/guard-pr-deletions.sh +70 -0
- package/package.json +9 -8
- package/scripts/postinstall.mjs +5 -4
package/libexec/aiur-engine.sh
CHANGED
|
@@ -35,32 +35,237 @@ 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
|
-
# The aiur project root
|
|
48
|
-
#
|
|
208
|
+
# The aiur project root used to key this instance. AIUR_REPO_ROOT (set by the dev
|
|
209
|
+
# shim) wins. Otherwise walk up from $PWD to the first dir holding a REPO-LOCAL
|
|
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
|
|
212
|
+
# project under $HOME onto one key (#443). When no repo-local config is found, the
|
|
213
|
+
# BEAM serves this run via the global config (mirroring its discovery order in
|
|
214
|
+
# src/lib/aiur/workflow.ex) — or via none — and in both cases the project being
|
|
215
|
+
# served is the cwd, so we key by realpath($PWD). That gives each global-config
|
|
216
|
+
# project a distinct identity instead of an empty key (legacy aiur-$USER@…) or $HOME.
|
|
217
|
+
#
|
|
218
|
+
# Caveat: a global-config run's key is cwd-derived (there is no repo root to
|
|
219
|
+
# converge on), so control commands (status/pause/stop) must be run from the SAME
|
|
220
|
+
# directory the run was launched from. A repo with a repo-local .aiur/config keeps
|
|
221
|
+
# the walk-up, so its control commands still resolve from any subdir.
|
|
49
222
|
aiur_project_root() {
|
|
50
223
|
if [ -n "${AIUR_REPO_ROOT:-}" ]; then printf '%s' "$AIUR_REPO_ROOT"; return; fi
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
224
|
+
|
|
225
|
+
# Canonicalize $PWD and $HOME so the home-boundary test below holds even when
|
|
226
|
+
# either is reached through a symlink.
|
|
227
|
+
local pwd_real home_real
|
|
228
|
+
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
229
|
+
# ${HOME:-} (not bare $HOME) so an unset HOME under `set -u` can't abort the
|
|
230
|
+
# script here; an empty home_real simply disables the boundary (walk to /).
|
|
231
|
+
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
232
|
+
|
|
233
|
+
local d="$pwd_real"
|
|
234
|
+
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
235
|
+
if [ -f "$d/.aiur/config" ]; then
|
|
54
236
|
printf '%s' "$d"
|
|
55
237
|
return
|
|
56
238
|
fi
|
|
57
239
|
d="$(dirname "$d")"
|
|
58
240
|
done
|
|
241
|
+
|
|
242
|
+
printf '%s' "$pwd_real"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
aiur_project_root_source() {
|
|
246
|
+
if [ -n "${AIUR_REPO_ROOT:-}" ]; then printf 'env'; return; fi
|
|
247
|
+
|
|
248
|
+
local pwd_real home_real
|
|
249
|
+
pwd_real="$(pwd -P 2>/dev/null || printf '%s' "$PWD")"
|
|
250
|
+
home_real="$(cd "${HOME:-}" 2>/dev/null && pwd -P || printf '%s' "${HOME:-}")"
|
|
251
|
+
|
|
252
|
+
local d="$pwd_real"
|
|
253
|
+
while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "$home_real" ]; do
|
|
254
|
+
if [ -f "$d/.aiur/config" ]; then
|
|
255
|
+
printf 'repo'
|
|
256
|
+
return
|
|
257
|
+
fi
|
|
258
|
+
d="$(dirname "$d")"
|
|
259
|
+
done
|
|
260
|
+
|
|
261
|
+
printf 'cwd'
|
|
59
262
|
}
|
|
60
263
|
|
|
61
264
|
# Short, stable, node-name-legal (lowercase hex) key for the project root, so two
|
|
62
265
|
# aiur instances for the same user get distinct node/session/socket names and can't
|
|
63
|
-
# reap each other.
|
|
266
|
+
# reap each other. Any real cwd now resolves a key (global-config runs key by
|
|
267
|
+
# realpath($PWD), #443); only a degenerate unreadable cwd yields empty, falling back
|
|
268
|
+
# to the legacy un-keyed name.
|
|
64
269
|
aiur_instance_key() {
|
|
65
270
|
local root
|
|
66
271
|
root="$(aiur_project_root)"
|
|
@@ -74,6 +279,8 @@ aiur_instance_key() {
|
|
|
74
279
|
|
|
75
280
|
aiur_resolve_identity() {
|
|
76
281
|
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
282
|
+
AIUR_PROJECT_ROOT="$(aiur_project_root)"
|
|
283
|
+
AIUR_PROJECT_ROOT_SOURCE="$(aiur_project_root_source)"
|
|
77
284
|
|
|
78
285
|
: "${AIUR_BG_STATE_DIR:=$config_home/aiur}"
|
|
79
286
|
: "${AIUR_COOKIE_FILE:=$AIUR_BG_STATE_DIR/cookie}"
|
|
@@ -87,7 +294,8 @@ aiur_resolve_identity() {
|
|
|
87
294
|
: "${AIUR_RELEASE_NODE:=aiur-${USER}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}@127.0.0.1}"
|
|
88
295
|
|
|
89
296
|
export AIUR_BG_STATE_DIR AIUR_COOKIE_FILE AIUR_SESSION_PREFIX \
|
|
90
|
-
AIUR_PROFILES_FILE AIUR_RELEASE_NODE AIUR_INSTANCE_KEY
|
|
297
|
+
AIUR_PROFILES_FILE AIUR_RELEASE_NODE AIUR_INSTANCE_KEY \
|
|
298
|
+
AIUR_PROJECT_ROOT AIUR_PROJECT_ROOT_SOURCE
|
|
91
299
|
}
|
|
92
300
|
|
|
93
301
|
aiur_print_identity() {
|
|
@@ -98,6 +306,8 @@ aiur_print_identity() {
|
|
|
98
306
|
printf 'AIUR_PROFILES_FILE=%s\n' "$AIUR_PROFILES_FILE"
|
|
99
307
|
printf 'AIUR_RELEASE_NODE=%s\n' "$AIUR_RELEASE_NODE"
|
|
100
308
|
printf 'AIUR_INSTANCE_KEY=%s\n' "$AIUR_INSTANCE_KEY"
|
|
309
|
+
printf 'AIUR_PROJECT_ROOT=%s\n' "$AIUR_PROJECT_ROOT"
|
|
310
|
+
printf 'AIUR_PROJECT_ROOT_SOURCE=%s\n' "$AIUR_PROJECT_ROOT_SOURCE"
|
|
101
311
|
printf 'AIUR_COOKIE_FILE=%s\n' "$AIUR_COOKIE_FILE"
|
|
102
312
|
}
|
|
103
313
|
|
|
@@ -164,16 +374,35 @@ release_dir=""
|
|
|
164
374
|
vsn_dir=""
|
|
165
375
|
release_bin=""
|
|
166
376
|
|
|
377
|
+
control_release_retry() {
|
|
378
|
+
[ -n "${AIUR_CONTROL_RELEASE_RETRY_SIGNAL:-}" ] && : >"$AIUR_CONTROL_RELEASE_RETRY_SIGNAL"
|
|
379
|
+
return 75
|
|
380
|
+
}
|
|
381
|
+
|
|
167
382
|
resolve_release() {
|
|
168
383
|
release_dir="${AIUR_RELEASE_DIR:-}"
|
|
169
384
|
[ -n "$release_dir" ] || die "AIUR_RELEASE_DIR is not set; the engine must be invoked via the aiur or aiurdev wrapper"
|
|
170
|
-
[ -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
|
|
171
394
|
|
|
172
395
|
local release_vsn
|
|
173
|
-
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)"
|
|
174
397
|
vsn_dir="$release_dir/releases/$release_vsn"
|
|
175
398
|
release_bin="$release_dir/bin/aiur"
|
|
176
|
-
[ -
|
|
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
|
|
177
406
|
}
|
|
178
407
|
|
|
179
408
|
# --- argv round-trip (System.argv is empty under `elixir --eval`) -------------
|
|
@@ -218,15 +447,40 @@ build_init_cmd() {
|
|
|
218
447
|
|
|
219
448
|
usage() {
|
|
220
449
|
cat <<'EOF'
|
|
221
|
-
Usage: aiur [--interactive] [--max-agents <n>] [--logs-root <path>] [--port <port>] [--host <host>] [path
|
|
222
|
-
aiur
|
|
223
|
-
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
|
|
224
454
|
aiur stop stop the running session
|
|
455
|
+
aiur restart [--no-build] [run flags] stop, refresh the build, start again (detached)
|
|
225
456
|
aiur status show agent status
|
|
226
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]
|
|
464
|
+
aiur alerts [--needs-attention] show structured alert feed
|
|
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
|
|
227
471
|
aiur set max-agents <n> change the concurrent-agent cap at runtime
|
|
228
|
-
aiur pause
|
|
229
|
-
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
|
|
483
|
+
aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
|
|
230
484
|
aiur --version
|
|
231
485
|
EOF
|
|
232
486
|
}
|
|
@@ -235,6 +489,8 @@ EOF
|
|
|
235
489
|
|
|
236
490
|
run_version() {
|
|
237
491
|
resolve_release
|
|
492
|
+
AIUR_CLI_VERSION="$(cli_package_version || true)"
|
|
493
|
+
export AIUR_CLI_VERSION
|
|
238
494
|
# Distribution-free (like init): printing the version is a compile-time
|
|
239
495
|
# constant, so never claim the node name — otherwise `aiur --version` fails
|
|
240
496
|
# whenever an aiur session is already running.
|
|
@@ -256,16 +512,81 @@ run_init() {
|
|
|
256
512
|
exec "${release_cmd[@]}"
|
|
257
513
|
}
|
|
258
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
|
+
|
|
259
576
|
# --- interactive / background run -------------------------------------------
|
|
260
577
|
#
|
|
261
578
|
# mode=foreground attaches the UI and tears down on exit; mode=background leaves
|
|
262
579
|
# the detached tmux session running and returns.
|
|
263
580
|
|
|
264
|
-
# Load
|
|
265
|
-
#
|
|
266
|
-
# 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.
|
|
267
583
|
load_dotenv() {
|
|
268
|
-
|
|
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
|
|
269
590
|
[ -f "$file" ] || return 0
|
|
270
591
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
271
592
|
line="${line#"${line%%[![:space:]]*}"}"
|
|
@@ -285,6 +606,56 @@ load_dotenv() {
|
|
|
285
606
|
done <"$file"
|
|
286
607
|
}
|
|
287
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
|
+
|
|
288
659
|
run_session() {
|
|
289
660
|
local mode="$1"
|
|
290
661
|
shift
|
|
@@ -311,34 +682,25 @@ run_session() {
|
|
|
311
682
|
# Pick up GITHUB_TOKEN / dashboard creds the wizard wrote to ./.env so the
|
|
312
683
|
# running tracker can authenticate. Shell exports still take precedence.
|
|
313
684
|
load_dotenv
|
|
685
|
+
scrub_run_only_env
|
|
686
|
+
export AIUR_DEFAULT_DASHBOARD_HOST="$(default_dashboard_host)"
|
|
314
687
|
|
|
315
688
|
init_argv_file
|
|
316
689
|
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
# (status/agents/message/pause/set).
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
for
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
--i-understand-that-this-will-be-running-without-the-usual-guardrails) has_ack=1 ;;
|
|
330
|
-
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
|
|
331
702
|
done
|
|
332
|
-
|
|
333
|
-
[ "$has_host" -eq 1 ] || injected+=(--host 127.0.0.1)
|
|
334
|
-
if [ "$mode" = "background" ] && [ "$has_interactive" -eq 0 ]; then
|
|
335
|
-
[ "$has_headless" -eq 1 ] || injected+=(--headless)
|
|
336
|
-
else
|
|
337
|
-
[ "$has_interactive" -eq 1 ] || injected+=(--interactive)
|
|
338
|
-
fi
|
|
339
|
-
[ "$has_ack" -eq 1 ] || injected+=(--i-understand-that-this-will-be-running-without-the-usual-guardrails)
|
|
340
|
-
|
|
341
|
-
write_argv "${injected[@]}" "$@"
|
|
703
|
+
write_argv "${run_argv[@]}"
|
|
342
704
|
export AIUR_ARGV_FILE="$argv_file"
|
|
343
705
|
|
|
344
706
|
prepare_distribution
|
|
@@ -355,6 +717,8 @@ run_session() {
|
|
|
355
717
|
conf="$(resolve_tmux_conf)"
|
|
356
718
|
[ -f "$conf" ] || die "tmux conf not found at $conf"
|
|
357
719
|
|
|
720
|
+
preflight_stale_manual_smoke
|
|
721
|
+
|
|
358
722
|
mkdir -p "$AIUR_BG_STATE_DIR"
|
|
359
723
|
printf '%s\n' "$session" >"$AIUR_BG_STATE_DIR/state"
|
|
360
724
|
export AIUR_TMUX_SESSION="$session"
|
|
@@ -372,6 +736,11 @@ run_session() {
|
|
|
372
736
|
export AIUR_AGENT_TMPFILE="${session_root}/aiur-${$}-agents"
|
|
373
737
|
: >"$AIUR_AGENT_TMPFILE"
|
|
374
738
|
|
|
739
|
+
# Workspace-root handoff for the shell cwd-sweep backstop. The BEAM writes the
|
|
740
|
+
# resolved Aiur.Config.workspace_root() here after config loads.
|
|
741
|
+
export AIUR_WORKSPACE_ROOT_FILE="${session_root}/aiur-${$}-workspace-root"
|
|
742
|
+
: >"$AIUR_WORKSPACE_ROOT_FILE"
|
|
743
|
+
|
|
375
744
|
# Background runs persist under a known run log dir so (a) the BEAM-death
|
|
376
745
|
# watchdog, which outlives the BEAM, can drop a crash record next to aiur.log,
|
|
377
746
|
# and (b) the post-start boot capture is not lost to /tmp when the launcher
|
|
@@ -382,6 +751,17 @@ run_session() {
|
|
|
382
751
|
export AIUR_LOGS_ROOT
|
|
383
752
|
fi
|
|
384
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
|
+
|
|
385
765
|
# Capture sink for BEAM startup (and, in background mode, the whole run's
|
|
386
766
|
# boot stdout/stderr). Foreground uses a throwaway tempfile; background points
|
|
387
767
|
# at a durable file in the run log dir. The dir/file is created lazily just
|
|
@@ -401,13 +781,15 @@ run_session() {
|
|
|
401
781
|
{
|
|
402
782
|
printf '#!/usr/bin/env bash\n'
|
|
403
783
|
printf 'set -o pipefail\n'
|
|
784
|
+
printf 'unset AIUR_CI_READINESS_TOKEN\n'
|
|
404
785
|
printf 'cd %q || exit 1\n' "$PWD"
|
|
405
786
|
local v
|
|
406
787
|
for v in AIUR_RELEASE_DIR AIUR_ARGV_FILE RELEASE_DISTRIBUTION RELEASE_NODE \
|
|
407
788
|
RELEASE_COOKIE ERL_AFLAGS ERL_EPMD_ADDRESS AIUR_NODE AIUR_ERLANG_COOKIE \
|
|
408
789
|
AIUR_TMUX_SESSION AIUR_TMUX_SOCKET AIUR_TMUX_CONF AIUR_BIN \
|
|
409
|
-
AIUR_SESSION_TMPFILE AIUR_AGENT_TMPFILE
|
|
410
|
-
AIUR_OPENCODE_BRIDGE_PORT AIUR_DEBUG
|
|
790
|
+
AIUR_SESSION_TMPFILE AIUR_AGENT_TMPFILE AIUR_WORKSPACE_ROOT_FILE \
|
|
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
|
|
411
793
|
if [ -n "${!v:-}" ]; then printf 'export %s=%q\n' "$v" "${!v}"; fi
|
|
412
794
|
done
|
|
413
795
|
printf 'capture=%q\n' "$startup_capture"
|
|
@@ -424,7 +806,8 @@ run_session() {
|
|
|
424
806
|
_session_socket="$socket" _session_name="$session" _session_conf="$conf" \
|
|
425
807
|
_session_tmpfile="$AIUR_SESSION_TMPFILE" _session_capture="$startup_capture" \
|
|
426
808
|
_session_argv="$argv_file" _session_release="$release_dir" _session_tmux="$tmux_bin" \
|
|
427
|
-
_session_node="$AIUR_RELEASE_NODE" _session_pidfile="$AIUR_AGENT_TMPFILE"
|
|
809
|
+
_session_node="$AIUR_RELEASE_NODE" _session_pidfile="$AIUR_AGENT_TMPFILE" \
|
|
810
|
+
_session_workspace_root_file="$AIUR_WORKSPACE_ROOT_FILE"
|
|
428
811
|
install_foreground_traps
|
|
429
812
|
fi
|
|
430
813
|
|
|
@@ -433,10 +816,14 @@ run_session() {
|
|
|
433
816
|
# session whose BEAM/control plane is gone should be reclaimed before retry.
|
|
434
817
|
if [ "$mode" = "background" ] && "$tmux_bin" -L "$socket" -f "$conf" has-session -t "$session" 2>/dev/null; then
|
|
435
818
|
if [ "$(probe_control_liveness)" = "up" ]; then
|
|
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")
|
|
436
823
|
echo "aiur is already running in the background (tmux session ${session})." >&2
|
|
437
824
|
echo "Use: aiur status # inspect agents" >&2
|
|
438
825
|
echo "Use: aiur stop # stop it before starting a fresh session" >&2
|
|
439
|
-
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" 2>/dev/null || true
|
|
826
|
+
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" "$AIUR_WORKSPACE_ROOT_FILE" 2>/dev/null || true
|
|
440
827
|
return 0
|
|
441
828
|
fi
|
|
442
829
|
|
|
@@ -474,7 +861,7 @@ run_session() {
|
|
|
474
861
|
-x "${COLUMNS:-200}" -y "${LINES:-50}" "$inner_cmd"; then
|
|
475
862
|
echo "❌ aiur failed to start; captured output:" >&2
|
|
476
863
|
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
477
|
-
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" 2>/dev/null || true
|
|
864
|
+
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" "$AIUR_WORKSPACE_ROOT_FILE" 2>/dev/null || true
|
|
478
865
|
exit 1
|
|
479
866
|
fi
|
|
480
867
|
|
|
@@ -483,11 +870,11 @@ run_session() {
|
|
|
483
870
|
# chat panes it opens. Best-effort — a missing title just shows the default.
|
|
484
871
|
"$tmux_bin" -L "$socket" -f "$conf" select-pane -t "$session" -T "AIUR Agents" 2>/dev/null || true
|
|
485
872
|
|
|
486
|
-
# Grace window: surface a boot crash instead of attaching to nothing.
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
|
|
490
|
-
|
|
873
|
+
# Grace window: surface a boot crash instead of attaching to nothing. Prove
|
|
874
|
+
# the control plane can answer before declaring startup usable: background
|
|
875
|
+
# control commands depend on that RPC path, and foreground automation must not
|
|
876
|
+
# treat a pre-TUI application crash as a successful `tmux attach`.
|
|
877
|
+
local require_control=1
|
|
491
878
|
if ! wait_for_session_startup "$tmux_bin" "$socket" "$conf" "$session" "$startup_capture" "$require_control"; then
|
|
492
879
|
if [ "$mode" = "background" ]; then
|
|
493
880
|
"$tmux_bin" -L "$socket" -f "$conf" kill-session -t "$session" 2>/dev/null || true
|
|
@@ -495,10 +882,17 @@ run_session() {
|
|
|
495
882
|
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
496
883
|
fi
|
|
497
884
|
[ "$mode" = "foreground" ] && return 1
|
|
498
|
-
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" 2>/dev/null || true
|
|
885
|
+
rm -f "$startup_capture" "$argv_file" "$launcher" "$AIUR_SESSION_TMPFILE" "$AIUR_AGENT_TMPFILE" "$AIUR_WORKSPACE_ROOT_FILE" 2>/dev/null || true
|
|
499
886
|
exit 1
|
|
500
887
|
fi
|
|
501
888
|
|
|
889
|
+
write_aiur_instance_record "$session" "$socket"
|
|
890
|
+
print_dashboard_status "$no_dashboard" "$startup_capture"
|
|
891
|
+
|
|
892
|
+
if [ "$mode" = "foreground" ]; then
|
|
893
|
+
echo "aiur foreground tmux socket ${socket}, session ${session}" >&2
|
|
894
|
+
fi
|
|
895
|
+
|
|
502
896
|
if [ "$mode" = "background" ]; then
|
|
503
897
|
# Fresh run: drop any stale crash/stop state from a prior dead instance so
|
|
504
898
|
# `status` doesn't report a phantom orphan and the watchdog starts clean.
|
|
@@ -508,9 +902,9 @@ run_session() {
|
|
|
508
902
|
background_watchdog_pid="$(start_beam_death_watchdog \
|
|
509
903
|
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1 1 \
|
|
510
904
|
"$AIUR_RELEASE_NODE" "${AIUR_LOGS_ROOT:-}" \
|
|
511
|
-
"$(aiur_stop_sentinel_path)" "$(aiur_crash_marker_path)")"
|
|
905
|
+
"$(aiur_stop_sentinel_path)" "$(aiur_crash_marker_path)" "$AIUR_WORKSPACE_ROOT_FILE")"
|
|
512
906
|
disown "$background_watchdog_pid" 2>/dev/null || true
|
|
513
|
-
echo "aiur started in the background (tmux session ${session}). Attach with: aiur" >&2
|
|
907
|
+
echo "aiur started in the background (tmux socket ${socket}, session ${session}). Attach with: aiur" >&2
|
|
514
908
|
# Keep $startup_capture (boot.out.log) for the run's lifetime; only the
|
|
515
909
|
# transient argv file is no longer needed.
|
|
516
910
|
rm -f "$argv_file" 2>/dev/null || true
|
|
@@ -524,11 +918,19 @@ run_session() {
|
|
|
524
918
|
# the session, and unblocks the attach. It polls the node name rather than a
|
|
525
919
|
# captured pid, so another Aiur instance from the same release cannot hold it open.
|
|
526
920
|
_session_watchdog_pid="$(start_beam_death_watchdog \
|
|
527
|
-
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1
|
|
921
|
+
"-name ${AIUR_RELEASE_NODE}" "$socket" "$AIUR_AGENT_TMPFILE" 1 0 \
|
|
922
|
+
"" "" "" "" "$AIUR_WORKSPACE_ROOT_FILE")"
|
|
528
923
|
|
|
529
924
|
# Foreground: attach the UI. Do not exec — that would drop the teardown trap.
|
|
530
|
-
|
|
531
|
-
|
|
925
|
+
# Avoid process substitution here: some sandboxed non-TTY launchers reject
|
|
926
|
+
# opening /dev/fd/* during the real manual-test wrapper path.
|
|
927
|
+
local attach_stderr attach_code
|
|
928
|
+
attach_stderr="$(mktemp "${TMPDIR:-/tmp}/aiur-attach-stderr.XXXXXX")"
|
|
929
|
+
"$tmux_bin" -L "$socket" -f "$conf" attach -t "$session" 2>"$attach_stderr"
|
|
930
|
+
attach_code=$?
|
|
931
|
+
grep -v -F "[server exited]" "$attach_stderr" >&2 || true
|
|
932
|
+
rm -f "$attach_stderr" 2>/dev/null || true
|
|
933
|
+
return "$attach_code"
|
|
532
934
|
}
|
|
533
935
|
|
|
534
936
|
resolve_tmux_conf() {
|
|
@@ -563,6 +965,237 @@ kill_beams_matching() {
|
|
|
563
965
|
for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true; done
|
|
564
966
|
}
|
|
565
967
|
|
|
968
|
+
pid_owner() {
|
|
969
|
+
ps -p "$1" -o user= 2>/dev/null | awk '{print $1}' || true
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
pid_command() {
|
|
973
|
+
ps -p "$1" -o command= 2>/dev/null || true
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
pid_ppid() {
|
|
977
|
+
ps -p "$1" -o ppid= 2>/dev/null | awk '{print $1}' || true
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
pid_cwd() {
|
|
981
|
+
local pid="$1" cwd
|
|
982
|
+
if [ -e "/proc/$pid/cwd" ]; then
|
|
983
|
+
readlink "/proc/$pid/cwd" 2>/dev/null || true
|
|
984
|
+
return 0
|
|
985
|
+
fi
|
|
986
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
987
|
+
cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 || true)"
|
|
988
|
+
[ -n "$cwd" ] && printf '%s\n' "$cwd"
|
|
989
|
+
fi
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
kill_pid_with_escalation() {
|
|
993
|
+
local pid="$1" waited=0
|
|
994
|
+
[ -n "$pid" ] || return 0
|
|
995
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
996
|
+
while [ "$waited" -lt 20 ]; do
|
|
997
|
+
kill -0 "$pid" 2>/dev/null || return 0
|
|
998
|
+
sleep 0.1
|
|
999
|
+
waited=$((waited + 1))
|
|
1000
|
+
done
|
|
1001
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
aiur_node_from_command() {
|
|
1005
|
+
local cmd="$1" node
|
|
1006
|
+
node="$(printf '%s\n' "$cmd" | sed -n "s/.*--name[[:space:]]\\(aiur-${USER}[-A-Za-z0-9_]*@127\\.0\\.0\\.1\\).*/\\1/p" | head -n 1)"
|
|
1007
|
+
[ -n "$node" ] && printf '%s\n' "$node"
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
aiur_release_root_from_command() {
|
|
1011
|
+
local cmd="$1"
|
|
1012
|
+
if [[ "$cmd" =~ --boot-var[[:space:]]+RELEASE_LIB[[:space:]]+([^[:space:]]+)/lib ]]; then
|
|
1013
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
1014
|
+
return 0
|
|
1015
|
+
fi
|
|
1016
|
+
if [[ "$cmd" =~ ([^[:space:]]*/src/_build/dev/rel/aiur)/releases/ ]]; then
|
|
1017
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
1018
|
+
return 0
|
|
1019
|
+
fi
|
|
1020
|
+
if [[ "$cmd" =~ ([^[:space:]]*/release)/releases/ ]]; then
|
|
1021
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
1022
|
+
return 0
|
|
1023
|
+
fi
|
|
1024
|
+
return 1
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
workspace_root_from_aiur_release_root() {
|
|
1028
|
+
local release_root="${1%/}"
|
|
1029
|
+
case "$release_root" in
|
|
1030
|
+
*/src/_build/dev/rel/aiur)
|
|
1031
|
+
printf '%s\n' "${release_root%/src/_build/dev/rel/aiur}"
|
|
1032
|
+
;;
|
|
1033
|
+
*)
|
|
1034
|
+
printf '%s\n' "$release_root"
|
|
1035
|
+
;;
|
|
1036
|
+
esac
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
aiur_node_tmux_session_alive() {
|
|
1040
|
+
local node="$1" short tmux_bin
|
|
1041
|
+
short="${node%@*}"
|
|
1042
|
+
tmux_bin="$(command -v tmux || true)"
|
|
1043
|
+
[ -n "$tmux_bin" ] || return 1
|
|
1044
|
+
"$tmux_bin" -L "$short" has-session -t "${short}-default" 2>/dev/null
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
stale_manual_smoke_beam_inventory() {
|
|
1048
|
+
local pid cmd node release_root workspace_root owner
|
|
1049
|
+
aiur_resolve_identity
|
|
1050
|
+
|
|
1051
|
+
for pid in $(pgrep -f -- "beam\\.smp.*--name aiur-${USER}.*@127\\.0\\.0\\.1" 2>/dev/null || true); do
|
|
1052
|
+
[ -n "$pid" ] || continue
|
|
1053
|
+
owner="$(pid_owner "$pid")"
|
|
1054
|
+
[ "$owner" = "$USER" ] || continue
|
|
1055
|
+
|
|
1056
|
+
cmd="$(pid_command "$pid")"
|
|
1057
|
+
node="$(aiur_node_from_command "$cmd")"
|
|
1058
|
+
[ -n "$node" ] || continue
|
|
1059
|
+
|
|
1060
|
+
release_root="$(aiur_release_root_from_command "$cmd" || true)"
|
|
1061
|
+
[ -n "$release_root" ] || continue
|
|
1062
|
+
workspace_root="$(workspace_root_from_aiur_release_root "$release_root")"
|
|
1063
|
+
|
|
1064
|
+
# Scope this broad cleanup to stale issue/manual-smoke workspaces. The
|
|
1065
|
+
# current instance cleanup path handles the active daemon's own node.
|
|
1066
|
+
case "$workspace_root" in
|
|
1067
|
+
*/aiur-workspaces/*) ;;
|
|
1068
|
+
*) continue ;;
|
|
1069
|
+
esac
|
|
1070
|
+
|
|
1071
|
+
# A matching live aiur tmux session means this node may still be in use.
|
|
1072
|
+
aiur_node_tmux_session_alive "$node" && continue
|
|
1073
|
+
|
|
1074
|
+
printf '%s\t%s\t%s\t%s\n' "$pid" "$node" "$workspace_root" "$release_root"
|
|
1075
|
+
done
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
manual_smoke_wrapper_tmux_sockets() {
|
|
1079
|
+
local tmux_bin sockdir path name panes current pane_path start manual_context all_sleep
|
|
1080
|
+
tmux_bin="$(command -v tmux || true)"
|
|
1081
|
+
[ -n "$tmux_bin" ] || return 0
|
|
1082
|
+
sockdir="${TMUX_TMPDIR:-/tmp}/tmux-$(id -u)"
|
|
1083
|
+
[ -d "$sockdir" ] || return 0
|
|
1084
|
+
|
|
1085
|
+
for path in "$sockdir"/*; do
|
|
1086
|
+
[ -S "$path" ] || continue
|
|
1087
|
+
name="${path##*/}"
|
|
1088
|
+
case "$name" in
|
|
1089
|
+
codex-driver-* | codex-aiur* | codex-[0-9]* | claude-driver*) ;;
|
|
1090
|
+
*) continue ;;
|
|
1091
|
+
esac
|
|
1092
|
+
panes="$("$tmux_bin" -L "$name" list-panes -a -F '#{pane_current_command} #{pane_current_path} #{pane_start_command}' 2>/dev/null || true)"
|
|
1093
|
+
[ -n "$panes" ] || continue
|
|
1094
|
+
manual_context=0
|
|
1095
|
+
all_sleep=1
|
|
1096
|
+
while IFS=$'\t' read -r current pane_path start; do
|
|
1097
|
+
case "$start" in
|
|
1098
|
+
*aiurdev\ --test* | *scripts/aiurdev\ --test*) manual_context=1 ;;
|
|
1099
|
+
esac
|
|
1100
|
+
[ "$current" = "sleep" ] || all_sleep=0
|
|
1101
|
+
done <<<"$panes"
|
|
1102
|
+
[ "$manual_context" -eq 1 ] && [ "$all_sleep" -eq 1 ] && printf '%s\n' "$name"
|
|
1103
|
+
done
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
orphaned_opencode_attach_inventory() {
|
|
1107
|
+
local pid owner ppid cmd cwd
|
|
1108
|
+
for pid in $(pgrep -f -- "opencode attach http://127\\.0\\.0\\.1:" 2>/dev/null || true); do
|
|
1109
|
+
owner="$(pid_owner "$pid")"
|
|
1110
|
+
[ "$owner" = "$USER" ] || continue
|
|
1111
|
+
ppid="$(pid_ppid "$pid")"
|
|
1112
|
+
[ "$ppid" = "1" ] || continue
|
|
1113
|
+
cwd="$(pid_cwd "$pid")"
|
|
1114
|
+
case "$cwd" in
|
|
1115
|
+
*/aiur-workspaces/*) ;;
|
|
1116
|
+
*) continue ;;
|
|
1117
|
+
esac
|
|
1118
|
+
cmd="$(pid_command "$pid")"
|
|
1119
|
+
case "$cmd" in
|
|
1120
|
+
*"opencode attach http://127.0.0.1:"*) printf '%s\t%s\t%s\n' "$pid" "$cwd" "$cmd" ;;
|
|
1121
|
+
esac
|
|
1122
|
+
done
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
report_stale_manual_smoke() {
|
|
1126
|
+
local found=0 pid node workspace_root release_root socket cwd cmd
|
|
1127
|
+
|
|
1128
|
+
while IFS=$'\t' read -r pid node workspace_root release_root; do
|
|
1129
|
+
[ -n "$pid" ] || continue
|
|
1130
|
+
if [ "$found" -eq 0 ]; then
|
|
1131
|
+
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
1132
|
+
found=1
|
|
1133
|
+
fi
|
|
1134
|
+
echo " BEAM pid=${pid} node=${node} workspace=${workspace_root} release=${release_root}" >&2
|
|
1135
|
+
done < <(stale_manual_smoke_beam_inventory)
|
|
1136
|
+
|
|
1137
|
+
while IFS= read -r socket; do
|
|
1138
|
+
[ -n "$socket" ] || continue
|
|
1139
|
+
if [ "$found" -eq 0 ]; then
|
|
1140
|
+
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
1141
|
+
found=1
|
|
1142
|
+
fi
|
|
1143
|
+
echo " wrapper tmux socket=${socket}" >&2
|
|
1144
|
+
done < <(manual_smoke_wrapper_tmux_sockets)
|
|
1145
|
+
|
|
1146
|
+
while IFS=$'\t' read -r pid cwd cmd; do
|
|
1147
|
+
[ -n "$pid" ] || continue
|
|
1148
|
+
if [ "$found" -eq 0 ]; then
|
|
1149
|
+
echo "aiur: stale manual-smoke leftovers detected:" >&2
|
|
1150
|
+
found=1
|
|
1151
|
+
fi
|
|
1152
|
+
echo " orphan opencode attach pid=${pid} workspace=${cwd}" >&2
|
|
1153
|
+
done < <(orphaned_opencode_attach_inventory)
|
|
1154
|
+
|
|
1155
|
+
if [ "$found" -eq 1 ]; then
|
|
1156
|
+
echo "aiur: run 'aiur cleanup-stale' to TERM/KILL only these same-user Aiur leftovers." >&2
|
|
1157
|
+
fi
|
|
1158
|
+
|
|
1159
|
+
return "$found"
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
preflight_stale_manual_smoke() {
|
|
1163
|
+
report_stale_manual_smoke >/dev/null || true
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
reap_stale_manual_smoke() {
|
|
1167
|
+
local include_wrappers="${1:-0}"
|
|
1168
|
+
local pid node workspace_root release_root socket cwd cmd tmux_bin reaped=0
|
|
1169
|
+
|
|
1170
|
+
while IFS=$'\t' read -r pid node workspace_root release_root; do
|
|
1171
|
+
[ -n "$pid" ] || continue
|
|
1172
|
+
echo "aiur: reaping stale BEAM pid=${pid} node=${node} workspace=${workspace_root}" >&2
|
|
1173
|
+
kill_pid_with_escalation "$pid"
|
|
1174
|
+
reaped=$((reaped + 1))
|
|
1175
|
+
done < <(stale_manual_smoke_beam_inventory)
|
|
1176
|
+
|
|
1177
|
+
tmux_bin="$(command -v tmux || true)"
|
|
1178
|
+
if [ "$include_wrappers" = "1" ] && [ -n "$tmux_bin" ]; then
|
|
1179
|
+
while IFS= read -r socket; do
|
|
1180
|
+
[ -n "$socket" ] || continue
|
|
1181
|
+
echo "aiur: reaping stale wrapper tmux socket=${socket}" >&2
|
|
1182
|
+
"$tmux_bin" -L "$socket" kill-server 2>/dev/null || true
|
|
1183
|
+
reaped=$((reaped + 1))
|
|
1184
|
+
done < <(manual_smoke_wrapper_tmux_sockets)
|
|
1185
|
+
fi
|
|
1186
|
+
|
|
1187
|
+
while IFS=$'\t' read -r pid cwd cmd; do
|
|
1188
|
+
[ -n "$pid" ] || continue
|
|
1189
|
+
echo "aiur: reaping orphan opencode attach pid=${pid}" >&2
|
|
1190
|
+
kill_pid_with_escalation "$pid"
|
|
1191
|
+
reaped=$((reaped + 1))
|
|
1192
|
+
done < <(orphaned_opencode_attach_inventory)
|
|
1193
|
+
|
|
1194
|
+
if [ "$reaped" -eq 0 ]; then
|
|
1195
|
+
echo "aiur: no stale manual-smoke leftovers found" >&2
|
|
1196
|
+
fi
|
|
1197
|
+
}
|
|
1198
|
+
|
|
566
1199
|
# Echo $1 and every descendant pid, depth-first. Mac-safe (`pgrep -P`, no
|
|
567
1200
|
# /proc), mirroring Aiur.RemoteControl.collect_descendants so the launcher reaps
|
|
568
1201
|
# the same tree the BEAM-side reaper would. The recorded agent pid is a bash
|
|
@@ -596,7 +1229,7 @@ agent_pid_matches() {
|
|
|
596
1229
|
#
|
|
597
1230
|
# tmux runs on aiur's PRIVATE socket (-L aiur-$USER), so kill-server tears down
|
|
598
1231
|
# every REPL/chat pane agent in one shot AND leaves no live aiur tmux server —
|
|
599
|
-
# never touching the
|
|
1232
|
+
# never touching the Executor’s own default tmux. Headless agents (claude/codex
|
|
600
1233
|
# app-servers spawned via Port) are bare OS processes that reparent to init on a
|
|
601
1234
|
# BEAM crash; kill-server can't see them, so they're reaped from the pidfile by
|
|
602
1235
|
# process tree, comm-guarded against pid reuse. Idempotent.
|
|
@@ -643,6 +1276,35 @@ aiur_state_slug() {
|
|
|
643
1276
|
printf '%s' "${AIUR_RELEASE_NODE:-aiur}" | tr -c 'A-Za-z0-9._-' '_'
|
|
644
1277
|
}
|
|
645
1278
|
|
|
1279
|
+
aiur_instances_dir() {
|
|
1280
|
+
printf '%s/instances' "${AIUR_BG_STATE_DIR:?}"
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
aiur_instance_record_path() {
|
|
1284
|
+
printf '%s/%s.instance' "$(aiur_instances_dir)" "$(aiur_state_slug)"
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
write_aiur_instance_record() {
|
|
1288
|
+
local session="$1" socket="$2" record_dir record tmp root
|
|
1289
|
+
record_dir="$(aiur_instances_dir)"
|
|
1290
|
+
record="$(aiur_instance_record_path)"
|
|
1291
|
+
root="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
|
|
1292
|
+
mkdir -p "$record_dir" 2>/dev/null || return 0
|
|
1293
|
+
tmp="$(mktemp "$record_dir/.${AIUR_INSTANCE_KEY:-aiur}.XXXXXX" 2>/dev/null)" || return 0
|
|
1294
|
+
{
|
|
1295
|
+
printf 'AIUR_RECORD_NODE=%q\n' "$AIUR_RELEASE_NODE"
|
|
1296
|
+
printf 'AIUR_RECORD_INSTANCE_KEY=%q\n' "$AIUR_INSTANCE_KEY"
|
|
1297
|
+
printf 'AIUR_RECORD_SESSION=%q\n' "$session"
|
|
1298
|
+
printf 'AIUR_RECORD_SOCKET=%q\n' "$socket"
|
|
1299
|
+
printf 'AIUR_RECORD_WORKSPACE_ROOT_FILE=%q\n' "${AIUR_WORKSPACE_ROOT_FILE:-}"
|
|
1300
|
+
printf 'AIUR_RECORD_PROJECT_ROOT=%q\n' "$root"
|
|
1301
|
+
printf 'AIUR_RECORD_PROJECT_ROOT_SOURCE=%q\n' "${AIUR_PROJECT_ROOT_SOURCE:-}"
|
|
1302
|
+
printf 'AIUR_RECORD_WRITTEN_AT=%q\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
|
|
1303
|
+
} >"$tmp" || { rm -f "$tmp" 2>/dev/null || true; return 0; }
|
|
1304
|
+
chmod 0600 "$tmp" 2>/dev/null || true
|
|
1305
|
+
mv "$tmp" "$record" 2>/dev/null || rm -f "$tmp" 2>/dev/null || true
|
|
1306
|
+
}
|
|
1307
|
+
|
|
646
1308
|
# Marker `status` reads to tell "daemon crashed, agents may be orphaned" apart
|
|
647
1309
|
# from "nothing was ever running". Written by the watchdog on an unexpected exit,
|
|
648
1310
|
# cleared on a clean stop and on the next successful background start.
|
|
@@ -690,24 +1352,128 @@ record_beam_crash() {
|
|
|
690
1352
|
fi
|
|
691
1353
|
}
|
|
692
1354
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1355
|
+
canonical_workspace_root() {
|
|
1356
|
+
local root="$1"
|
|
1357
|
+
if [ -d "$root" ]; then
|
|
1358
|
+
(cd "$root" 2>/dev/null && pwd -P) || printf '%s\n' "$root"
|
|
1359
|
+
else
|
|
1360
|
+
printf '%s\n' "$root"
|
|
1361
|
+
fi
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
workspace_root_is_shallow() {
|
|
1365
|
+
local root="${1%/}" trimmed
|
|
1366
|
+
trimmed="${root#/}"
|
|
1367
|
+
[ -n "$trimmed" ] || return 0
|
|
1368
|
+
case "$trimmed" in
|
|
1369
|
+
*/*) return 1 ;;
|
|
1370
|
+
*) return 0 ;;
|
|
1371
|
+
esac
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
workspace_cwd_pids() {
|
|
1375
|
+
local root="${1%/}" proc_dir="${2:-/proc}" entry pid cwd
|
|
1376
|
+
[ -d "$proc_dir" ] || return 0
|
|
1377
|
+
|
|
1378
|
+
for entry in "$proc_dir"/[0-9]*; do
|
|
1379
|
+
[ -d "$entry" ] || continue
|
|
1380
|
+
pid="${entry##*/}"
|
|
1381
|
+
cwd="$(readlink "$entry/cwd" 2>/dev/null || true)"
|
|
1382
|
+
[ -n "$cwd" ] || continue
|
|
1383
|
+
cwd="${cwd%/}"
|
|
1384
|
+
if [ "$cwd" != "$root" ] && [[ "$cwd/" == "$root/"* ]]; then
|
|
1385
|
+
printf '%s\n' "$pid"
|
|
1386
|
+
fi
|
|
1387
|
+
done
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
shell_protected_pid_list() {
|
|
1391
|
+
local p
|
|
1392
|
+
printf '%s\n' "$$"
|
|
1393
|
+
[ "${BASHPID:-$$}" = "$$" ] || printf '%s\n' "$BASHPID"
|
|
1394
|
+
[ -n "${PPID:-}" ] && printf '%s\n' "$PPID"
|
|
1395
|
+
for p in $(agent_pid_tree "$$" 2>/dev/null || true); do
|
|
1396
|
+
printf '%s\n' "$p"
|
|
1397
|
+
done
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
reap_workspace_cwd_agents() {
|
|
1401
|
+
local root="${1:-}"
|
|
1402
|
+
[ -n "$root" ] || return 0
|
|
1403
|
+
[ -d /proc ] || return 0
|
|
1404
|
+
|
|
1405
|
+
root="$(canonical_workspace_root "$root")"
|
|
1406
|
+
if workspace_root_is_shallow "$root"; then
|
|
1407
|
+
echo "⚠️ refusing shallow workspace cwd sweep root: $root" >&2
|
|
1408
|
+
return 0
|
|
1409
|
+
fi
|
|
1410
|
+
|
|
1411
|
+
local max_sweeps="${AIUR_WORKSPACE_REAP_SWEEPS:-6}"
|
|
1412
|
+
case "$max_sweeps" in '' | *[!0-9]*) max_sweeps=6 ;; esac
|
|
1413
|
+
[ "$max_sweeps" -gt 0 ] || return 0
|
|
1414
|
+
|
|
1415
|
+
local protected pids=() filtered=() pid p sweep waited any
|
|
1416
|
+
protected=" $(shell_protected_pid_list | tr '\n' ' ') "
|
|
1417
|
+
|
|
1418
|
+
sweep=0
|
|
1419
|
+
while [ "$sweep" -lt "$max_sweeps" ]; do
|
|
1420
|
+
mapfile -t pids < <(workspace_cwd_pids "$root" /proc)
|
|
1421
|
+
filtered=()
|
|
1422
|
+
for pid in "${pids[@]}"; do
|
|
1423
|
+
case "$protected" in *" $pid "*) continue ;; esac
|
|
1424
|
+
kill -0 "$pid" 2>/dev/null && filtered+=("$pid")
|
|
1425
|
+
done
|
|
1426
|
+
|
|
1427
|
+
[ "${#filtered[@]}" -gt 0 ] || return 0
|
|
1428
|
+
|
|
1429
|
+
for p in "${filtered[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
1430
|
+
waited=0
|
|
1431
|
+
while [ "$waited" -lt 10 ]; do
|
|
1432
|
+
any=0
|
|
1433
|
+
for p in "${filtered[@]}"; do kill -0 "$p" 2>/dev/null && { any=1; break; }; done
|
|
1434
|
+
[ "$any" -eq 0 ] && break
|
|
1435
|
+
sleep 0.1
|
|
1436
|
+
waited=$((waited + 1))
|
|
1437
|
+
done
|
|
1438
|
+
for p in "${filtered[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
1439
|
+
|
|
1440
|
+
sweep=$((sweep + 1))
|
|
1441
|
+
sleep 0.1
|
|
1442
|
+
done
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
reap_workspace_cwd_from_file() {
|
|
1446
|
+
local root_file="${1:-}" root
|
|
1447
|
+
[ -n "$root_file" ] && [ -s "$root_file" ] || return 0
|
|
1448
|
+
IFS= read -r root <"$root_file" || root=""
|
|
1449
|
+
reap_workspace_cwd_agents "$root"
|
|
1450
|
+
}
|
|
1451
|
+
|
|
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"
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
# Background watchdog that survives the BEAM. Polls for the release BEAM by
|
|
1459
|
+
# command pattern and, once it has SEEN the BEAM and then the BEAM disappears
|
|
1460
|
+
# (orderly halt OR :emfile-style crash), reaps every agent. Polling the pattern
|
|
1461
|
+
# rather than a captured pid avoids two failure modes: a recycled BEAM pid the
|
|
1462
|
+
# watchdog would poll forever, and an empty pid at arm time that would silently
|
|
1463
|
+
# disarm the only crash reaper. The `seen` latch prevents a startup race from
|
|
1464
|
+
# reaping before the BEAM has come up. kill-server collapses the orphaned tmux
|
|
1465
|
+
# session, which returns the foreground `tmux attach` so the EXIT trap
|
|
1466
|
+
# (session_cleanup) runs its idempotent reap too.
|
|
1467
|
+
#
|
|
1468
|
+
# $1 beam_pattern $2 socket $3 pidfile $4 interval_s $5 initial_seen
|
|
1469
|
+
# $6 node $7 run_log_dir $8 stop_sentinel $9 crash_marker
|
|
1470
|
+
# $10 workspace_root_file
|
|
1471
|
+
# Args 6-9 arm crash recording (background mode). Omit them (foreground) and the
|
|
1472
|
+
# watchdog only reaps — a foreground BEAM death is already visible at the UI.
|
|
1473
|
+
# Prints the watchdog's own pid so the caller can kill it on a clean teardown.
|
|
1474
|
+
start_beam_death_watchdog() {
|
|
709
1475
|
local beam_pattern="$1" socket="$2" pidfile="$3" interval="${4:-1}" initial_seen="${5:-0}"
|
|
710
|
-
local node="${6:-}" run_log_dir="${7:-}" stop_sentinel="${8:-}" crash_marker="${9:-}"
|
|
1476
|
+
local node="${6:-}" run_log_dir="${7:-}" stop_sentinel="${8:-}" crash_marker="${9:-}" workspace_root_file="${10:-}"
|
|
711
1477
|
# Redirect the subshell's stdout so a command-substitution caller
|
|
712
1478
|
# (`pid=$(start_beam_death_watchdog ...)`) returns immediately instead of
|
|
713
1479
|
# blocking on the still-open pipe until the watchdog finishes.
|
|
@@ -730,13 +1496,17 @@ start_beam_death_watchdog() {
|
|
|
730
1496
|
record_beam_crash "$node" "$run_log_dir" "$crash_marker"
|
|
731
1497
|
fi
|
|
732
1498
|
reap_aiur_agents "$socket" "$pidfile"
|
|
1499
|
+
reap_workspace_cwd_from_file "$workspace_root_file"
|
|
733
1500
|
) >/dev/null 2>&1 &
|
|
734
1501
|
printf '%s\n' "$!"
|
|
735
1502
|
}
|
|
736
1503
|
|
|
737
1504
|
probe_control_liveness() {
|
|
738
1505
|
local expression output status
|
|
739
|
-
|
|
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'
|
|
740
1510
|
|
|
741
1511
|
set +e
|
|
742
1512
|
output="$("$release_bin" rpc "$expression" 2>&1)"
|
|
@@ -750,11 +1520,51 @@ probe_control_liveness() {
|
|
|
750
1520
|
fi
|
|
751
1521
|
}
|
|
752
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
|
+
|
|
753
1559
|
wait_for_session_startup() {
|
|
754
1560
|
local tmux_bin="$1" socket="$2" conf="$3" session="$4" startup_capture="$5" require_control="$6"
|
|
755
1561
|
local max_ticks="${AIUR_TMUX_GRACE_TICKS:-30}" tick control_state
|
|
756
1562
|
if [ "$require_control" = "1" ]; then
|
|
757
|
-
|
|
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}"
|
|
758
1568
|
fi
|
|
759
1569
|
|
|
760
1570
|
for ((tick = 0; tick < max_ticks; tick++)); do
|
|
@@ -778,7 +1588,16 @@ wait_for_session_startup() {
|
|
|
778
1588
|
done
|
|
779
1589
|
|
|
780
1590
|
if [ "$require_control" = "1" ]; then
|
|
781
|
-
|
|
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
|
|
782
1601
|
tail -n 30 "$startup_capture" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
783
1602
|
return 1
|
|
784
1603
|
fi
|
|
@@ -789,7 +1608,7 @@ wait_for_session_startup() {
|
|
|
789
1608
|
# Foreground teardown: kill the session + BEAM and reap opencode sessions on exit.
|
|
790
1609
|
_session_socket="" _session_name="" _session_conf="" _session_tmpfile=""
|
|
791
1610
|
_session_capture="" _session_argv="" _session_release="" _session_tmux="" _session_node=""
|
|
792
|
-
_session_pidfile="" _session_watchdog_pid=""
|
|
1611
|
+
_session_pidfile="" _session_watchdog_pid="" _session_workspace_root_file=""
|
|
793
1612
|
_cleanup_ran=0
|
|
794
1613
|
session_cleanup() {
|
|
795
1614
|
[ "$_cleanup_ran" = 1 ] && return 0
|
|
@@ -819,6 +1638,11 @@ session_cleanup() {
|
|
|
819
1638
|
# release dir, so release-path pgrep would terminate sibling workflows.
|
|
820
1639
|
[ -n "$_session_node" ] && kill_beams_matching "-name ${_session_node}"
|
|
821
1640
|
|
|
1641
|
+
# Final cwd-sweep backstop after the BEAM is gone: catches workspace-rooted
|
|
1642
|
+
# agents or test children that registered too late, reparented during the
|
|
1643
|
+
# BEAM-side sweep, or survived a bounded in-BEAM cleanup.
|
|
1644
|
+
reap_workspace_cwd_from_file "$_session_workspace_root_file"
|
|
1645
|
+
|
|
822
1646
|
# Reap the epmd our BEAM spawned for distribution so it doesn't linger after
|
|
823
1647
|
# exit. The node is dead by now, so `epmd -kill` succeeds; it safely refuses
|
|
824
1648
|
# while any *other* Erlang node is alive, so a daemon shared with an unrelated
|
|
@@ -846,7 +1670,8 @@ session_cleanup() {
|
|
|
846
1670
|
# tempfiles are spared (fresh + live pid) before the explicit rm below clears them.
|
|
847
1671
|
sweep_stale_tmp_artifacts || true
|
|
848
1672
|
|
|
849
|
-
rm -f "$_session_tmpfile" "$_session_capture" "$_session_argv" "$_session_pidfile" 2>/dev/null || true
|
|
1673
|
+
rm -f "$_session_tmpfile" "$_session_capture" "$_session_argv" "$_session_pidfile" "$_session_workspace_root_file" 2>/dev/null || true
|
|
1674
|
+
rm -f "$(aiur_instance_record_path)" 2>/dev/null || true
|
|
850
1675
|
return $code
|
|
851
1676
|
}
|
|
852
1677
|
install_foreground_traps() {
|
|
@@ -891,21 +1716,281 @@ probe_node_liveness() {
|
|
|
891
1716
|
printf 'unknown'
|
|
892
1717
|
}
|
|
893
1718
|
|
|
1719
|
+
probe_named_node_liveness() {
|
|
1720
|
+
local node="$1" old_node="${RELEASE_NODE:-}" state
|
|
1721
|
+
RELEASE_NODE="$node"
|
|
1722
|
+
state="$(probe_node_liveness)"
|
|
1723
|
+
RELEASE_NODE="$old_node"
|
|
1724
|
+
printf '%s' "$state"
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
load_aiur_instance_record() {
|
|
1728
|
+
local file="$1"
|
|
1729
|
+
AIUR_RECORD_NODE=""
|
|
1730
|
+
AIUR_RECORD_INSTANCE_KEY=""
|
|
1731
|
+
AIUR_RECORD_SESSION=""
|
|
1732
|
+
AIUR_RECORD_SOCKET=""
|
|
1733
|
+
AIUR_RECORD_WORKSPACE_ROOT_FILE=""
|
|
1734
|
+
AIUR_RECORD_PROJECT_ROOT=""
|
|
1735
|
+
AIUR_RECORD_PROJECT_ROOT_SOURCE=""
|
|
1736
|
+
[ -r "$file" ] || return 1
|
|
1737
|
+
# Records are written by this engine as KEY=%q lines under the user's state dir.
|
|
1738
|
+
# shellcheck disable=SC1090
|
|
1739
|
+
source "$file" 2>/dev/null || return 1
|
|
1740
|
+
[ -n "$AIUR_RECORD_NODE" ] && [ -n "$AIUR_RECORD_SESSION" ] && \
|
|
1741
|
+
[ -n "$AIUR_RECORD_SOCKET" ] && [ -n "$AIUR_RECORD_PROJECT_ROOT" ]
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
path_is_within_root() {
|
|
1745
|
+
local root child
|
|
1746
|
+
root="$(canonical_workspace_root "$1")"
|
|
1747
|
+
child="$(canonical_workspace_root "$2")"
|
|
1748
|
+
[ "$child" = "$root" ] || [[ "$child/" == "$root/"* ]]
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
AIUR_CONTROL_ADOPTED_RECORD=0
|
|
1752
|
+
AIUR_CONTROL_CURRENT_NODE_STATE=""
|
|
1753
|
+
AIUR_CONTROL_HINT_ROOTS=""
|
|
1754
|
+
AIUR_CONTROL_CALLER_ROOT=""
|
|
1755
|
+
AIUR_CONTROL_CALLER_NODE=""
|
|
1756
|
+
AIUR_CONTROL_CALLER_ROOT_SOURCE=""
|
|
1757
|
+
|
|
1758
|
+
resolve_control_identity_from_records() {
|
|
1759
|
+
AIUR_CONTROL_ADOPTED_RECORD=0
|
|
1760
|
+
AIUR_CONTROL_CURRENT_NODE_STATE="$(probe_node_liveness)"
|
|
1761
|
+
AIUR_CONTROL_HINT_ROOTS=""
|
|
1762
|
+
AIUR_CONTROL_CALLER_ROOT="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
|
|
1763
|
+
AIUR_CONTROL_CALLER_NODE="$AIUR_RELEASE_NODE"
|
|
1764
|
+
AIUR_CONTROL_CALLER_ROOT_SOURCE="${AIUR_PROJECT_ROOT_SOURCE:-}"
|
|
1765
|
+
|
|
1766
|
+
[ "$AIUR_CONTROL_CURRENT_NODE_STATE" = "down" ] || return 0
|
|
1767
|
+
[ "${AIUR_PROJECT_ROOT_SOURCE:-}" = "cwd" ] || return 0
|
|
1768
|
+
|
|
1769
|
+
local dir file state root match_count=0
|
|
1770
|
+
local match_node="" match_key="" match_session="" match_socket="" match_root="" match_source=""
|
|
1771
|
+
dir="$(aiur_instances_dir)"
|
|
1772
|
+
[ -d "$dir" ] || return 0
|
|
1773
|
+
|
|
1774
|
+
for file in "$dir"/*.instance; do
|
|
1775
|
+
[ -e "$file" ] || continue
|
|
1776
|
+
load_aiur_instance_record "$file" || continue
|
|
1777
|
+
state="$(probe_named_node_liveness "$AIUR_RECORD_NODE")"
|
|
1778
|
+
[ "$state" = "up" ] || continue
|
|
1779
|
+
|
|
1780
|
+
root="$(canonical_workspace_root "$AIUR_RECORD_PROJECT_ROOT")"
|
|
1781
|
+
case "$AIUR_CONTROL_HINT_ROOTS" in
|
|
1782
|
+
*"
|
|
1783
|
+
$root
|
|
1784
|
+
"*) ;;
|
|
1785
|
+
*) AIUR_CONTROL_HINT_ROOTS="${AIUR_CONTROL_HINT_ROOTS}${root}
|
|
1786
|
+
" ;;
|
|
1787
|
+
esac
|
|
1788
|
+
|
|
1789
|
+
path_is_within_root "$root" "$AIUR_CONTROL_CALLER_ROOT" || continue
|
|
1790
|
+
match_count=$((match_count + 1))
|
|
1791
|
+
match_node="$AIUR_RECORD_NODE"
|
|
1792
|
+
match_key="$AIUR_RECORD_INSTANCE_KEY"
|
|
1793
|
+
match_session="$AIUR_RECORD_SESSION"
|
|
1794
|
+
match_socket="$AIUR_RECORD_SOCKET"
|
|
1795
|
+
match_root="$root"
|
|
1796
|
+
match_source="$AIUR_RECORD_PROJECT_ROOT_SOURCE"
|
|
1797
|
+
done
|
|
1798
|
+
|
|
1799
|
+
[ "$match_count" -eq 1 ] || return 0
|
|
1800
|
+
|
|
1801
|
+
AIUR_RELEASE_NODE="$match_node"
|
|
1802
|
+
AIUR_INSTANCE_KEY="$match_key"
|
|
1803
|
+
AIUR_PROJECT_ROOT="$match_root"
|
|
1804
|
+
AIUR_PROJECT_ROOT_SOURCE="$match_source"
|
|
1805
|
+
AIUR_ADOPTED_TMUX_SESSION="$match_session"
|
|
1806
|
+
AIUR_ADOPTED_TMUX_SOCKET="$match_socket"
|
|
1807
|
+
export AIUR_RELEASE_NODE AIUR_INSTANCE_KEY AIUR_PROJECT_ROOT AIUR_PROJECT_ROOT_SOURCE \
|
|
1808
|
+
AIUR_ADOPTED_TMUX_SESSION AIUR_ADOPTED_TMUX_SOCKET
|
|
1809
|
+
RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
1810
|
+
export RELEASE_NODE
|
|
1811
|
+
AIUR_CONTROL_ADOPTED_RECORD=1
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
print_global_config_control_hint() {
|
|
1815
|
+
[ -n "${AIUR_CONTROL_HINT_ROOTS:-}" ] || return 0
|
|
1816
|
+
[ "${AIUR_CONTROL_CALLER_ROOT_SOURCE:-}" = "cwd" ] || return 0
|
|
1817
|
+
[ "${AIUR_CONTROL_CURRENT_NODE_STATE:-}" = "down" ] || return 0
|
|
1818
|
+
|
|
1819
|
+
echo "aiur: global-config control identity is keyed by cwd ${AIUR_CONTROL_CALLER_ROOT:-${AIUR_PROJECT_ROOT:-unknown}}" >&2
|
|
1820
|
+
echo "aiur: run control commands from the launch directory, or from a subdirectory of that launch directory" >&2
|
|
1821
|
+
echo "aiur: live launch directory candidate(s):" >&2
|
|
1822
|
+
printf '%s' "$AIUR_CONTROL_HINT_ROOTS" | sed '/^$/d; s/^/ /' >&2
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
control_rpc_timeout_seconds() {
|
|
1826
|
+
local seconds="${AIUR_CONTROL_RPC_TIMEOUT_SECONDS:-10}"
|
|
1827
|
+
case "$seconds" in
|
|
1828
|
+
'' | *[!0-9]* | 0) seconds=10 ;;
|
|
1829
|
+
esac
|
|
1830
|
+
printf '%s' "$seconds"
|
|
1831
|
+
}
|
|
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
|
+
|
|
1850
|
+
kill_control_rpc_process() {
|
|
1851
|
+
local pid="$1" grouped="$2" p tree=()
|
|
1852
|
+
[ -n "$pid" ] || return 0
|
|
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
|
+
|
|
1859
|
+
if [ "$grouped" = "1" ]; then
|
|
1860
|
+
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
1861
|
+
fi
|
|
1862
|
+
|
|
1863
|
+
for p in "${tree[@]}"; do kill -TERM "$p" 2>/dev/null || true; done
|
|
1864
|
+
sleep 0.2
|
|
1865
|
+
if [ "$grouped" = "1" ]; then
|
|
1866
|
+
kill -KILL "-$pid" 2>/dev/null || true
|
|
1867
|
+
fi
|
|
1868
|
+
for p in "${tree[@]}"; do kill -KILL "$p" 2>/dev/null || true; done
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
run_release_rpc_with_timeout() {
|
|
1872
|
+
local expression="$1" timeout output_file timeout_file pid watchdog_pid status grouped=0
|
|
1873
|
+
timeout="$(control_rpc_timeout_seconds)"
|
|
1874
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/aiur-control-rpc-output.XXXXXX")"
|
|
1875
|
+
timeout_file="$(mktemp "${TMPDIR:-/tmp}/aiur-control-rpc-timeout.XXXXXX")"
|
|
1876
|
+
rm -f "$timeout_file" 2>/dev/null || true
|
|
1877
|
+
|
|
1878
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
1879
|
+
setsid "$release_bin" rpc "$expression" >"$output_file" 2>&1 &
|
|
1880
|
+
grouped=1
|
|
1881
|
+
else
|
|
1882
|
+
"$release_bin" rpc "$expression" >"$output_file" 2>&1 &
|
|
1883
|
+
fi
|
|
1884
|
+
pid=$!
|
|
1885
|
+
|
|
1886
|
+
(
|
|
1887
|
+
sleep "$timeout"
|
|
1888
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
1889
|
+
: >"$timeout_file"
|
|
1890
|
+
kill_control_rpc_process "$pid" "$grouped"
|
|
1891
|
+
fi
|
|
1892
|
+
) &
|
|
1893
|
+
watchdog_pid=$!
|
|
1894
|
+
|
|
1895
|
+
if wait "$pid"; then
|
|
1896
|
+
status=0
|
|
1897
|
+
else
|
|
1898
|
+
status=$?
|
|
1899
|
+
fi
|
|
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
|
|
1908
|
+
wait "$watchdog_pid" 2>/dev/null || true
|
|
1909
|
+
|
|
1910
|
+
AIUR_CONTROL_RPC_OUTPUT="$(cat "$output_file" 2>/dev/null || true)"
|
|
1911
|
+
if [ -f "$timeout_file" ]; then
|
|
1912
|
+
AIUR_CONTROL_RPC_TIMED_OUT=1
|
|
1913
|
+
status=124
|
|
1914
|
+
else
|
|
1915
|
+
AIUR_CONTROL_RPC_TIMED_OUT=0
|
|
1916
|
+
fi
|
|
1917
|
+
|
|
1918
|
+
rm -f "$output_file" "$timeout_file" 2>/dev/null || true
|
|
1919
|
+
return "$status"
|
|
1920
|
+
}
|
|
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
|
+
|
|
894
1941
|
# RPC an expression into the running node. The control CLI prints a trailing
|
|
895
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.
|
|
896
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() {
|
|
897
1965
|
local expression="$1"
|
|
898
|
-
resolve_release
|
|
1966
|
+
resolve_release || return $?
|
|
899
1967
|
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
1968
|
+
resolve_control_identity_from_records
|
|
1969
|
+
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -eq 1 ]; then
|
|
1970
|
+
prepare_distribution || die "distribution setup failed; cannot contact aiur"
|
|
1971
|
+
fi
|
|
900
1972
|
|
|
901
|
-
local marker="__AIUR_CONTROL_EXIT__:"
|
|
902
|
-
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=""
|
|
903
1975
|
|
|
904
1976
|
set +e
|
|
905
|
-
|
|
1977
|
+
run_release_rpc_with_timeout "$expression"
|
|
906
1978
|
status=$?
|
|
1979
|
+
output="$AIUR_CONTROL_RPC_OUTPUT"
|
|
907
1980
|
set -e
|
|
908
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
|
+
|
|
1988
|
+
if [ "${AIUR_CONTROL_RPC_TIMED_OUT:-0}" -eq 1 ]; then
|
|
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."
|
|
1991
|
+
return 124
|
|
1992
|
+
fi
|
|
1993
|
+
|
|
909
1994
|
if [ "$status" -ne 0 ]; then
|
|
910
1995
|
# A non-zero exit here is the rpc transport failing — application-level
|
|
911
1996
|
# outcomes ride the marker path below. `bin/aiur rpc` (Elixir --rpc-eval)
|
|
@@ -915,25 +2000,31 @@ run_control_rpc() {
|
|
|
915
2000
|
# node epmd confirms is down earns the friendly "start aiur" hint; an `up`
|
|
916
2001
|
# node failed for a real reason, and an `unknown` probe must not be assumed
|
|
917
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.
|
|
918
2008
|
case "$(probe_node_liveness)" in
|
|
919
2009
|
down)
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if [ -f "$crash_marker" ]; then
|
|
923
|
-
echo "aiur: background daemon at ${RELEASE_NODE} is DOWN after an unexpected exit; agents may be orphaned" >&2
|
|
924
|
-
sed 's/^/ /' "$crash_marker" >&2 2>/dev/null || true
|
|
925
|
-
echo "aiur: run 'aiur stop' to reap any orphaned agents, then start aiur again" >&2
|
|
926
|
-
else
|
|
927
|
-
echo "aiur: no running aiur node at ${RELEASE_NODE}; start aiur and try again" >&2
|
|
928
|
-
fi
|
|
2010
|
+
print_control_down_message
|
|
2011
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
929
2012
|
;;
|
|
930
2013
|
up)
|
|
931
|
-
|
|
932
|
-
|
|
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
|
|
933
2020
|
;;
|
|
934
2021
|
*)
|
|
935
|
-
|
|
936
|
-
|
|
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
|
|
937
2028
|
;;
|
|
938
2029
|
esac
|
|
939
2030
|
return 1
|
|
@@ -945,15 +2036,55 @@ run_control_rpc() {
|
|
|
945
2036
|
saw_marker=1
|
|
946
2037
|
exit_code="${line#"$marker"}"
|
|
947
2038
|
;;
|
|
2039
|
+
"$error_marker"*)
|
|
2040
|
+
saw_error=1
|
|
2041
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2042
|
+
printf '%s\n' "${line#"$error_marker"}" >&2
|
|
2043
|
+
;;
|
|
948
2044
|
:ok | "") ;;
|
|
949
|
-
*)
|
|
2045
|
+
*)
|
|
2046
|
+
saw_output=1
|
|
2047
|
+
AIUR_CONTROL_RPC_DIAGNOSED=1
|
|
2048
|
+
printf '%s\n' "$line"
|
|
2049
|
+
;;
|
|
950
2050
|
esac
|
|
951
2051
|
done <<<"$output"
|
|
952
2052
|
|
|
953
|
-
[ "$saw_marker" -
|
|
2053
|
+
if [ "$saw_marker" -ne 1 ]; then
|
|
2054
|
+
control_rpc_say "aiur: $(control_command_label) to ${RELEASE_NODE} returned no exit marker; command output may be incomplete"
|
|
2055
|
+
return 1
|
|
2056
|
+
fi
|
|
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
|
+
|
|
954
2062
|
return "$exit_code"
|
|
955
2063
|
}
|
|
956
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
|
+
|
|
957
2088
|
elixir_list_literal() {
|
|
958
2089
|
local first=1 item
|
|
959
2090
|
printf '['
|
|
@@ -997,11 +2128,26 @@ cmd_status() {
|
|
|
997
2128
|
run_control_rpc "Aiur.AgentControlCLI.status()"
|
|
998
2129
|
}
|
|
999
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
|
+
|
|
1000
2138
|
cmd_pause_resume() {
|
|
1001
2139
|
local command="$1"
|
|
1002
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
|
+
|
|
1003
2149
|
if ! parse_issue_targets "$@"; then
|
|
1004
|
-
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
|
|
1005
2151
|
exit 64
|
|
1006
2152
|
fi
|
|
1007
2153
|
|
|
@@ -1015,7 +2161,28 @@ cmd_pause_resume() {
|
|
|
1015
2161
|
run_control_rpc "$expression"
|
|
1016
2162
|
}
|
|
1017
2163
|
|
|
1018
|
-
# `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.
|
|
1019
2186
|
# The text is base64-encoded for the RPC hop so arbitrary content (quotes,
|
|
1020
2187
|
# backslashes, `#{}`, newlines) survives without Elixir-string escaping.
|
|
1021
2188
|
cmd_message() {
|
|
@@ -1046,6 +2213,431 @@ cmd_agents() {
|
|
|
1046
2213
|
run_control_rpc "Aiur.AgentControlCLI.agents()"
|
|
1047
2214
|
}
|
|
1048
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
|
+
|
|
2476
|
+
# `aiur alerts` — newline-delimited structured alert feed from persisted
|
|
2477
|
+
# per-agent logs. `--needs-attention` filters to Executor-actionable alerts.
|
|
2478
|
+
cmd_alerts() {
|
|
2479
|
+
local needs_attention=0 arg
|
|
2480
|
+
for arg in "$@"; do
|
|
2481
|
+
case "$arg" in
|
|
2482
|
+
--needs-attention) needs_attention=1 ;;
|
|
2483
|
+
*)
|
|
2484
|
+
echo "aiur: alerts only accepts --needs-attention" >&2
|
|
2485
|
+
exit 64
|
|
2486
|
+
;;
|
|
2487
|
+
esac
|
|
2488
|
+
done
|
|
2489
|
+
|
|
2490
|
+
if [ "$needs_attention" -eq 1 ]; then
|
|
2491
|
+
run_control_rpc "Aiur.AgentControlCLI.alerts(needs_attention: true)"
|
|
2492
|
+
else
|
|
2493
|
+
run_control_rpc "Aiur.AgentControlCLI.alerts()"
|
|
2494
|
+
fi
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
# `aiur watch` — the server-side status board. Compiles one row per active
|
|
2498
|
+
# agent (state · complexity · activity-age · doing) plus an actionable section
|
|
2499
|
+
# from aiur's own state, with no GitHub round-trip. `--changes` (default) prints
|
|
2500
|
+
# only state-level deltas since the last call; `--full` prints every row;
|
|
2501
|
+
# `--interval N` re-renders every N seconds as a foreground watcher (the Elixir
|
|
2502
|
+
# call stays one-shot — the loop lives here).
|
|
2503
|
+
cmd_watch() {
|
|
2504
|
+
local mode="changes" interval="" arg
|
|
2505
|
+
|
|
2506
|
+
while [ "$#" -gt 0 ]; do
|
|
2507
|
+
arg="$1"
|
|
2508
|
+
case "$arg" in
|
|
2509
|
+
--full) mode="full" ;;
|
|
2510
|
+
--changes) mode="changes" ;;
|
|
2511
|
+
--once) : ;;
|
|
2512
|
+
--interval)
|
|
2513
|
+
shift
|
|
2514
|
+
interval="${1:-}"
|
|
2515
|
+
watch_validate_interval "$interval"
|
|
2516
|
+
;;
|
|
2517
|
+
--interval=*)
|
|
2518
|
+
interval="${arg#--interval=}"
|
|
2519
|
+
watch_validate_interval "$interval"
|
|
2520
|
+
;;
|
|
2521
|
+
*)
|
|
2522
|
+
echo "aiur: watch accepts --full, --changes, --once, --interval <secs>" >&2
|
|
2523
|
+
exit 64
|
|
2524
|
+
;;
|
|
2525
|
+
esac
|
|
2526
|
+
shift
|
|
2527
|
+
done
|
|
2528
|
+
|
|
2529
|
+
local expression="Aiur.AgentControlCLI.watch(mode: :${mode})"
|
|
2530
|
+
|
|
2531
|
+
if [ -n "$interval" ]; then
|
|
2532
|
+
while true; do
|
|
2533
|
+
run_control_rpc "$expression" || true
|
|
2534
|
+
sleep "$interval"
|
|
2535
|
+
done
|
|
2536
|
+
else
|
|
2537
|
+
run_control_rpc "$expression"
|
|
2538
|
+
fi
|
|
2539
|
+
}
|
|
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
|
+
|
|
2613
|
+
watch_validate_interval() {
|
|
2614
|
+
if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -le 0 ]; then
|
|
2615
|
+
echo "aiur: watch --interval expects a positive integer (seconds)" >&2
|
|
2616
|
+
exit 64
|
|
2617
|
+
fi
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
cmd_cleanup_stale() {
|
|
2621
|
+
local dry_run=0 arg
|
|
2622
|
+
for arg in "$@"; do
|
|
2623
|
+
case "$arg" in
|
|
2624
|
+
--dry-run) dry_run=1 ;;
|
|
2625
|
+
*)
|
|
2626
|
+
echo "aiur: cleanup-stale only accepts --dry-run" >&2
|
|
2627
|
+
exit 64
|
|
2628
|
+
;;
|
|
2629
|
+
esac
|
|
2630
|
+
done
|
|
2631
|
+
|
|
2632
|
+
aiur_resolve_identity
|
|
2633
|
+
if [ "$dry_run" -eq 1 ]; then
|
|
2634
|
+
report_stale_manual_smoke || true
|
|
2635
|
+
else
|
|
2636
|
+
report_stale_manual_smoke || true
|
|
2637
|
+
reap_stale_manual_smoke 1
|
|
2638
|
+
fi
|
|
2639
|
+
}
|
|
2640
|
+
|
|
1049
2641
|
# `aiur set <key> <value>` — runtime config overrides without editing
|
|
1050
2642
|
# `.aiur/config`. Currently: `aiur set max-agents N`.
|
|
1051
2643
|
cmd_set() {
|
|
@@ -1098,7 +2690,7 @@ sweep_dead_tmux_sockets() {
|
|
|
1098
2690
|
# foreground teardown (session_cleanup) and on `aiur stop`. Bounded and safe:
|
|
1099
2691
|
#
|
|
1100
2692
|
# * Only exact top-level Aiur artifact families under the temp roots the engine
|
|
1101
|
-
# and BEAM write to. Arbitrary
|
|
2693
|
+
# and BEAM write to. Arbitrary Executor worktrees/checkouts like
|
|
1102
2694
|
# `/tmp/aiur-pr490` are not candidates.
|
|
1103
2695
|
# * Age-gated by AIUR_TMP_REAP_MINUTES (default 1440 = 24h). An entry is removed
|
|
1104
2696
|
# only when NOTHING in its subtree was modified within the window, so a live
|
|
@@ -1192,15 +2784,72 @@ sweep_stale_tmp_artifacts() {
|
|
|
1192
2784
|
return 0
|
|
1193
2785
|
}
|
|
1194
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
|
+
|
|
1195
2819
|
# Stop the running session: kill this instance's tmux + BEAM, then sweep.
|
|
1196
2820
|
cmd_stop() {
|
|
2821
|
+
AIUR_STOP_FOUND_NOTHING=0
|
|
1197
2822
|
resolve_release
|
|
1198
2823
|
aiur_resolve_identity
|
|
2824
|
+
RELEASE_NODE="$AIUR_RELEASE_NODE"
|
|
2825
|
+
ERL_EPMD_ADDRESS="${ERL_EPMD_ADDRESS:-127.0.0.1}"
|
|
2826
|
+
export RELEASE_NODE ERL_EPMD_ADDRESS
|
|
2827
|
+
resolve_control_identity_from_records
|
|
2828
|
+
|
|
2829
|
+
local workspace_root_file
|
|
2830
|
+
workspace_root_file="$(workspace_root_file_from_instance_record 2>/dev/null || true)"
|
|
1199
2831
|
|
|
1200
2832
|
local tmux_bin
|
|
1201
2833
|
tmux_bin="$(command -v tmux || true)"
|
|
1202
|
-
local session="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default"
|
|
1203
|
-
local socket="${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}"
|
|
2834
|
+
local session="${AIUR_ADOPTED_TMUX_SESSION:-${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}-default}"
|
|
2835
|
+
local socket="${AIUR_ADOPTED_TMUX_SOCKET:-${AIUR_SESSION_PREFIX}-${USER:-user}${AIUR_INSTANCE_KEY:+-$AIUR_INSTANCE_KEY}}"
|
|
2836
|
+
|
|
2837
|
+
local has_session=0
|
|
2838
|
+
if [ -n "$tmux_bin" ] && "$tmux_bin" -L "$socket" has-session -t "$session" 2>/dev/null; then
|
|
2839
|
+
has_session=1
|
|
2840
|
+
fi
|
|
2841
|
+
|
|
2842
|
+
if [ "${AIUR_CONTROL_ADOPTED_RECORD:-0}" -ne 1 ] && \
|
|
2843
|
+
[ "${AIUR_CONTROL_CURRENT_NODE_STATE:-}" = "down" ] && \
|
|
2844
|
+
[ "${AIUR_PROJECT_ROOT_SOURCE:-}" = "cwd" ] && \
|
|
2845
|
+
[ "$has_session" -eq 0 ] && \
|
|
2846
|
+
[ ! -f "$(aiur_crash_marker_path)" ]; then
|
|
2847
|
+
echo "aiur: no running aiur node at ${AIUR_RELEASE_NODE}; nothing stopped" >&2
|
|
2848
|
+
warn_other_aiur_daemons "$AIUR_RELEASE_NODE"
|
|
2849
|
+
print_global_config_control_hint
|
|
2850
|
+
AIUR_STOP_FOUND_NOTHING=1
|
|
2851
|
+
return 1
|
|
2852
|
+
fi
|
|
1204
2853
|
|
|
1205
2854
|
# Tell the background BEAM-death watchdog this exit is intentional before we
|
|
1206
2855
|
# kill the BEAM, so it consumes the sentinel instead of recording a crash. The
|
|
@@ -1219,6 +2868,11 @@ cmd_stop() {
|
|
|
1219
2868
|
# it. Do not sweep by release dir: sibling instances share dev releases.
|
|
1220
2869
|
kill_beams_matching "-name ${AIUR_RELEASE_NODE}"
|
|
1221
2870
|
|
|
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"
|
|
2875
|
+
|
|
1222
2876
|
# The BEAM (alive until the TERM above) reaped its own headless agents through
|
|
1223
2877
|
# ProcessReaper on Application.stop. kill-server is the guarantee the earlier
|
|
1224
2878
|
# kill-session can't give for mid-turn agents: every pane agent across all
|
|
@@ -1238,14 +2892,248 @@ cmd_stop() {
|
|
|
1238
2892
|
rm -f "$agentfile" 2>/dev/null || true
|
|
1239
2893
|
done
|
|
1240
2894
|
|
|
2895
|
+
reap_stale_manual_smoke 0
|
|
1241
2896
|
sweep_dead_tmux_sockets
|
|
1242
2897
|
sweep_stale_tmp_artifacts
|
|
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
|
|
1243
3109
|
}
|
|
1244
3110
|
|
|
1245
3111
|
# --- dispatch ----------------------------------------------------------------
|
|
1246
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
|
+
|
|
1247
3132
|
aiur_engine_main() {
|
|
1248
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}"
|
|
1249
3137
|
case "$cmd" in
|
|
1250
3138
|
__identity)
|
|
1251
3139
|
aiur_print_identity
|
|
@@ -1256,25 +3144,97 @@ aiur_engine_main() {
|
|
|
1256
3144
|
--version)
|
|
1257
3145
|
run_version "$@"
|
|
1258
3146
|
;;
|
|
3147
|
+
--todo)
|
|
3148
|
+
run_todo "$@"
|
|
3149
|
+
;;
|
|
3150
|
+
--only)
|
|
3151
|
+
echo "aiur: --only is valid only with --todo" >&2
|
|
3152
|
+
exit 64
|
|
3153
|
+
;;
|
|
1259
3154
|
init)
|
|
1260
3155
|
run_init "$@"
|
|
1261
3156
|
;;
|
|
1262
|
-
|
|
3157
|
+
findings)
|
|
3158
|
+
run_findings "$@"
|
|
3159
|
+
;;
|
|
3160
|
+
guard-pr-deletions)
|
|
1263
3161
|
shift
|
|
1264
|
-
|
|
3162
|
+
"$engine_dir/guard-pr-deletions.sh" "$@"
|
|
3163
|
+
;;
|
|
3164
|
+
ask | asks)
|
|
3165
|
+
run_asks "$@"
|
|
3166
|
+
;;
|
|
3167
|
+
--bg)
|
|
3168
|
+
dispatch_run "$@"
|
|
1265
3169
|
;;
|
|
1266
3170
|
run)
|
|
1267
3171
|
shift
|
|
1268
|
-
|
|
3172
|
+
dispatch_run "$@"
|
|
1269
3173
|
;;
|
|
1270
3174
|
status)
|
|
1271
3175
|
shift
|
|
1272
3176
|
cmd_status "$@"
|
|
1273
3177
|
;;
|
|
3178
|
+
usage)
|
|
3179
|
+
shift
|
|
3180
|
+
cmd_usage "$@"
|
|
3181
|
+
;;
|
|
1274
3182
|
agents)
|
|
1275
3183
|
shift
|
|
1276
3184
|
cmd_agents "$@"
|
|
1277
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
|
+
;;
|
|
3210
|
+
alerts)
|
|
3211
|
+
shift
|
|
3212
|
+
cmd_alerts "$@"
|
|
3213
|
+
;;
|
|
3214
|
+
watch)
|
|
3215
|
+
shift
|
|
3216
|
+
cmd_watch "$@"
|
|
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
|
+
;;
|
|
1278
3238
|
set)
|
|
1279
3239
|
shift
|
|
1280
3240
|
cmd_set "$@"
|
|
@@ -1283,26 +3243,39 @@ aiur_engine_main() {
|
|
|
1283
3243
|
shift
|
|
1284
3244
|
cmd_pause_resume "$cmd" "$@"
|
|
1285
3245
|
;;
|
|
3246
|
+
reset-budget)
|
|
3247
|
+
shift
|
|
3248
|
+
cmd_reset_budget "$@"
|
|
3249
|
+
;;
|
|
1286
3250
|
message)
|
|
1287
3251
|
shift
|
|
1288
3252
|
cmd_message "$@"
|
|
1289
3253
|
;;
|
|
3254
|
+
cleanup-stale)
|
|
3255
|
+
shift
|
|
3256
|
+
cmd_cleanup_stale "$@"
|
|
3257
|
+
;;
|
|
1290
3258
|
stop)
|
|
1291
3259
|
cmd_stop
|
|
1292
3260
|
;;
|
|
3261
|
+
restart)
|
|
3262
|
+
shift
|
|
3263
|
+
cmd_restart "$@"
|
|
3264
|
+
;;
|
|
1293
3265
|
"")
|
|
1294
|
-
|
|
3266
|
+
dispatch_run
|
|
1295
3267
|
;;
|
|
1296
3268
|
-*)
|
|
1297
3269
|
# leading-flag forms (e.g. `aiur --interactive <config>`) are a run
|
|
1298
|
-
|
|
3270
|
+
dispatch_run "$@"
|
|
1299
3271
|
;;
|
|
1300
3272
|
*)
|
|
1301
3273
|
# a path/config argument is a run; anything else is a usage error
|
|
1302
3274
|
if [ -e "$cmd" ]; then
|
|
1303
|
-
|
|
3275
|
+
dispatch_run "$@"
|
|
1304
3276
|
else
|
|
1305
3277
|
echo "aiur: unknown command: $cmd" >&2
|
|
3278
|
+
warn_if_cli_behind_release_checkout
|
|
1306
3279
|
usage >&2
|
|
1307
3280
|
exit 64
|
|
1308
3281
|
fi
|