aiur-cli 0.0.9-nightly.5a0d430 → 0.0.9-nightly.f31ca99

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/bin/aiur.js CHANGED
@@ -165,8 +165,8 @@ function ensureTmux() {
165
165
  // True only when the pinned opencode is on PATH; a wrong (e.g. newer,
166
166
  // unverified) version must be replaced with the pin, so presence alone
167
167
  // isn't enough.
168
- function opencodeMatchesPin() {
169
- const result = safeSpawn("opencode", ["--version"], { encoding: "utf8" });
168
+ function opencodeMatchesPin(command = "opencode") {
169
+ const result = safeSpawn(command, ["--version"], { encoding: "utf8" });
170
170
  return !result.error && result.status === 0 && (result.stdout || "").trim() === opencodeVersion();
171
171
  }
172
172
 
@@ -185,40 +185,60 @@ function installOpencode() {
185
185
  return !result.error && result.status === 0;
186
186
  }
187
187
 
188
+ // npm's global bin may be shadowed by another OpenCode on the incoming PATH.
189
+ // Verify the actual installed executable before making it the release's choice.
190
+ function selectGlobalOpencode() {
191
+ const result = safeSpawn("npm", ["prefix", "-g"], { encoding: "utf8" });
192
+ if (result.error || result.status !== 0) return false;
193
+ const prefix = (result.stdout || "").trim();
194
+ if (!path.isAbsolute(prefix)) return false;
195
+ const bin = path.join(prefix, "bin");
196
+ if (!opencodeMatchesPin(path.join(bin, "opencode"))) return false;
197
+ process.env.PATH = `${bin}${path.delimiter}${process.env.PATH || ""}`;
198
+ return true;
199
+ }
200
+
188
201
  // opencode powers the interactive "take the wheel" panes. Non-fatal: everything
189
202
  // else works without it.
190
203
  function ensureOpencode() {
191
- if (opencodeMatchesPin()) return;
204
+ if (opencodeMatchesPin() || selectGlobalOpencode()) return;
192
205
  if (process.env.AIUR_SKIP_OPENCODE_INSTALL !== "1") {
193
206
  process.stderr.write(
194
207
  `aiur: provisioning opencode@${opencodeVersion()} (one-time, for the interactive 'take the wheel' panes)…\n`,
195
208
  );
196
209
  installOpencode();
197
- if (opencodeMatchesPin()) return;
210
+ if (opencodeMatchesPin() || selectGlobalOpencode()) return;
198
211
  }
199
212
  process.stderr.write(
200
- "aiur: opencode was not found on PATH. The interactive 'take the wheel' " +
213
+ `aiur: opencode@${opencodeVersion()} is unavailable. The interactive 'take the wheel' ` +
201
214
  "feature needs it; everything else works without it. " +
202
- "Install from https://opencode.ai\n",
215
+ `Install with: npm install -g opencode-ai@${opencodeVersion()}, ` +
216
+ "and put npm's global bin ahead of other opencode binaries on PATH.\n",
203
217
  );
204
218
  }
205
219
 
206
- // `init` and `--version` run as foreground one-shots that never start the
207
- // tmux-backed UI, so their tmux/opencode provisioning is irrelevant — and tmux
208
- // may legitimately be absent on a machine that only ever runs `aiur init`.
209
- function isForegroundOneShot(argv) {
210
- for (const arg of argv) {
211
- if (arg === "--version") return true;
212
- if (arg.startsWith("-")) continue;
213
- return arg === "init";
214
- }
215
- return false;
220
+ // Match the shared engine's first-argument dispatch: only session launch paths
221
+ // need interactive tools. Unknown commands still reach the engine's usage error.
222
+ function requiresInteractiveTools(argv) {
223
+ const command = argv[0] || "";
224
+ if (["", "run", "restart", "--bg"].includes(command)) return true;
225
+ const nonLaunchCommands = new Set([
226
+ "__identity", "help", "-h", "-help", "--h", "--help", "--version", "--todo", "--only",
227
+ "init", "findings", "ask", "asks", "status", "usage", "agents", "commands", "units",
228
+ "build-orders", "queue", "analytics", "github-cost", "github-usage", "alerts", "watch", "set",
229
+ "upgrade", "pause", "resume", "reset-budget", "message", "cleanup-stale", "stop",
230
+ "executor-answer", "executor-escalate", "executor-moot", "executor-listen", "executor-wait",
231
+ "executor-emit", "executor-subscribe", "executor-unsubscribe", "executor-subscriptions",
232
+ "executor-roster", "executor-fast-forward", "executor-claim", "executor-release", "executor-revoke",
233
+ ]);
234
+ if (nonLaunchCommands.has(command)) return false;
235
+ return command.startsWith("-") || fs.existsSync(command);
216
236
  }
217
237
 
218
238
  function main() {
219
239
  const releaseDir = resolveReleaseDir();
220
240
 
221
- if (!isForegroundOneShot(process.argv.slice(2))) {
241
+ if (requiresInteractiveTools(process.argv.slice(2))) {
222
242
  ensureTmux();
223
243
  ensureOpencode();
224
244
  }
@@ -0,0 +1,12 @@
1
+ # Read-only capability report through the daemon's local control RPC.
2
+ cmd_capabilities() {
3
+ local opts="" arg
4
+ for arg in "$@"; do
5
+ case "$arg" in
6
+ --json) opts="json: true" ;;
7
+ -*) echo "aiur: capabilities received an unknown option: $arg" >&2; exit 64 ;;
8
+ *) echo "aiur: capabilities does not accept positional arguments" >&2; exit 64 ;;
9
+ esac
10
+ done
11
+ run_control_rpc "Aiur.AgentControlCLI.capabilities([$opts])"
12
+ }
@@ -20,9 +20,7 @@
20
20
  #
21
21
  # The release is self-contained (bundled ERTS), so it runs without mise/Elixir on
22
22
  # PATH. Dev's build-if-stale step lives in the aiurdev shim, not here.
23
-
24
23
  set -euo pipefail
25
-
26
24
  # Raise the soft open-file limit toward the hard maximum. High agent concurrency
27
25
  # spawns many tmux/opencode/git subprocesses + sockets; on hosts with a low
28
26
  # default (macOS ships 256) that exhausts file descriptors (:emfile) and crashes
@@ -34,7 +32,6 @@ elif [ -n "${__aiur_hard_nofile}" ]; then
34
32
  ulimit -Sn "${__aiur_hard_nofile}" 2>/dev/null || true
35
33
  fi
36
34
  unset __aiur_hard_nofile
37
-
38
35
  # Export the effective soft limit after the best-effort raise. The BEAM uses
39
36
  # this inherited value for FD-headroom admission on hosts without procfs,
40
37
  # avoiding a runtime `ulimit` subprocess precisely when descriptors are scarce.
@@ -45,7 +42,6 @@ else
45
42
  unset AIUR_NOFILE_SOFT_LIMIT
46
43
  fi
47
44
  unset __aiur_soft_nofile
48
-
49
45
  # Preserve the shell that initiated the run as a best-effort Executor root.
50
46
  # An explicit positive override wins (service managers may know a better root);
51
47
  # otherwise the engine's parent is the nearest identity available before tmux
@@ -57,12 +53,10 @@ if ! [[ "${AIUR_OPERATOR_PID:-}" =~ ^[1-9][0-9]*$ ]]; then
57
53
  unset AIUR_OPERATOR_PID
58
54
  fi
59
55
  fi
60
-
61
56
  die() {
62
57
  echo "❌ $*" >&2
63
58
  exit 1
64
59
  }
65
-
66
60
  legacy_config_path() {
67
61
  while [ "$#" -gt 0 ]; do
68
62
  case "$1" in
@@ -203,7 +197,6 @@ warn_if_cli_behind_release_checkout() {
203
197
  fi
204
198
  }
205
199
 
206
- # --- distribution identity (per-instance: keyed by the aiur project root) -----
207
200
 
208
201
  # The aiur project root used to key this instance. AIUR_REPO_ROOT (set by the dev
209
202
  # shim) wins. Otherwise walk up from $PWD to the first dir holding a REPO-LOCAL
@@ -311,7 +304,6 @@ aiur_print_identity() {
311
304
  printf 'AIUR_COOKIE_FILE=%s\n' "$AIUR_COOKIE_FILE"
312
305
  }
313
306
 
314
- # --- BEAM distribution (cookie + named node) ---------------------------------
315
307
 
316
308
  ensure_bg_state_dir() {
317
309
  aiur_resolve_identity
@@ -368,7 +360,6 @@ prepare_distribution() {
368
360
  export AIUR_ERLANG_COOKIE="$RELEASE_COOKIE"
369
361
  }
370
362
 
371
- # --- release resolution ------------------------------------------------------
372
363
 
373
364
  release_dir=""
374
365
  vsn_dir=""
@@ -405,7 +396,6 @@ resolve_release() {
405
396
  fi
406
397
  }
407
398
 
408
- # --- argv round-trip (System.argv is empty under `elixir --eval`) -------------
409
399
 
410
400
  argv_file=""
411
401
  init_argv_file() {
@@ -450,6 +440,9 @@ usage() {
450
440
  Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agents <n>] [--logs-root <path>] [--port <port>] [--host <host>] [config-path]
451
441
  aiur run [--bg] [--no-dashboard] [--executor] [--debug] explicit launch form (foreground unless --bg)
452
442
  aiur init [--force] scaffold .aiur/config (interactive setup wizard)
443
+ aiur login <harness> <name> [--dir <path>] sign in to a supported backend account
444
+ aiur accounts [<harness>] [--json] list registered accounts
445
+ aiur logout <harness> <name> [--purge] remove a backend account
453
446
  aiur --bg [--no-dashboard] [--executor] [--debug] start detached; dashboard on unless suppressed
454
447
  aiur stop stop the running session
455
448
  aiur restart [--no-build] [run flags] stop, refresh the build, start again (detached)
@@ -460,18 +453,26 @@ Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agent
460
453
  aiur executor-escalate <decision-id> --expected-version <n> --reason <text> [--executor-id <id>]
461
454
  aiur executor-moot <decision-id> --expected-version <n> --reason-class <class> [--reason <text>] [--executor-id <id>]
462
455
  aiur units [--scope live|unfinished|all|none] [--condition active|alert|paused|queued|finished]... [--format auto|table|records] [--json]
456
+ aiur queue show [--queue NAME] [--json] read queue; add/remove/reorder/hold/release/recover/clear steer it
463
457
  aiur build-orders [<root>] [--json] show the Build Order catalog or one root
458
+ aiur epic set <epic> <ids...> [--as <who>] [--source cli|backfill-agent] [--json]
459
+ aiur epic clear <ids...> [--as <who>] [--json]
460
+ aiur epic show [<ids...>] [--json]
461
+ aiur epic list [--json]
464
462
  aiur analytics [--range run|full] [--since <ISO-8601>] [--until <ISO-8601>] [--build-order <id>] [--json]
465
463
  aiur github-cost [--budget graphql|core|all] [--format auto|table|records] [--json] rank GitHub API spend by call site
464
+ aiur capabilities [--json] read-only instance capability report
466
465
  aiur github-usage [--json] per-actor (daemon vs agent) GitHub usage and ceilings
467
466
  aiur alerts [--needs-attention] show structured alert feed
468
467
  aiur watch [--full|--changes] [--interval <secs>] server-side status board
469
- aiur executor-listen [--topic <pattern>] stream Executor events as JSON lines
468
+ aiur listen [--topic <pattern> | --ticket <id>] stream events as JSON lines
469
+ aiur executor-listen [--topic <pattern>] deprecated alias for listen
470
470
  aiur executor-wait [--timeout <seconds>] [--json] block until Executor work arrives
471
471
  aiur executor-fast-forward <wake-id> [--as <id>] acknowledge an externally covered wake prefix
472
472
  aiur executor-emit <topic> --payload <json> publish an Executor event
473
473
  aiur executor-subscribe|executor-unsubscribe <pattern>
474
474
  aiur executor-subscriptions list persistent Executor bindings
475
+ aiur workspace-recover <ticket-identifier> <generation> release a held workspace after verified provider exit
475
476
  aiur executor-roster [--json] list Executor consumers with their liveness evidence
476
477
  aiur executor-claim [--as <id>] claim the wake stream, or refuse and name the live owner
477
478
  aiur executor-release [--as <id>] give up this consumer's claim
@@ -488,14 +489,13 @@ Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agent
488
489
  aiur ask <title> [--body <text>|--body-file <path>] [--urgency low|normal|high] [--blocking]
489
490
  aiur ask --done <id> [--note <text>] create or resolve an operator request
490
491
  aiur asks [--open|--all] [--json] inspect current-repository operator requests
492
+ aiur doctor [--repair] check mise shims; repair only with consent
491
493
  aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
492
494
  aiur --version
493
-
494
495
  Bare aiur: start or attach to this directory's interactive session.
495
496
  EOF
496
497
  }
497
498
 
498
- # --- one-shot: --version (no tmux) -------------------------------------------
499
499
 
500
500
  run_version() {
501
501
  resolve_release
@@ -511,7 +511,6 @@ run_version() {
511
511
  exec "${release_cmd[@]}"
512
512
  }
513
513
 
514
- # --- one-shot: init (interactive wizard, distribution-free, no tmux) ----------
515
514
 
516
515
  run_init() {
517
516
  resolve_release
@@ -522,6 +521,47 @@ run_init() {
522
521
  exec "${release_cmd[@]}"
523
522
  }
524
523
 
524
+ run_local_cli() {
525
+ resolve_release
526
+ build_init_cmd
527
+ init_argv_file
528
+ write_argv "$@"
529
+ export AIUR_ARGV_FILE="$argv_file"
530
+ exec "${release_cmd[@]}"
531
+ }
532
+
533
+ run_claude_login() {
534
+ resolve_release
535
+ local profile_dir
536
+ build_init_cmd
537
+ init_argv_file
538
+ write_argv "__login_prepare" "${@:2}"
539
+ export AIUR_ARGV_FILE="$argv_file"
540
+ profile_dir="$("${release_cmd[@]}")" || return $?
541
+ [ -n "$profile_dir" ] || { echo "aiur: account profile directory is unavailable" >&2; return 1; }
542
+ exec env "CLAUDE_CONFIG_DIR=$profile_dir" claude
543
+ }
544
+
545
+ run_account_login() {
546
+ local harness="$2"
547
+ case "$harness" in
548
+ kimi|deepseek|openrouter) run_local_cli "$@"; return $? ;;
549
+ esac
550
+ resolve_release
551
+ local profile_dir
552
+ build_init_cmd
553
+ init_argv_file
554
+ write_argv "__login_prepare" "$harness" "${@:3}"
555
+ export AIUR_ARGV_FILE="$argv_file"
556
+ profile_dir="$("${release_cmd[@]}")" || return $?
557
+ [ -n "$profile_dir" ] || { echo "aiur: account profile directory is unavailable" >&2; return 1; }
558
+ case "$harness" in
559
+ claude) exec env "CLAUDE_CONFIG_DIR=$profile_dir" claude ;;
560
+ codex) exec env "CODEX_HOME=$profile_dir" codex ;;
561
+ *) echo "aiur: unsupported account harness: $harness" >&2; return 64 ;;
562
+ esac
563
+ }
564
+
525
565
  # --- one-shot: --todo (control RPC; requires a running daemon) ----------------
526
566
 
527
567
  parsed_todo_only=0
@@ -723,22 +763,10 @@ scrub_run_only_env() {
723
763
  }
724
764
 
725
765
  run_argv=()
726
- # Default dashboard bind host. Prefer this machine's Tailscale IPv4 so the
727
- # dashboard is reachable across the tailnet by default (when config omits it);
728
- # fall back to loopback when Tailscale is absent, or when dashboard credentials
729
- # are unset (a non-loopback bind requires them, so we stay on loopback rather
730
- # than refuse to start). The BEAM applies this below an explicit `server.host`,
731
- # while an explicit `--host` remains the highest-precedence value.
766
+ # Default dashboard bind host. Remote access is an explicit operator choice.
767
+ # The BEAM applies this below `server.host`; `--host` has highest precedence.
732
768
  default_dashboard_host() {
733
- local ip=""
734
- if command -v tailscale >/dev/null 2>&1; then
735
- ip="$(tailscale ip -4 2>/dev/null | grep -m1 -E '^100\.[0-9]+\.[0-9]+\.[0-9]+$' || true)"
736
- fi
737
- if [ -n "$ip" ] && [ -n "${AIUR_DASHBOARD_USERNAME:-}" ] && [ -n "${AIUR_DASHBOARD_PASSWORD:-}" ]; then
738
- printf '%s' "$ip"
739
- else
740
- printf '127.0.0.1'
741
- fi
769
+ printf '%s' "${AIUR_DEFAULT_DASHBOARD_HOST:-127.0.0.1}"
742
770
  }
743
771
 
744
772
  build_run_argv() {
@@ -981,7 +1009,7 @@ run_session() {
981
1009
  AIUR_SESSION_TMPFILE AIUR_AGENT_TMPFILE AIUR_WORKSPACE_ROOT_FILE AIUR_ALERT_LEDGER_PATH_FILE \
982
1010
  ELIXIR_ERL_OPTIONS AIUR_LOGS_ROOT AIUR_OPENCODE_BRIDGE_PORT AIUR_DEFAULT_DASHBOARD_HOST AIUR_DEBUG AIUR_DEV_TEST_TICKET_IDS \
983
1011
  AIUR_OPERATOR_PID AIUR_LAUNCHER_PID AIUR_NOFILE_SOFT_LIMIT ERL_CRASH_DUMP ERL_CRASH_DUMP_SECONDS \
984
- AIUR_BG_STATE_DIR AIUR_CLI_VERSION; do
1012
+ AIUR_BG_STATE_DIR AIUR_INSTANCE_KEY AIUR_CLI_VERSION; do
985
1013
  if [ -n "${!v:-}" ]; then printf 'export %s=%q\n' "$v" "${!v}"; fi
986
1014
  done
987
1015
  printf 'capture=%q\n' "$startup_capture"
@@ -2620,8 +2648,8 @@ parse_issue_targets() {
2620
2648
 
2621
2649
  [ "${#parsed_targets[@]}" -gt 0 ]
2622
2650
  }
2623
-
2624
2651
  cmd_status() {
2652
+ [ ! -f "$engine_dir/aiur-mise-doctor" ] || bash "$engine_dir/aiur-mise-doctor" --check || true
2625
2653
  [ "$#" -eq 0 ] || die "status does not accept arguments"
2626
2654
  run_control_rpc "Aiur.AgentControlCLI.status()"
2627
2655
  }
@@ -2633,6 +2661,44 @@ cmd_usage() {
2633
2661
  run_control_rpc "Aiur.AgentControlCLI.usage()"
2634
2662
  }
2635
2663
 
2664
+ cmd_accounts() {
2665
+ local json_arg=false harness="" arg encoded expression
2666
+ for arg in "$@"; do
2667
+ case "$arg" in
2668
+ --json)
2669
+ [ "$json_arg" = false ] || die "accounts accepts --json only once"
2670
+ json_arg=true
2671
+ ;;
2672
+ --all)
2673
+ ;;
2674
+ -*)
2675
+ die "accounts accepts an optional harness and --json"
2676
+ ;;
2677
+ *)
2678
+ [ -z "$harness" ] || die "accounts accepts only one harness"
2679
+ harness="$arg"
2680
+ ;;
2681
+ esac
2682
+ done
2683
+
2684
+ resolve_release || return $?
2685
+ prepare_distribution || die "distribution setup failed; cannot contact aiur"
2686
+ resolve_control_identity_from_records
2687
+ if [ "$(probe_node_liveness)" = "down" ]; then
2688
+ # The local one-shot CLI renders the identity and marks usage unavailable.
2689
+ # It never makes a provider request.
2690
+ run_local_cli accounts "$@"
2691
+ else
2692
+ if [ -n "$harness" ]; then
2693
+ encoded="$(printf '%s' "$harness" | base64 | tr -d '\n')"
2694
+ expression="Aiur.AgentControlCLI.accounts($json_arg, Base.decode64!(\"$encoded\"))"
2695
+ else
2696
+ expression="Aiur.AgentControlCLI.accounts($json_arg)"
2697
+ fi
2698
+ run_control_rpc "$expression"
2699
+ fi
2700
+ }
2701
+
2636
2702
  cmd_pause_resume() {
2637
2703
  local command="$1"
2638
2704
  shift
@@ -2680,6 +2746,16 @@ cmd_reset_budget() {
2680
2746
  run_control_rpc "$expression"
2681
2747
  }
2682
2748
 
2749
+ # Requires the operator to name the exact ticket and generation shown by
2750
+ # status. The daemon independently verifies the recorded boot proof.
2751
+ cmd_workspace_recover() {
2752
+ [ "$#" -eq 2 ] || { echo "aiur: workspace-recover expects a ticket identifier and generation (e.g. aiur workspace-recover ENG-123 7)" >&2; exit 64; }
2753
+ local ticket="$1" generation="$2" encoded
2754
+ [[ "$generation" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: workspace-recover generation must be a positive integer" >&2; exit 64; }
2755
+ encoded="$(printf '%s' "$ticket" | base64 | tr -d '\n')"
2756
+ run_control_rpc "Aiur.AgentControlCLI.recover_workspace(Base.decode64!(\"$encoded\"), $generation)"
2757
+ }
2758
+
2683
2759
  # `aiur message <issue> <text>` — deliver Executor text to one running agent.
2684
2760
  # The text is base64-encoded for the RPC hop so arbitrary content (quotes,
2685
2761
  # backslashes, `#{}`, newlines) survives without Elixir-string escaping.
@@ -3073,7 +3149,6 @@ cmd_github_cost() {
3073
3149
 
3074
3150
  run_control_rpc "Aiur.AgentControlCLI.github_cost([$opts])"
3075
3151
  }
3076
-
3077
3152
  # `aiur github-usage` — per-actor (daemon vs each agent workspace) Core/GraphQL
3078
3153
  # usage and ceilings from the shared admission broker. Read-only; it reads the
3079
3154
  # broker database and issues no GitHub request of its own.
@@ -3092,10 +3167,8 @@ cmd_github_usage() {
3092
3167
 
3093
3168
  local opts=""
3094
3169
  [ "$json" -eq 1 ] && opts="json: true"
3095
-
3096
3170
  run_control_rpc "Aiur.AgentControlCLI.github_usage([$opts])"
3097
3171
  }
3098
-
3099
3172
  # `aiur alerts` — newline-delimited structured alert feed from persisted
3100
3173
  # per-agent logs. `--needs-attention` filters to Executor-actionable alerts.
3101
3174
  cmd_alerts() {
@@ -3161,21 +3234,61 @@ cmd_watch() {
3161
3234
  fi
3162
3235
  }
3163
3236
 
3164
- cmd_executor_listen() {
3165
- local topic="executor.#" arg
3237
+ listen_clock() { printf '%s' "$SECONDS"; }
3238
+
3239
+ cmd_listen() {
3240
+ local topic="executor.#" ticket="" arg
3166
3241
  while [ "$#" -gt 0 ]; do
3167
3242
  arg="$1"
3168
3243
  case "$arg" in
3169
3244
  --topic) shift; topic="${1:-}" ;;
3170
3245
  --topic=*) topic="${arg#--topic=}" ;;
3171
- *) echo "aiur: executor-listen accepts --topic <pattern>" >&2; exit 64 ;;
3246
+ --ticket) shift; ticket="${1:-}" ;;
3247
+ --ticket=*) ticket="${arg#--ticket=}" ;;
3248
+ *) echo "aiur: listen accepts --topic <pattern> or --ticket <id>" >&2; exit 64 ;;
3172
3249
  esac
