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

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
  }
@@ -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,25 @@ 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
466
464
  aiur github-usage [--json] per-actor (daemon vs agent) GitHub usage and ceilings
467
465
  aiur alerts [--needs-attention] show structured alert feed
468
466
  aiur watch [--full|--changes] [--interval <secs>] server-side status board
469
- aiur executor-listen [--topic <pattern>] stream Executor events as JSON lines
467
+ aiur listen [--topic <pattern> | --ticket <id>] stream events as JSON lines
468
+ aiur executor-listen [--topic <pattern>] deprecated alias for listen
470
469
  aiur executor-wait [--timeout <seconds>] [--json] block until Executor work arrives
471
470
  aiur executor-fast-forward <wake-id> [--as <id>] acknowledge an externally covered wake prefix
472
471
  aiur executor-emit <topic> --payload <json> publish an Executor event
473
472
  aiur executor-subscribe|executor-unsubscribe <pattern>
474
473
  aiur executor-subscriptions list persistent Executor bindings
474
+ aiur workspace-recover <ticket-identifier> <generation> release a held workspace after verified provider exit
475
475
  aiur executor-roster [--json] list Executor consumers with their liveness evidence
476
476
  aiur executor-claim [--as <id>] claim the wake stream, or refuse and name the live owner
477
477
  aiur executor-release [--as <id>] give up this consumer's claim
@@ -488,14 +488,13 @@ Usage: aiur [--interactive] [--no-dashboard] [--executor] [--pause] [--max-agent
488
488
  aiur ask <title> [--body <text>|--body-file <path>] [--urgency low|normal|high] [--blocking]
489
489
  aiur ask --done <id> [--note <text>] create or resolve an operator request
490
490
  aiur asks [--open|--all] [--json] inspect current-repository operator requests
491
+ aiur doctor [--repair] check mise shims; repair only with consent
491
492
  aiur cleanup-stale [--dry-run] list/reap stale manual-smoke leftovers
492
493
  aiur --version
493
-
494
494
  Bare aiur: start or attach to this directory's interactive session.
495
495
  EOF
496
496
  }
497
497
 
498
- # --- one-shot: --version (no tmux) -------------------------------------------
499
498
 
500
499
  run_version() {
501
500
  resolve_release
@@ -511,7 +510,6 @@ run_version() {
511
510
  exec "${release_cmd[@]}"
512
511
  }
513
512
 
514
- # --- one-shot: init (interactive wizard, distribution-free, no tmux) ----------
515
513
 
516
514
  run_init() {
517
515
  resolve_release
@@ -522,6 +520,47 @@ run_init() {
522
520
  exec "${release_cmd[@]}"
523
521
  }
524
522
 
523
+ run_local_cli() {
524
+ resolve_release
525
+ build_init_cmd
526
+ init_argv_file
527
+ write_argv "$@"
528
+ export AIUR_ARGV_FILE="$argv_file"
529
+ exec "${release_cmd[@]}"
530
+ }
531
+
532
+ run_claude_login() {
533
+ resolve_release
534
+ local profile_dir
535
+ build_init_cmd
536
+ init_argv_file
537
+ write_argv "__login_prepare" "${@:2}"
538
+ export AIUR_ARGV_FILE="$argv_file"
539
+ profile_dir="$("${release_cmd[@]}")" || return $?
540
+ [ -n "$profile_dir" ] || { echo "aiur: account profile directory is unavailable" >&2; return 1; }
541
+ exec env "CLAUDE_CONFIG_DIR=$profile_dir" claude
542
+ }
543
+
544
+ run_account_login() {
545
+ local harness="$2"
546
+ case "$harness" in
547
+ kimi|deepseek|openrouter) run_local_cli "$@"; return $? ;;
548
+ esac
549
+ resolve_release
550
+ local profile_dir
551
+ build_init_cmd
552
+ init_argv_file
553
+ write_argv "__login_prepare" "$harness" "${@:3}"
554
+ export AIUR_ARGV_FILE="$argv_file"
555
+ profile_dir="$("${release_cmd[@]}")" || return $?
556
+ [ -n "$profile_dir" ] || { echo "aiur: account profile directory is unavailable" >&2; return 1; }
557
+ case "$harness" in
558
+ claude) exec env "CLAUDE_CONFIG_DIR=$profile_dir" claude ;;
559
+ codex) exec env "CODEX_HOME=$profile_dir" codex ;;
560
+ *) echo "aiur: unsupported account harness: $harness" >&2; return 64 ;;
561
+ esac
562
+ }
563
+
525
564
  # --- one-shot: --todo (control RPC; requires a running daemon) ----------------
