agent-bios 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -113,8 +113,39 @@ real Microsoft Excel engine|claude/CLAUDE.md|claude/guides/coding-staged-workflo
113
113
  severity contract|README.md|claude/guides/coding-staged-workflow.md
114
114
  Ambient state|claude/CLAUDE.md|claude/guides/tooling-gotchas.md
115
115
  the full lifecycle of what you create|claude/CLAUDE.md|claude/guides/tooling-gotchas.md
116
+ dual-provider frontier design drafts|claude/CLAUDE.md|claude/guides/cli-multi-model-workflow.md
116
117
  ANCHORS
117
118
 
119
+ # Domain-manifest gate: bullet<->anchor bijection, file coverage, router
120
+ # co-packaging (config/domains.json vs the monolith); its --self-test proves
121
+ # every negative control still fails, so a green gate is falsifiable.
122
+ if [ -f config/domains.json ]; then
123
+ python3 scripts/check-domains.py >/dev/null \
124
+ || { echo "FAIL: domains manifest gate (run scripts/check-domains.py)"; fail=1; }
125
+ python3 scripts/check-domains.py --self-test >/dev/null \
126
+ || { echo "FAIL: domains gate self-test missed a negative control"; fail=1; }
127
+ bash scripts/test-assemble.sh >/dev/null \
128
+ || { echo "FAIL: assembler scenario suite (run scripts/test-assemble.sh)"; fail=1; }
129
+ fi
130
+
131
+ # Learning record gate: config/learning.schema.json (collection-loop
132
+ # SSOT) vs its fixtures; --self-test proves every mutation is still caught.
133
+ if [ -f config/learning.schema.json ]; then
134
+ python3 scripts/check-learning.py >/dev/null \
135
+ || { echo "FAIL: learning record gate (run scripts/check-learning.py)"; fail=1; }
136
+ python3 scripts/check-learning.py --self-test >/dev/null \
137
+ || { echo "FAIL: learning gate self-test missed a negative control"; fail=1; }
138
+ fi
139
+
140
+ # Lexicon gate: forbid deprecated terminology tokens in live files
141
+ # (LEXICON.md is the SSOT); --self-test proves the detector can fire.
142
+ if [ -f LEXICON.md ]; then
143
+ python3 scripts/check-lexicon.py >/dev/null \
144
+ || { echo "FAIL: lexicon gate (run scripts/check-lexicon.py)"; fail=1; }
145
+ python3 scripts/check-lexicon.py --self-test >/dev/null \
146
+ || { echo "FAIL: lexicon gate self-test failed"; fail=1; }
147
+ fi
148
+
118
149
  # The launcher's Textual preflight UI tests need the managed venv (textual).
119
150
  # Provision it if missing; every non-UI check above runs under system python.
120
151
  VENV="${AGENT_LAUNCH_VENV:-$HOME/.local/share/agent-launch/venv}"
@@ -551,7 +582,9 @@ if launcher.is_file() and launch_profile:
551
582
  def set_plan(self, plan):
552
583
  self.plan = plan
553
584
 
554
- def choose(self, title, options, default, allow_back, preview=None, learning_lines=None):
585
+ def choose(self, title, options, default, allow_back, preview=None, corpus_lines=None):
586
+ if title == "Mode":
587
+ return "builder"
555
588
  if title == "Preset":
556
589
  if preview is not None:
557
590
  balanced = launcher_module.setup_summary_lines(preview("balanced"))
@@ -772,12 +805,17 @@ if launcher.is_file() and launch_profile:
772
805
  os.close(master)
773
806
  return bytes(transcript), picker_status
774
807
 
808
+ # Root menu is now a Mode picker (General user / Builder / Session
809
+ # distill); Builder is the default highlight and lists Custom. Reaching
810
+ # Custom means: Enter the root to open Builder's preset submenu, then
811
+ # step down through its fixed presets (Balanced default-highlighted).
775
812
  open_custom_steps = (
813
+ (b"Esc cancel | q cancel", b"\r"),
776
814
  (b"native multi-perspective review", b""),
777
- (b"Esc cancel | q cancel", b"\x1b[B"),
815
+ (b"Esc back | q cancel", b"\x1b[B"),
778
816
  (b"hybrid onto", b"\x1b[B"),
779
817
  (b"high-volume", b"\x1b[B"),
780
- (b"learning-refinement", b"\x1b[B"),
818
+ (b"the standing spawn policy is lifted", b"\x1b[B"),
781
819
  (b"Open a settings hub", b"\r"),
782
820
  )