3173
- [ -n "$topic" ] || { echo "aiur: executor-listen requires a topic" >&2; exit 64; }
3174
3250
  shift
3175
3251
  done
3252
+ if [ -n "$ticket" ]; then
3253
+ [[ "$ticket" =~ ^[0-9]+$ ]] || { echo "aiur: listen --ticket expects a numeric ticket id" >&2; exit 64; }
3254
+ [ "$topic" = "executor.#" ] || { echo "aiur: listen cannot combine --ticket and --topic" >&2; exit 64; }
3255
+ topic="ticket.${ticket}.#"
3256
+ fi
3257
+ [ -n "$topic" ] || { echo "aiur: listen requires a topic" >&2; exit 64; }
3176
3258
  local encoded
3177
3259
  encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
3178
- run_control_stream "Aiur.AgentControlCLI.executor_listen(topic: Base.decode64!(\"$encoded\"))"
3260
+ run_control_rpc "Aiur.AgentControlCLI.executor_listen_validate(Base.decode64!(\"$encoded\"))" || return $?
3261
+ # A stream that stayed up 30s had a live connection, so losing it starts a
3262
+ # new outage. Each outage gets a bounded backoff totalling ~10 minutes, long
3263
+ # enough to outlast a normal `aiur restart`.
3264
+ # ponytail: attempt duration stands in for "connected"; a wedged daemon whose
3265
+ # RPC hangs 30s+ before failing keeps the listener retrying past the budget.
3266
+ # Upgrade to a listener-ready signal if that case shows up in practice.
3267
+ local waited=0 delay=2 started status
3268
+ while :; do
3269
+ started="$(listen_clock)"
3270
+ status=0
3271
+ # Subshell: a `die` inside (e.g. the release dir missing mid-rebuild during
3272
+ # `aiurdev restart`) fails this attempt with exit 1 instead of the listener.
3273
+ (run_control_stream "Aiur.AgentControlCLI.executor_listen(topic: Base.decode64!(\"$encoded\"))") || status=$?
3274
+ [ "$status" -eq 0 ] && return 0
3275
+ if [ "$status" -ne 1 ] || [ "${AIUR_LISTEN_RECONNECT:-1}" -ne 1 ]; then
3276
+ echo "aiur: listen stopped after streaming control RPC failure (exit ${status}); restart the command after correcting the daemon error" >&2
3277
+ return "$status"
3278
+ fi
3279
+ if [ $(($(listen_clock) - started)) -ge 30 ]; then
3280
+ waited=0
3281
+ delay=2
3282
+ fi
3283
+ if [ "$waited" -ge 600 ]; then
3284
+ echo "aiur: listen could not reconnect within ${waited} seconds; daemon may be unavailable" >&2
3285
+ return 1
3286
+ fi
3287
+ echo "aiur: listen lost the daemon stream (exit ${status}); reconnecting in ${delay} seconds" >&2
3288
+ sleep "$delay"
3289
+ waited=$((waited + delay))
3290
+ delay=$((delay * 2 > 60 ? 60 : delay * 2))
3291
+ done
3179
3292
  }