526
565
 
527
566
  parsed_todo_only=0
@@ -723,22 +762,10 @@ scrub_run_only_env() {
723
762
  }
724
763
 
725
764
  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.
765
+ # Default dashboard bind host. Remote access is an explicit operator choice.
766
+ # The BEAM applies this below `server.host`; `--host` has highest precedence.
732
767
  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
768
+ printf '%s' "${AIUR_DEFAULT_DASHBOARD_HOST:-127.0.0.1}"
742
769
  }
743
770
 
744
771
  build_run_argv() {
@@ -2620,8 +2647,8 @@ parse_issue_targets() {
2620
2647
 
2621
2648
  [ "${#parsed_targets[@]}" -gt 0 ]
2622
2649
  }
2623
-
2624
2650
  cmd_status() {
2651
+ [ ! -f "$engine_dir/aiur-mise-doctor" ] || bash "$engine_dir/aiur-mise-doctor" --check || true
2625
2652
  [ "$#" -eq 0 ] || die "status does not accept arguments"
2626
2653
  run_control_rpc "Aiur.AgentControlCLI.status()"
2627
2654
  }
@@ -2633,6 +2660,44 @@ cmd_usage() {
2633
2660
  run_control_rpc "Aiur.AgentControlCLI.usage()"
2634
2661
  }
2635
2662
 
2663
+ cmd_accounts() {
2664
+ local json_arg=false harness="" arg encoded expression
2665
+ for arg in "$@"; do
2666
+ case "$arg" in
2667
+ --json)
2668
+ [ "$json_arg" = false ] || die "accounts accepts --json only once"
2669
+ json_arg=true
2670
+ ;;
2671
+ --all)
2672
+ ;;
2673
+ -*)
2674
+ die "accounts accepts an optional harness and --json"
2675
+ ;;
2676
+ *)
2677
+ [ -z "$harness" ] || die "accounts accepts only one harness"
2678
+ harness="$arg"
2679
+ ;;
2680
+ esac
2681
+ done
2682
+
2683
+ resolve_release || return $?
2684
+ prepare_distribution || die "distribution setup failed; cannot contact aiur"
2685
+ resolve_control_identity_from_records
2686
+ if [ "$(probe_node_liveness)" = "down" ]; then
2687
+ # The local one-shot CLI renders the identity and marks usage unavailable.
2688
+ # It never makes a provider request.
2689
+ run_local_cli accounts "$@"
2690
+ else
2691
+ if [ -n "$harness" ]; then
2692
+ encoded="$(printf '%s' "$harness" | base64 | tr -d '\n')"
2693
+ expression="Aiur.AgentControlCLI.accounts($json_arg, Base.decode64!(\"$encoded\"))"
2694
+ else
2695
+ expression="Aiur.AgentControlCLI.accounts($json_arg)"
2696
+ fi
2697
+ run_control_rpc "$expression"
2698
+ fi
2699
+ }
2700
+
2636
2701
  cmd_pause_resume() {
2637
2702
  local command="$1"
2638
2703
  shift
@@ -2680,6 +2745,16 @@ cmd_reset_budget() {
2680
2745
  run_control_rpc "$expression"
2681
2746
  }
2682
2747
 
2748
+ # Requires the operator to name the exact ticket and generation shown by
2749
+ # status. The daemon independently verifies the recorded boot proof.
2750
+ cmd_workspace_recover() {
2751
+ [ "$#" -eq 2 ] || { echo "aiur: workspace-recover expects a ticket identifier and generation (e.g. aiur workspace-recover ENG-123 7)" >&2; exit 64; }
2752
+ local ticket="$1" generation="$2" encoded
2753
+ [[ "$generation" =~ ^[1-9][0-9]*$ ]] || { echo "aiur: workspace-recover generation must be a positive integer" >&2; exit 64; }
2754
+ encoded="$(printf '%s' "$ticket" | base64 | tr -d '\n')"
2755
+ run_control_rpc "Aiur.AgentControlCLI.recover_workspace(Base.decode64!(\"$encoded\"), $generation)"
2756
+ }
2757
+
2683
2758
  # `aiur message <issue> <text>` — deliver Executor text to one running agent.
2684
2759
  # The text is base64-encoded for the RPC hop so arbitrary content (quotes,
2685
2760
  # backslashes, `#{}`, newlines) survives without Elixir-string escaping.
@@ -3161,21 +3236,61 @@ cmd_watch() {
3161
3236
  fi
3162
3237
  }
3163
3238
 
3164
- cmd_executor_listen() {
3165
- local topic="executor.#" arg
3239
+ listen_clock() { printf '%s' "$SECONDS"; }
3240
+
3241
+ cmd_listen() {
3242
+ local topic="executor.#" ticket="" arg
3166
3243
  while [ "$#" -gt 0 ]; do
3167
3244
  arg="$1"
3168
3245
  case "$arg" in
3169
3246
  --topic) shift; topic="${1:-}" ;;
3170
3247
  --topic=*) topic="${arg#--topic=}" ;;
3171
- *) echo "aiur: executor-listen accepts --topic <pattern>" >&2; exit 64 ;;
3248
+ --ticket) shift; ticket="${1:-}" ;;
3249
+ --ticket=*) ticket="${arg#--ticket=}" ;;
3250
+ *) echo "aiur: listen accepts --topic <pattern> or --ticket <id>" >&2; exit 64 ;;
3172
3251
  esac