783
821
  down = b"\x1b[B"
@@ -798,7 +836,7 @@ if launcher.is_file() and launch_profile:
798
836
  b"WORKHORSE",
799
837
  b"SWEEP",
800
838
  b"About highlighted option",
801
- b"HELM default for everyday work",
839
+ b"Tune tiers, review routes, and permissions",
802
840
  b"Options (",
803
841
  )
804
842
  positions = {token: transcript.find(token) for token in layout_tokens}
@@ -809,7 +847,7 @@ if launcher.is_file() and launch_profile:
809
847
  positions[b"Current setup"]
810
848
  < min(positions[token] for token in (b"FRONTIER", b"HELM", b"WORKHORSE", b"SWEEP"))
811
849
  < positions[b"About highlighted option"]
812
- < positions[b"HELM default for everyday work"]
850
+ < positions[b"Tune tiers, review routes, and permissions"]
813
851
  < positions[b"Options ("]
814
852
  ):
815
853
  mark_fail("agent-launch layout is not setup then description then options")
@@ -821,12 +859,27 @@ if launcher.is_file() and launch_profile:
821
859
  if picker_status != 130:
822
860
  mark_fail(f"agent-launch root Esc returned {picker_status}, want 130")
823
861
 
862
+ # Root Mode picker navigation: Esc from the Builder preset submenu
863
+ # returns to a freshly re-rendered Mode picker (Builder's own
864
+ # description reappears) rather than cancelling the launcher outright.
865
+ _, picker_status = run_picker_scenario(
866
+ "mode picker navigation",
867
+ (
868
+ (b"Esc cancel | q cancel", b"\r"),
869
+ (b"native multi-perspective review", b"\x1b"),
870
+ (b"Tune tiers, review routes, and permissions", b"q"),
871
+ ),
872
+ )
873
+ if picker_status != 130:
874
+ mark_fail(f"agent-launch mode picker Esc-back returned {picker_status}, want 130")
875
+
824
876
  if argv_log.exists():
825
877
  argv_log.unlink()