3180
3293
 
3181
3294
  cmd_executor_wait() {
@@ -4061,8 +4174,8 @@ cmd_upgrade() {
4061
4174
  }
4062
4175
 
4063
4176
  # --- dispatch ----------------------------------------------------------------
4064
-
4065
4177
  dispatch_run() {
4178
+ [ ! -f "$engine_dir/aiur-mise-doctor" ] || bash "$engine_dir/aiur-mise-doctor" --check || true
4066
4179
  local mode="foreground" arg
4067
4180
  local args=()
4068
4181
 
@@ -4080,7 +4193,6 @@ dispatch_run() {
4080
4193
  # `set -u` — happens for a bare `--bg` run. Guard the expansion.
4081
4194
  run_session "$mode" "${args[@]+"${args[@]}"}"
4082
4195
  }
4083
-
4084
4196
  aiur_engine_main() {
4085
4197
  local cmd="${1:-}"
4086
4198
  # Names the running subcommand in control-RPC diagnostics so a failure says
@@ -4106,6 +4218,16 @@ aiur_engine_main() {
4106
4218
  init)
4107
4219
  run_init "$@"
4108
4220
  ;;
4221
+ login)
4222
+ run_account_login "$@"
4223
+ ;;
4224
+ accounts)
4225
+ shift
4226
+ cmd_accounts "$@"
4227
+ ;;
4228
+ logout)
4229
+ run_local_cli "$@"
4230
+ ;;
4109
4231
  findings)