3173
- [ -n "$topic" ] || { echo "aiur: executor-listen requires a topic" >&2; exit 64; }
3174
3252
  shift
3175
3253
  done
3254
+ if [ -n "$ticket" ]; then
3255
+ [[ "$ticket" =~ ^[0-9]+$ ]] || { echo "aiur: listen --ticket expects a numeric ticket id" >&2; exit 64; }
3256
+ [ "$topic" = "executor.#" ] || { echo "aiur: listen cannot combine --ticket and --topic" >&2; exit 64; }
3257
+ topic="ticket.${ticket}.#"
3258
+ fi
3259
+ [ -n "$topic" ] || { echo "aiur: listen requires a topic" >&2; exit 64; }
3176
3260
  local encoded
3177
3261
  encoded="$(printf '%s' "$topic" | base64 | tr -d '\n')"
3178
- run_control_stream "Aiur.AgentControlCLI.executor_listen(topic: Base.decode64!(\"$encoded\"))"
3262
+ run_control_rpc "Aiur.AgentControlCLI.executor_listen_validate(Base.decode64!(\"$encoded\"))" || return $?
3263
+ # A stream that stayed up 30s had a live connection, so losing it starts a
3264
+ # new outage. Each outage gets a bounded backoff totalling ~10 minutes, long
3265
+ # enough to outlast a normal `aiur restart`.
3266
+ # ponytail: attempt duration stands in for "connected"; a wedged daemon whose
3267
+ # RPC hangs 30s+ before failing keeps the listener retrying past the budget.
3268
+ # Upgrade to a listener-ready signal if that case shows up in practice.
3269
+ local waited=0 delay=2 started status
3270
+ while :; do
3271
+ started="$(listen_clock)"
3272
+ status=0
3273
+ # Subshell: a `die` inside (e.g. the release dir missing mid-rebuild during
3274
+ # `aiurdev restart`) fails this attempt with exit 1 instead of the listener.
3275
+ (run_control_stream "Aiur.AgentControlCLI.executor_listen(topic: Base.decode64!(\"$encoded\"))") || status=$?
3276
+ [ "$status" -eq 0 ] && return 0
3277
+ if [ "$status" -ne 1 ] || [ "${AIUR_LISTEN_RECONNECT:-1}" -ne 1 ]; then
3278
+ echo "aiur: listen stopped after streaming control RPC failure (exit ${status}); restart the command after correcting the daemon error" >&2
3279
+ return "$status"
3280
+ fi
3281
+ if [ $(($(listen_clock) - started)) -ge 30 ]; then
3282
+ waited=0
3283
+ delay=2
3284
+ fi
3285
+ if [ "$waited" -ge 600 ]; then
3286
+ echo "aiur: listen could not reconnect within ${waited} seconds; daemon may be unavailable" >&2
3287
+ return 1
3288
+ fi
3289
+ echo "aiur: listen lost the daemon stream (exit ${status}); reconnecting in ${delay} seconds" >&2
3290
+ sleep "$delay"
3291
+ waited=$((waited + delay))
3292
+ delay=$((delay * 2 > 60 ? 60 : delay * 2))
3293
+ done
3179
3294
  }
3180
3295
 