826
878
  _, picker_status = run_picker_scenario(
827
879
  "launch confirmation q cancellation",
828
880
  (
829
881
  (b"Esc cancel | q cancel", b"\r"),
882
+ (b"HELM default for everyday work", b"\r"),
830
883
  (b"Launch? [Y/n/q]:", b"q\r"),
831
884
  ),
832
885
  dry_run=False,
@@ -1128,10 +1181,19 @@ if launcher.is_file() and launch_profile:
1128
1181
  # TERM=dumb routes to numbered prompts (textual renders on any usable
1129
1182
  # terminal, so the numbered fallback is gated on TERM/non-TTY/textual
1130
1183
  # availability, not a terminfo probe). 'b' is the numbered back command.
1131
- custom_number = str(len(fake_data["presets"]) + 1).encode()
1184
+ # The root Mode menu is fixed (General=1, Builder=2, Session distill=3)
1185
+ # regardless of preset count; only Custom's position within Builder's
1186
+ # own preset submenu depends on how many builder-mode presets exist.
1187
+ builder_preset_count = sum(
1188
+ 1
1189
+ for data in fake_data["presets"].values()
1190
+ if data.get("mode", "builder") == "builder"
1191
+ )
1192
+ custom_number = str(builder_preset_count + 1).encode()
1132
1193
  transcript, picker_status = run_picker_scenario(
1133
1194
  "numbered fallback (TERM=dumb)",
1134
1195
  (
1196
+ (b"Tune tiers, review routes, and permissions", b"2\n"),
1135
1197
  (b"Open a settings hub", custom_number + b"\n"),
1136
1198
  (b"Custom settings", b"b\n"),
1137
1199
  (b"HELM default for everyday work", b"1\n"),
@@ -1145,27 +1207,27 @@ if launcher.is_file() and launch_profile:
1145
1207
  ):
1146
1208
  mark_fail("agent-launch numbered fallback (TERM=dumb) did not preserve numbered back")
1147
1209
 
1148
- # Session Learning hub: enter, render status, back out, launch balanced.
1149
- learning_number = str(len(fake_data["presets"]) + 2).encode()
1210
+ # Session Distill hub: enter, render status, back out, launch balanced.
1150
1211
  transcript, picker_status = run_picker_scenario(
1151
- "numbered learning hub (TERM=dumb)",
1212
+ "numbered distill hub (TERM=dumb)",
1152
1213
  (
1153
- (b"Session Learning", learning_number + b"\n"),
1214
+ (b"Session distill", b"3\n"),
1154
1215
  (b"Versions & rollback", b"4\n"),
1216
+ (b"Tune tiers, review routes, and permissions", b"2\n"),
1155
1217
  (b"HELM default for everyday work", b"1\n"),
1156
1218
  ),
1157
1219
  term="dumb",
1158
1220
  )
1159
- learning_panel_rendered = (
1221
+ corpus_panel_rendered = (
1160
1222
  b"Applied version" in transcript or b"not projected yet" in transcript
1161
1223
  )
1162
1224
  if (
1163
1225
  picker_status != 0
1164
1226
  or b"Traceback" in transcript
1165
- or not learning_panel_rendered
1227
+ or not corpus_panel_rendered
1166
1228
  or b"Preset Balanced" not in transcript
1167
1229
  ):
1168
- mark_fail("agent-launch numbered learning hub did not render or return")
1230
+ mark_fail("agent-launch numbered distill hub did not render or return")
1169
1231
 
1170
1232
  passed = invoke([
1171
1233
  sys.executable, str(launcher), "--no-tui", "codex", "--", "exec", "--json", "probe"
@@ -1492,6 +1554,29 @@ if launcher.is_file() and launch_profile:
1492
1554
  "agent-launch Claude expert overrides were not appended last"
1493
1555
  )
1494
1556
 
1557
+ # Vanilla (mode=general): the raw backend with nothing applied — no
1558
+ # launch contract, no agents, no permission-bypass flag.
1559
+ vanilla_configured = invoke([
1560
+ sys.executable, str(launcher), "--preset", "vanilla", "--yes", "--dry-run",
1561
+ "claude",
1562
+ ], env=env)
1563
+ if vanilla_configured.returncode != 0:
1564
+ mark_fail(
1565
+ f"agent-launch vanilla dry-run failed: {vanilla_configured.stderr.strip()}"
1566
+ )
1567
+ else:
1568
+ vanilla_argv = json.loads(vanilla_configured.stdout.splitlines()[-1])
1569
+ for forbidden in (
1570
+ "--append-system-prompt", "--agents", "--dangerously-skip-permissions",
1571
+ ):
1572
+ if forbidden in vanilla_argv:
1573
+ mark_fail(f"agent-launch vanilla preset unexpectedly carried {forbidden}")
1574
+ if vanilla_argv != [str(backend)]:
1575
+ mark_fail(
1576
+ "agent-launch vanilla preset must project the bare backend with "
1577
+ f"nothing applied: {vanilla_argv!r}"
1578
+ )
1579
+
1495
1580
  # Cross-family default: each main routes every review route to the OPPOSITE
1496
1581
  # model family, with concrete tools/paths/bindings named in the contract.
1497
1582
  # The cross CODEX_HOME needs the reviewer wrappers (bin) and the same-family
@@ -1766,5 +1851,5 @@ then
1766
1851
  fail=1
1767
1852
  fi
1768
1853
 
1769
- [ "$fail" -eq 0 ] && echo "PARITY OK: mirrors, globals, guides, launch profile, bypass paths, role bindings, and wrapper defaults aligned"
1854
+ [ "$fail" -eq 0 ] && echo "PARITY OK: mirrors, globals, guides, domain manifest, assembler, launch profile, bypass paths, role bindings, and wrapper defaults aligned"
1770
1855
  exit "$fail"
@@ -102,6 +102,29 @@ deploy_glob() {
102
102
  done
103
103
  }
104
104
 
105
+ # ---- packaged (domain-selected) corpus deploy ----------------------------
106
+ # Active when --domains is passed or a prior selection exists in STATE_DIR.
107
+ # assemble.py owns the corpus surfaces (central tree, entry seeding, codex
108
+ # marker region, settings merge); the entry CLAUDE.md and AGENTS.md are NOT
109
+ # manifested — the entry is user-owned after seeding, AGENTS.md holds a
110
+ # personal region — so uninstall never deletes them.
111
+ packaged_mode() { [ "${DOMAINS_SET:-0}" = 1 ] || [ -f "$STATE_DIR/selection.json" ]; }
112
+
113
+ assemble_packaged() {
114
+ local args=(--claude-dir "$CLAUDE_DIR" --codex-dir "$CODEX_DIR" --state-dir "$STATE_DIR") rc=0
115
+ [ "${DOMAINS_SET:-0}" = 1 ] && args+=(--domains "$DOMAINS_ARG")
116
+ [ "$DRY_RUN" = 1 ] && args+=(--dry-run)
117
+ python3 "$REPO/scripts/assemble.py" "${args[@]}" || rc=$?
118
+ if [ "$rc" = 2 ]; then
119
+ log "packaged: entry file needs user action (import line missing); central content will not load until it is added"
120
+ elif [ "$rc" != 0 ]; then
121
+ return 1
122
+ fi
123
+ if [ "$DRY_RUN" != 1 ]; then
124
+ find "$CLAUDE_DIR/central" "$CODEX_DIR/guides" -type f 2>/dev/null >> "$MANIFEST"
125
+ fi
126
+ }
127
+
105
128
  add_zsh_hook() {
106
129
  if [ -f "$ZSHRC" ] && grep -qF "$HOOK_MARK" "$ZSHRC"; then
107
130
  info "zsh hook present $ZSHRC"
@@ -399,12 +422,17 @@ cmd_install() {
399
422
  : > "$MANIFEST"
400
423
  fi
401
424
  log "Deploying agent-bios from $REPO"
402
- deploy_file "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
403
- deploy_glob "$REPO/claude/guides" "*.md" "$CLAUDE_DIR/guides"
404
- deploy_glob "$REPO/claude/agents" "*.md" "$CLAUDE_DIR/agents"
405
- deploy_glob "$REPO/claude/hooks" "*.py" "$CLAUDE_DIR/hooks" "+x"
406
- deploy_file "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md"
407
- deploy_glob "$REPO/codex/guides" "*.md" "$CODEX_DIR/guides"
425
+ if packaged_mode; then
426
+ log "packaged mode: assembling selected domains (state: $STATE_DIR/selection.json)"
427
+ assemble_packaged || exit 1
428
+ else
429
+ deploy_file "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
430
+ deploy_glob "$REPO/claude/guides" "*.md" "$CLAUDE_DIR/guides"
431
+ deploy_glob "$REPO/claude/agents" "*.md" "$CLAUDE_DIR/agents"
432
+ deploy_glob "$REPO/claude/hooks" "*.py" "$CLAUDE_DIR/hooks" "+x"
433
+ deploy_file "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md"
434
+ deploy_glob "$REPO/codex/guides" "*.md" "$CODEX_DIR/guides"
435
+ fi
408
436
  deploy_glob "$REPO/codex/agents" "*.toml" "$CODEX_DIR/agents"
409
437
  codex_config_additions merge || exit 1
410
438
  deploy_file "$REPO/scripts/codex-run.sh" "$CODEX_DIR/bin/codex-run" "+x"
@@ -424,10 +452,10 @@ cmd_install() {
424
452
  || log "warning: venv provisioning failed (numbered-prompt fallback applies)"
425
453
  fi
426
454
  add_zsh_hook
427
- if python3 "$REPO/scripts/session-learning/learning-state.py" project --repo "$REPO" >/dev/null 2>&1; then
428
- info "learning-status projected"
455
+ if python3 "$REPO/scripts/session-distill/corpus-state.py" project --repo "$REPO" >/dev/null 2>&1; then
456
+ info "corpus-status projected"
429
457
  else
430
- log "note: learning-status projection unavailable (versions.json/ledger missing?)"
458
+ log "note: corpus-status projection unavailable (versions.json/ledger missing?)"
431
459
  fi
432
460
  log ""
433
461
  log "Verifying deployment..."
@@ -448,15 +476,32 @@ verify_present() { if [ -f "$1" ]; then return 0; else log "missing $1"; return
448
476
 
449
477
  cmd_verify() {
450
478
  local fail=0 gp
451
- verify_match "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" || fail=1
452
- verify_match "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md" || fail=1
479
+ if packaged_mode; then
480
+ # Packaged: corpus surfaces are selection-derived, not repo-identical.
481
+ # The entry file is user-owned — READ-check the import line, never rewrite.
482
+ python3 "$REPO/scripts/check-domains.py" >/dev/null 2>&1 && info "domains gate OK" || { log "domains gate FAILED"; fail=1; }
483
+ verify_present "$CLAUDE_DIR/central/bundle.md" || fail=1
484
+ if grep -qF '@central/bundle.md' "$CLAUDE_DIR/CLAUDE.md" 2>/dev/null; then
485
+ info "entry import line present"
486
+ else
487
+ log "entry $CLAUDE_DIR/CLAUDE.md lacks '@central/bundle.md' — central corpus is NOT loading"; fail=1
488
+ fi
489
+ if grep -qF 'agent-bios:central:start' "$CODEX_DIR/AGENTS.md" 2>/dev/null; then
490
+ info "codex central region present"
491
+ else
492
+ log "codex AGENTS.md central region missing"; fail=1
493
+ fi
494
+ else
495
+ verify_match "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" || fail=1
496
+ verify_match "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md" || fail=1
497
+ for gp in "$REPO"/claude/guides/*.md; do verify_present "$CLAUDE_DIR/guides/$(basename "$gp")" || fail=1; done
498
+ for gp in "$REPO"/claude/agents/*.md; do verify_present "$CLAUDE_DIR/agents/$(basename "$gp")" || fail=1; done
499
+ for gp in "$REPO"/claude/hooks/*.py; do verify_present "$CLAUDE_DIR/hooks/$(basename "$gp")" || fail=1; done
500
+ for gp in "$REPO"/codex/guides/*.md; do verify_present "$CODEX_DIR/guides/$(basename "$gp")" || fail=1; done
501
+ fi
453
502
  verify_match "$REPO/scripts/agent-launch.py" "$BIN_DIR/agent-launch" || fail=1
454
503
  verify_match "$REPO/config/agent-launch.toml" "$LAUNCH_DIR/profiles.toml" || fail=1
455
504
  verify_match "$REPO/shell/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh" || fail=1
456
- for gp in "$REPO"/claude/guides/*.md; do verify_present "$CLAUDE_DIR/guides/$(basename "$gp")" || fail=1; done
457
- for gp in "$REPO"/claude/agents/*.md; do verify_present "$CLAUDE_DIR/agents/$(basename "$gp")" || fail=1; done
458
- for gp in "$REPO"/claude/hooks/*.py; do verify_present "$CLAUDE_DIR/hooks/$(basename "$gp")" || fail=1; done
459
- for gp in "$REPO"/codex/guides/*.md; do verify_present "$CODEX_DIR/guides/$(basename "$gp")" || fail=1; done
460
505
  python3 - "$CODEX_DIR/agents" <<'PY' && info "agent TOMLs OK" || fail=1
461
506
  import sys, pathlib, tomllib
462
507
  root = pathlib.Path(sys.argv[1])
@@ -526,6 +571,49 @@ cmd_uninstall() {
526
571
  log "Kept: managed venv + backups under $STATE_DIR (rm -rf \"$STATE_DIR\" to purge)."
527
572
  }
528
573
 
574
+ cmd_onboard() {
575
+ log "agent-bios onboarding — pick your domain packages (core + infra always install)"
576
+ local names=() line i=1 choice sel="" n picks
577
+ while IFS= read -r line; do names+=("$line"); done \
578
+ < <(python3 -c "import json;print('\n'.join(sorted(json.load(open('$REPO/config/domains.json'))['domains'])))")
579
+ [ "${#names[@]}" -ge 1 ] || { log "no domains found in config/domains.json"; exit 1; }
580
+ if [ "$DOMAINS_SET" = 1 ]; then
581
+ # Non-interactive path, per this installer's input contract (stdin is
582
+ # detached at the top of the script): selection arrives as domain names.
583
+ [ "$DOMAINS_ARG" = none ] && DOMAINS_ARG=""
584
+ sel="$DOMAINS_ARG"
585
+ elif ( : </dev/tty ) 2>/dev/null; then
586
+ for line in "${names[@]}"; do info "$i) $line"; i=$((i+1)); done
587
+ printf 'Select by number, comma-separated (empty = core+infra only): '
588
+ read -r choice </dev/tty || choice=""
589
+ if [ -n "$choice" ]; then
590
+ # bash 3.2 + set -u: expanding an EMPTY array errors, so split only
591
+ # when there is input; empty input means core+infra only.
592
+ IFS=',' read -ra picks <<<"$choice"
593
+ for n in "${picks[@]}"; do
594
+ n="${n// /}"; [ -n "$n" ] || continue
595
+ case "$n" in (*[!0-9]*) log "invalid selection: $n"; exit 2 ;; esac
596
+ [ "$n" -ge 1 ] && [ "$n" -le "${#names[@]}" ] || { log "selection out of range: $n"; exit 2; }
597
+ sel="$sel${sel:+,}${names[$((n-1))]}"
598
+ done
599
+ fi
600
+ else
601
+ log "onboard needs a terminal or an explicit selection — run:"
602
+ log " agent-bios onboard --domains a,b (or --domains none for core+infra only)"
603
+ exit 2
604
+ fi
605
+ DOMAINS_ARG="$sel"; DOMAINS_SET=1
606
+ log "selection: ${sel:-<core+infra only>}"
607
+ cmd_install
608
+ log ""
609
+ log "Activation canary (proves the bundle loads in a live session)..."
610
+ if [ "$DRY_RUN" = 1 ]; then info "[dry-run] skip canary probe"; return; fi
611
+ bash "$REPO/scripts/canary.sh" || {
612
+ log "ONBOARDING INCOMPLETE: the bundle is installed but not loading — fix the cause above and re-run: agent-bios verify"
613
+ exit 1
614
+ }
615
+ }
616
+
529
617
  cmd_status() {
530
618
  local version p
531
619
  if [ -d "$REPO/.git" ]; then
@@ -561,6 +649,7 @@ usage() {
561
649
  agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
562
650
 
563
651
  agent-bios install deploy into this environment (backs up + verifies)
652
+ agent-bios onboard interactive domain selection + packaged install + activation canary
564
653
  agent-bios verify check the deployed state matches the source
565
654
  agent-bios status show what is installed and where
566
655
  agent-bios update git pull + reinstall (clone), or print the npm update line
@@ -568,6 +657,13 @@ agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
568
657
  agent-bios help
569
658
 
570
659
  Flags: --dry-run print actions without changing anything
660
+ --domains a,b packaged mode (install/onboard): assemble ONLY the named
661
+ domain packages (plus core+infra) instead of the full
662
+ corpus; with onboard, 'none' means core+infra only. The
663
+ selection persists in the state dir, so later
664
+ installs/updates stay packaged until the selection file
665
+ is removed. Default (no flag, no saved selection) keeps
666
+ today's full-corpus deploy.
571
667
  --with a,b also install the named optional dependencies (install only).
572
668
  Without it, install offers each missing one when the terminal
573
669
  is interactive, and otherwise just prints its install line.
@@ -581,11 +677,15 @@ EOF
581
677
  CMD="${1:-help}"
582
678
  if [ $# -gt 0 ]; then shift; fi
583
679
  WITH=""
680
+ DOMAINS_ARG=""
681
+ DOMAINS_SET=0
584
682
  while [ $# -gt 0 ]; do
585
683
  case "$1" in
586
684
  --dry-run) DRY_RUN=1 ;;
587
685
  --with) shift; WITH="${1:-}"; [ -n "$WITH" ] || { log "--with needs a comma-separated capability list"; exit 2; } ;;
588
686
  --with=*) WITH="${1#--with=}"; [ -n "$WITH" ] || { log "--with needs a comma-separated capability list"; exit 2; } ;;
687
+ --domains) shift; DOMAINS_ARG="${1:-}"; DOMAINS_SET=1; [ -n "$DOMAINS_ARG" ] || { log "--domains needs a comma-separated domain list (use onboard for core-only)"; exit 2; } ;;
688
+ --domains=*) DOMAINS_ARG="${1#--domains=}"; DOMAINS_SET=1; [ -n "$DOMAINS_ARG" ] || { log "--domains needs a comma-separated domain list (use onboard for core-only)"; exit 2; } ;;
589
689
  *) log "unknown flag: $1"; exit 2 ;;
590
690
  esac
591
691
  shift
@@ -593,6 +693,7 @@ done
593
693
 
594
694
  case "$CMD" in
595
695
  install) cmd_install ;;
696
+ onboard) cmd_onboard ;;
596
697
  update) cmd_update ;;
597
698
  uninstall) cmd_uninstall ;;
598
699
  verify) if cmd_verify; then log "VERIFY OK"; else log "VERIFY FAILED"; exit 1; fi ;;