4110
4232
  run_findings "$@"
4111
4233
  ;;
@@ -4119,6 +4241,7 @@ aiur_engine_main() {
4119
4241
  shift
4120
4242
  dispatch_run "$@"
4121
4243
  ;;
4244
+ doctor) shift; exec bash "$engine_dir/aiur-mise-doctor" "$@" ;;
4122
4245
  status)
4123
4246
  shift
4124
4247
  cmd_status "$@"
@@ -4151,6 +4274,11 @@ aiur_engine_main() {
4151
4274
  shift
4152
4275
  cmd_units "$@"
4153
4276
  ;;
4277
+ queue)
4278
+ shift
4279
+ cmd_queue "$@"
4280
+ ;;
4281
+ epic) shift; cmd_epic "$@" ;;
4154
4282
  build-orders)
4155
4283
  shift
4156
4284
  cmd_build_orders "$@"
@@ -4163,6 +4291,8 @@ aiur_engine_main() {
4163
4291
  shift
4164
4292
  cmd_github_cost "$@"
4165
4293
  ;;
4294
+ capabilities)
4295
+ shift; cmd_capabilities "$@" ;;
4166
4296
  github-usage)
4167
4297
  shift
4168
4298
  cmd_github_usage "$@"
@@ -4175,9 +4305,9 @@ aiur_engine_main() {
4175
4305
  shift
4176
4306
  cmd_watch "$@"
4177
4307
  ;;
4178
- executor-listen)
4308
+ listen|executor-listen)
4179
4309
  shift