3181
3296
  cmd_executor_wait() {
@@ -4061,8 +4176,8 @@ cmd_upgrade() {
4061
4176
  }
4062
4177
 
4063
4178
  # --- dispatch ----------------------------------------------------------------
4064
-
4065
4179
  dispatch_run() {
4180
+ [ ! -f "$engine_dir/aiur-mise-doctor" ] || bash "$engine_dir/aiur-mise-doctor" --check || true
4066
4181
  local mode="foreground" arg
4067
4182
  local args=()
4068
4183
 
@@ -4080,7 +4195,6 @@ dispatch_run() {
4080
4195
  # `set -u` — happens for a bare `--bg` run. Guard the expansion.
4081
4196
  run_session "$mode" "${args[@]+"${args[@]}"}"
4082
4197
  }
4083
-
4084
4198
  aiur_engine_main() {
4085
4199
  local cmd="${1:-}"
4086
4200
  # Names the running subcommand in control-RPC diagnostics so a failure says
@@ -4106,6 +4220,16 @@ aiur_engine_main() {
4106
4220
  init)
4107
4221
  run_init "$@"
4108
4222
  ;;
4223
+ login)
4224
+ run_account_login "$@"
4225
+ ;;
4226
+ accounts)
4227
+ shift
4228
+ cmd_accounts "$@"
4229
+ ;;
4230
+ logout)
4231
+ run_local_cli "$@"
4232
+ ;;
4109
4233
  findings)
4110
4234
  run_findings "$@"
4111
4235
  ;;
@@ -4119,6 +4243,7 @@ aiur_engine_main() {
4119
4243
  shift
4120
4244
  dispatch_run "$@"
4121
4245
  ;;
4246
+ doctor) shift; exec bash "$engine_dir/aiur-mise-doctor" "$@" ;;
4122
4247
  status)
4123
4248
  shift
4124
4249
  cmd_status "$@"
@@ -4151,6 +4276,11 @@ aiur_engine_main() {
4151
4276
  shift
4152
4277
  cmd_units "$@"
4153
4278
  ;;
4279
+ queue)
4280
+ shift
4281
+ cmd_queue "$@"
4282
+ ;;
4283
+ epic) shift; cmd_epic "$@" ;;
4154
4284
  build-orders)
4155
4285
  shift
4156
4286
  cmd_build_orders "$@"
@@ -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 "$@"
@@ -4267,6 +4401,8 @@ aiur_engine_main() {
4267
4401
  esac
4268
4402
  }
4269
4403
 
4404
+ source "$(dirname "${BASH_SOURCE[0]}")/aiur-queue.sh"
4405
+ source "$(dirname "${BASH_SOURCE[0]}")/aiur-epic.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,93 @@
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="" opts="" id status=0 force=0 remove_markers=0 yes=0
14
+ local ids=()
15
+ case "$verb" in
16
+ show) ;;
17
+ add|remove|reorder|hold|release|recover|clear) queue_workspace_guard ;;
18
+ *) queue_usage_error 'expects show, add, 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
+ --after)
33
+ [ "$verb" = add ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^[1-9][0-9]*$ ]] && [ -z "$after" ] || queue_usage_error '--after requires add and a positive ticket ID'
34
+ after="$2"; shift ;;
35
+ --at)
36
+ [ "$verb" = add ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^(0|[1-9][0-9]*)$ ]] && [ -z "$at" ] || queue_usage_error '--at requires add and a non-negative position'
37
+ at="$2"; shift ;;
38
+ --to)
39
+ [ "$verb" = reorder ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^(0|[1-9][0-9]*)$ ]] && [ -z "$to" ] || queue_usage_error '--to requires reorder and a non-negative position'
40
+ to="$2"; shift ;;
41
+ --build-order)
42
+ [ "$verb" = add ] && [ "$#" -ge 2 ] && [[ "$2" =~ ^[1-9][0-9]*$ ]] && [ -z "$root" ] || queue_usage_error '--build-order requires add and a positive root ID'
43
+ root="$2"; shift ;;
44
+ -*) queue_usage_error "received an unknown argument: $1" ;;
45
+ *)
46
+ [[ "$1" =~ ^[1-9][0-9]*$ ]] || queue_usage_error "invalid ticket ID: $1"
47
+ for id in "${ids[@]}"; do [ "$id" != "$1" ] || queue_usage_error "duplicate ticket ID: $1"; done
48
+ ids+=("$1") ;;
49
+ esac
50
+ shift
51
+ done
52
+ case "$verb" in
53
+ show) [ "${#ids[@]}" -eq 0 ] || queue_usage_error 'show does not take ticket IDs' ;;
54
+ add)
55
+ if [ -n "$root" ]; then
56
+ [ "${#ids[@]}" -eq 0 ] && [ -z "$after$at" ] || queue_usage_error '--build-order cannot be combined with ticket IDs, --after, or --at'
57
+ else
58
+ [ "${#ids[@]}" -gt 0 ] || queue_usage_error 'add requires ticket IDs or --build-order'
59
+ fi ;;
60
+ recover) [ "${#ids[@]}" -eq 0 ] || queue_usage_error 'recover does not take ticket IDs' ;;
61
+ clear)
62
+ [ "${#ids[@]}" -eq 0 ] && [ "$remove_markers" -eq 1 ] || queue_usage_error 'clear requires --remove-markers and no ticket IDs'
63
+ [ "$yes" -eq 1 ] || { echo 'aiur: queue clear requires --yes; removes all queue membership and markers, keeping todo' >&2; exit 1; } ;;
64
+ remove) [ "${#ids[@]}" -gt 0 ] || queue_usage_error 'remove requires ticket IDs' ;;
65
+ reorder) [ "${#ids[@]}" -eq 1 ] && [ -n "$to" ] || queue_usage_error 'reorder requires one ticket ID and --to POS' ;;
66
+ hold|release)
67
+ { [ "${#ids[@]}" -eq 1 ] && [ -z "$queue" ]; } ||
68
+ { [ "${#ids[@]}" -eq 0 ] && [ -n "$queue" ]; } || queue_usage_error "$verb requires one ticket ID or --queue NAME" ;;
69
+ esac
70
+ if [ "$verb" != show ]; then
71
+ opts="verb: :$verb, caller_agent_workspace: $(queue_string "${AIUR_AGENT_WORKSPACE:-}")"
72
+ fi
73
+ [ "$force" -eq 1 ] && opts="$opts, force: true"
74
+ [ "$remove_markers" -eq 1 ] && opts="$opts, remove_markers: true"
75
+ [ "$yes" -eq 1 ] && opts="$opts, yes: true"
76
+ [ "$json" -eq 1 ] && opts="json: true"
77
+ [ -n "$queue" ] && opts="${opts:+$opts, }queue: $(queue_string "$queue")"
78
+ [ -n "$root" ] && opts="$opts, build_order: $root"
79
+ [ -n "$after" ] && opts="$opts, after: \"$after\""
80
+ [ -n "$at" ] && opts="$opts, at: $at"
81
+ [ -n "$to" ] && opts="$opts, to: $to"
82
+ if [ "${#ids[@]}" -gt 0 ]; then
83
+ opts="$opts, ids: ["
84
+ for id in "${ids[@]}"; do opts="$opts\"$id\","; done
85
+ opts="${opts%,}]"
86
+ fi
87
+ if [ "$verb" = add ] || [ "$verb" = remove ]; then
88
+ AIUR_CONTROL_RPC_TIMEOUT_SECONDS="$(todo_rpc_seconds "${#ids[@]}" 0)"
89
+ fi
90
+ AIUR_CONTROL_COMMAND="queue $verb" run_control_rpc "Aiur.AgentControlCLI.queue([$opts])" || status=$?
91
+ if [ "$status" -eq 124 ]; then echo 'aiur: queue outcome unknown; run aiur queue show before retrying' >&2; fi
92
+ return "$status"
93
+ }
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.fac02a9",
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,8 @@
18
18
  "files": [
19
19
  "bin/aiur.js",
20
20
  "libexec/aiur-engine.sh",
21
+ "libexec/aiur-queue.sh",
22
+ "libexec/aiur-epic.sh",
21
23
  "libexec/aiur-pane-ctrlc",
22
24
  "scripts/postinstall.mjs",
23
25
  "share/aiur.tmux.conf"
@@ -35,8 +37,8 @@
35
37
  ],
36
38
  "opencodeVersion": "1.17.10",
37
39
  "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"
40
+ "aiur-cli-linux-x64": "0.0.9-nightly.fac02a9",
41
+ "aiur-cli-linux-arm64": "0.0.9-nightly.fac02a9",
42
+ "aiur-cli-darwin-arm64": "0.0.9-nightly.fac02a9"
41
43
  }
42
44
  }