4180
- cmd_executor_listen "$@"
4310
+ cmd_listen "$@"
4181
4311
  ;;
4182
4312
  executor-wait)
4183
4313
  shift
@@ -4231,6 +4361,10 @@ aiur_engine_main() {
4231
4361
  shift
4232
4362
  cmd_reset_budget "$@"
4233
4363
  ;;
4364
+ workspace-recover)
4365
+ shift
4366
+ cmd_workspace_recover "$@"
4367
+ ;;
4234
4368
  message)
4235
4369
  shift
4236
4370
  cmd_message "$@"
@@ -4266,7 +4400,9 @@ aiur_engine_main() {
4266
4400
  ;;
4267
4401
  esac
4268
4402
  }
4269
-
4403
+ source "$(dirname "${BASH_SOURCE[0]}")/aiur-queue.sh"
4404
+ source "$(dirname "${BASH_SOURCE[0]}")/aiur-epic.sh"
4405
+ source "$(dirname "${BASH_SOURCE[0]}")/aiur-capabilities.sh"
4270
4406
  if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
4271
4407
  aiur_engine_main "$@"
4272
4408
  fi
@@ -0,0 +1,41 @@
1
+ # Shared launcher epic command parser. Sourced by aiur-engine.sh.
2
+ cmd_epic() {
3
+ local action="${1:-}" epic="" who="${USER:-}" source="cli" json=0 ids="" count=0 arg n opts
4
+ [ "$#" -gt 0 ] && shift
5
+ case "$action" in
6
+ set) [ "$#" -gt 0 ] || { echo 'aiur: epic set requires an epic' >&2; exit 64; }; epic="$1"; shift
7
+ [[ "$epic" =~ ^[a-z0-9][a-z0-9_-]{0,63}$ ]] || { echo 'aiur: invalid epic key' >&2; exit 64; } ;;
8
+ clear|show|list) ;;
9
+ *) echo 'aiur: epic requires set, clear, show or list' >&2; exit 64 ;;
10
+ esac
11
+ while [ "$#" -gt 0 ]; do
12
+ arg="$1"
13
+ case "$arg" in
14
+ --json) json=1 ;;
15
+ --as)
16
+ [[ "$action" == set || "$action" == clear ]] && [ "$#" -ge 2 ] || { echo 'aiur: invalid --as' >&2; exit 64; }
17
+ who="$2"; shift ;;
18
+ --source)
19
+ [ "$action" = set ] && [ "$#" -ge 2 ] || { echo 'aiur: invalid --source' >&2; exit 64; }
20
+ source="$2"; shift
21
+ [[ "$source" == cli || "$source" == backfill-agent ]] || { echo 'aiur: invalid epic source' >&2; exit 64; } ;;
22
+ -*) echo 'aiur: unknown epic option' >&2; exit 64 ;;
23
+ *)
24
+ [ "$action" != list ] && [[ "$arg" =~ ^#?[1-9][0-9]{0,9}$ ]] || { echo 'aiur: invalid epic ticket id' >&2; exit 64; }
25
+ n="${arg#\#}"; [ -n "$ids" ] && ids="$ids, "; ids="$ids$n"; count=$((count + 1)) ;;
26
+ esac
27
+ shift
28
+ done
29
+ [ "$count" -le 200 ] || { echo 'aiur: epic accepts at most 200 ids' >&2; exit 64; }
30
+ opts="action: :$action"
31
+ if [[ "$action" == set || "$action" == clear ]]; then
32
+ [ "$count" -gt 0 ] && [[ "$who" =~ ^[A-Za-z0-9._-]{1,64}$ ]] || { echo 'aiur: epic write requires ids and a valid --as or USER' >&2; exit 64; }
33
+ [ "$action" = set ] && opts="$opts, epic: Base.decode64!(\"$(encode_control_value "$epic")\")"
34
+ opts="$opts, ids: [$ids], who: Base.decode64!(\"$(encode_control_value "$who")\")"
35
+ [ "$action" = set ] && { if [ "$source" = cli ]; then opts="$opts, source: :cli"; else opts="$opts, source: :\"backfill-agent\""; fi; }
36
+ elif [ "$count" -gt 0 ]; then
37
+ opts="$opts, ids: [$ids]"
38
+ fi
39
+ [ "$json" -eq 1 ] && opts="$opts, json: true"
40
+ run_control_rpc "Aiur.AgentControlCLI.epic([$opts])"
41
+ }
@@ -0,0 +1,107 @@
1
+ # Queue commands shared by the installed CLI and aiurdev.
2
+ queue_usage_error() { echo "aiur: queue $*" >&2; exit 64; }
3
+ queue_string() { printf 'Base.decode64!("%s")' "$(printf '%s' "$1" | base64 | tr -d '\n')"; }
4
+ queue_workspace_guard() {
5
+ if [ -n "${AIUR_AGENT_WORKSPACE:-}" ] || [[ "$PWD" == */aiur-workspaces/* ]] ||
6
+ [[ "${AIUR_PROJECT_ROOT:-}" == */aiur-workspaces/* ]] || [[ "${AIUR_REPO_ROOT:-}" == */aiur-workspaces/* ]]; then
7
+ echo 'aiur queue changes are blocked inside agent workspaces' >&2
8
+ exit 64
9
+ fi
10
+ }
11
+
12
+ cmd_queue() {
13
+ local verb="${1:-}" json=0 queue="" after="" at="" to="" root="" start_on="" opts="" id status=0 force=0 remove_markers=0 yes=0
14
+ local ids=()
15
+ case "$verb" in
16
+ show) ;;
17
+ add|set|remove|reorder|hold|release|recover|clear) queue_workspace_guard ;;
18
+ *) queue_usage_error 'expects show, add, set, remove, reorder, hold, release, recover, or clear' ;;
19
+ esac
20
+ shift
21
+ while [ "$#" -gt 0 ]; do
22
+ case "$1" in
23
+ --force) [ "$verb" = recover ] && [ "$force" -eq 0 ] || queue_usage_error '--force requires recover'; force=1 ;;
24
+ --remove-markers) [ "$verb" = clear ] && [ "$remove_markers" -eq 0 ] || queue_usage_error '--remove-markers requires clear'; remove_markers=1 ;;
25
+ --yes) [ "$verb" = clear ] && [ "$yes" -eq 0 ] || queue_usage_error '--yes requires clear'; yes=1 ;;
26
+ --json) [ "$verb" = show ] || queue_usage_error '--json requires show'; json=1 ;;
27
+ --queue)
28
+ [ "$#" -ge 2 ] && [ -n "$2" ] && [[ "$2" != -* ]] || queue_usage_error '--queue requires a name'
29
+ case "$verb" in show|add|hold|release) ;; *) queue_usage_error "--queue is not valid for $verb" ;; esac
30
+ [ -z "$queue" ] || queue_usage_error 'duplicate --queue'
31
+ queue="$2"; shift ;;
32
+ --start-on)
33
+ { [ "$verb" = add ] || [ "$verb" = set ]; } && [ "$#" -ge 2 ] && [ -z "$start_on" ] || queue_usage_error '--start-on requires add or set and a trigger'
34
+ case "$2" in
35
+ issue_closed|pr_merged|pr_approved|pr_ci_green|pr_opened) ;;
36
+ default) [ "$verb" = set ] || queue_usage_error 'default requires queue set' ;;
37
+ *) queue_usage_error '--start-on must be one of: issue_closed, pr_merged, pr_approved, pr_ci_green, pr_opened (set also accepts default)' ;;
38
+ esac
39
+ start_on="$2"; shift ;;
40
+ --after)
41
+ [ "$verb" = add ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^[1-9][0-9]*$ ]] && [ -z "$after" ] || queue_usage_error '--after requires add and a positive ticket ID'
42
+ after="$2"; shift ;;
43
+ --at)
44
+ [ "$verb" = add ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^(0|[1-9][0-9]*)$ ]] && [ -z "$at" ] || queue_usage_error '--at requires add and a non-negative position'
45
+ at="$2"; shift ;;
46
+ --to)
47
+ [ "$verb" = reorder ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^(0|[1-9][0-9]*)$ ]] && [ -z "$to" ] || queue_usage_error '--to requires reorder and a non-negative position'
48
+ to="$2"; shift ;;
49
+ --build-order)
50
+ [ "$verb" = add ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^[1-9][0-9]*$ ]] && [ -z "$root" ] || queue_usage_error '--build-order requires add and a positive root ID'
51
+ root="$2"; shift ;;
52
+ -*) queue_usage_error "received an unknown argument: $1" ;;
53
+ *)
54
+ if [ "$verb" = set ]; then
55
+ [ -z "$queue" ] && [ -n "$1" ] || queue_usage_error 'set requires exactly one queue name'
56
+ queue="$1"; shift; continue
57
+ fi
58
+ [[ "$1" =~ ^[1-9][0-9]*$ ]] || queue_usage_error "invalid ticket ID: $1"
59
+ for id in "${ids[@]}"; do [ "$id" != "$1" ] || queue_usage_error "duplicate ticket ID: $1"; done
60
+ ids+=("$1") ;;
61
+ esac
62
+ shift
63
+ done
64
+ case "$verb" in
65
+ show) [ "${#ids[@]}" -eq 0 ] || queue_usage_error 'show does not take ticket IDs' ;;
66
+ add)
67
+ if [ -n "$root" ]; then
68
+ [ "${#ids[@]}" -eq 0 ] && [ -z "$after$at" ] || queue_usage_error '--build-order cannot be combined with ticket IDs, --after, or --at'
69
+ else
70
+ [ "${#ids[@]}" -gt 0 ] || queue_usage_error 'add requires ticket IDs or --build-order'
71
+ fi ;;
72
+ set) [ -n "$queue" ] && [ -n "$start_on" ] || queue_usage_error 'set requires a queue name and --start-on TRIGGER|default' ;;
73
+ recover) [ "${#ids[@]}" -eq 0 ] || queue_usage_error 'recover does not take ticket IDs' ;;
74
+ clear)
75
+ [ "${#ids[@]}" -eq 0 ] && [ "$remove_markers" -eq 1 ] || queue_usage_error 'clear requires --remove-markers and no ticket IDs'
76
+ [ "$yes" -eq 1 ] || { echo 'aiur: queue clear requires --yes; removes all queue membership and markers, keeping todo' >&2; exit 1; } ;;
77
+ remove) [ "${#ids[@]}" -gt 0 ] || queue_usage_error 'remove requires ticket IDs' ;;
78
+ reorder) [ "${#ids[@]}" -eq 1 ] && [ -n "$to" ] || queue_usage_error 'reorder requires one ticket ID and --to POS' ;;
79
+ hold|release)
80
+ { [ "${#ids[@]}" -eq 1 ] && [ -z "$queue" ]; } ||
81
+ { [ "${#ids[@]}" -eq 0 ] && [ -n "$queue" ]; } || queue_usage_error "$verb requires one ticket ID or --queue NAME" ;;
82
+ esac
83
+ if [ "$verb" != show ]; then
84
+ opts="verb: :$verb, caller_agent_workspace: $(queue_string "${AIUR_AGENT_WORKSPACE:-}")"
85
+ fi
86
+ [ "$force" -eq 1 ] && opts="$opts, force: true"
87
+ [ "$remove_markers" -eq 1 ] && opts="$opts, remove_markers: true"
88
+ [ "$yes" -eq 1 ] && opts="$opts, yes: true"
89
+ [ "$json" -eq 1 ] && opts="json: true"
90
+ [ -n "$queue" ] && opts="${opts:+$opts, }queue: $(queue_string "$queue")"
91
+ [ -n "$start_on" ] && opts="$opts, start_on: $(queue_string "$start_on")"
92
+ [ -n "$root" ] && opts="$opts, build_order: $root"
93
+ [ -n "$after" ] && opts="$opts, after: \"$after\""
94
+ [ -n "$at" ] && opts="$opts, at: $at"
95
+ [ -n "$to" ] && opts="$opts, to: $to"
96
+ if [ "${#ids[@]}" -gt 0 ]; then
97
+ opts="$opts, ids: ["
98
+ for id in "${ids[@]}"; do opts="$opts\"$id\","; done
99
+ opts="${opts%,}]"
100
+ fi
101
+ if [ "$verb" = add ] || [ "$verb" = remove ]; then
102
+ AIUR_CONTROL_RPC_TIMEOUT_SECONDS="$(todo_rpc_seconds "${#ids[@]}" 0)"
103
+ fi
104
+ AIUR_CONTROL_COMMAND="queue $verb" run_control_rpc "Aiur.AgentControlCLI.queue([$opts])" || status=$?
105
+ if [ "$status" -eq 124 ]; then echo 'aiur: queue outcome unknown; run aiur queue show before retrying' >&2; fi
106
+ return "$status"
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiur-cli",
3
- "version": "0.0.9-nightly.5a0d430",
3
+ "version": "0.0.9-nightly.f31ca99",
4
4
  "description": "AI Unit Runtime for Executors.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/aiur-team/aiur",
@@ -18,6 +18,9 @@
18
18
  "files": [
19
19
  "bin/aiur.js",
20
20
  "libexec/aiur-engine.sh",
21
+ "libexec/aiur-queue.sh",
22
+ "libexec/aiur-epic.sh",
23
+ "libexec/aiur-capabilities.sh",
21
24
  "libexec/aiur-pane-ctrlc",
22
25
  "scripts/postinstall.mjs",
23
26
  "share/aiur.tmux.conf"
@@ -35,8 +38,8 @@
35
38
  ],
36
39
  "opencodeVersion": "1.17.10",
37
40
  "optionalDependencies": {
38
- "aiur-cli-linux-x64": "0.0.9-nightly.5a0d430",
39
- "aiur-cli-linux-arm64": "0.0.9-nightly.5a0d430",
40
- "aiur-cli-darwin-arm64": "0.0.9-nightly.5a0d430"
41
+ "aiur-cli-linux-x64": "0.0.9-nightly.f31ca99",
42
+ "aiur-cli-linux-arm64": "0.0.9-nightly.f31ca99",
43
+ "aiur-cli-darwin-arm64": "0.0.9-nightly.f31ca99"
41
44
  }
42
45
  }