diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index 11a0dea..cf7b816 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -24,6 +24,11 @@ _BASE_SETUP_CHECK_OK=() _BASE_SETUP_CHECK_MESSAGES=() _BASE_SETUP_CHECK_RECOVERIES=() _BASE_SETUP_CHECK_DEBUG_MESSAGES=() +_BASE_SETUP_PARSED_CHECK_NAME="" +_BASE_SETUP_PARSED_CHECK_OK="" +_BASE_SETUP_PARSED_CHECK_MESSAGE="" +_BASE_SETUP_PARSED_CHECK_RECOVERY="" +_BASE_SETUP_PARSED_CHECK_DEBUG_MESSAGE="" _BASE_SETUP_VENV_HEALTH_MESSAGE="" setup_refresh_cached_paths() { @@ -1000,94 +1005,257 @@ setup_add_check_result() { _BASE_SETUP_CHECK_DEBUG_MESSAGES+=("$debug_message") } -setup_collect_base_check_results() { - local brew_bin click_ok=false click_package - local missing=0 - local pyyaml_ok=false pyyaml_package - local refresh_brew_failure_mode="${1:-warn}" +setup_write_check_result_file() { + local debug_message="${6:-}" + local message="$4" + local name="$2" + local ok="$3" + local path="$1" + local recovery="${5:-}" - setup_clear_check_results - setup_require_macos - click_package="$(setup_click_package)" - pyyaml_package="$(setup_pyyaml_package)" - setup_ensure_cached_paths + { + printf 'name=%s\n' "$name" + printf 'ok=%s\n' "$ok" + printf 'message=%s\n' "$message" + printf 'recovery=%s\n' "$recovery" + printf 'debug=%s\n' "$debug_message" + } >"$path" +} - if brew_bin="$(setup_find_brew_bin)"; then +setup_parse_check_result_file() { + local line path="$1" + + [[ -f "$path" ]] || fatal_error "Base check probe did not produce result file '$path'." + + _BASE_SETUP_PARSED_CHECK_NAME="" + _BASE_SETUP_PARSED_CHECK_OK="" + _BASE_SETUP_PARSED_CHECK_MESSAGE="" + _BASE_SETUP_PARSED_CHECK_RECOVERY="" + _BASE_SETUP_PARSED_CHECK_DEBUG_MESSAGE="" + + while IFS= read -r line || [[ -n "$line" ]]; do + case "$line" in + name=*) + _BASE_SETUP_PARSED_CHECK_NAME="${line#name=}" + ;; + ok=*) + _BASE_SETUP_PARSED_CHECK_OK="${line#ok=}" + ;; + message=*) + _BASE_SETUP_PARSED_CHECK_MESSAGE="${line#message=}" + ;; + recovery=*) + _BASE_SETUP_PARSED_CHECK_RECOVERY="${line#recovery=}" + ;; + debug=*) + _BASE_SETUP_PARSED_CHECK_DEBUG_MESSAGE="${line#debug=}" + ;; + esac + done <"$path" + + [[ -n "$_BASE_SETUP_PARSED_CHECK_NAME" ]] || fatal_error "Base check probe result '$path' is missing a name." + case "$_BASE_SETUP_PARSED_CHECK_OK" in + true|false) + ;; + *) + fatal_error "Base check probe result '$path' has invalid ok value '$_BASE_SETUP_PARSED_CHECK_OK'." + ;; + esac + [[ -n "$_BASE_SETUP_PARSED_CHECK_MESSAGE" ]] || fatal_error "Base check probe result '$path' is missing a message." +} + +setup_add_parsed_check_result() { + setup_add_check_result \ + "$_BASE_SETUP_PARSED_CHECK_NAME" \ + "$_BASE_SETUP_PARSED_CHECK_OK" \ + "$_BASE_SETUP_PARSED_CHECK_MESSAGE" \ + "$_BASE_SETUP_PARSED_CHECK_RECOVERY" \ + "$_BASE_SETUP_PARSED_CHECK_DEBUG_MESSAGE" +} + +setup_read_check_result_file() { + setup_parse_check_result_file "$1" + setup_add_parsed_check_result + [[ "$_BASE_SETUP_PARSED_CHECK_OK" == true ]] +} + +setup_read_homebrew_check_result_file() { + local path="$1" + local refresh_brew_failure_mode="$2" + + setup_parse_check_result_file "$path" + [[ "$_BASE_SETUP_PARSED_CHECK_NAME" == homebrew ]] || + fatal_error "Base check probe result '$path' contains unexpected name '$_BASE_SETUP_PARSED_CHECK_NAME'." + + if [[ "$_BASE_SETUP_PARSED_CHECK_OK" == true ]]; then if setup_refresh_brew_path; then - setup_add_check_result "homebrew" true "Homebrew is installed." "" "Resolved Homebrew binary: $brew_bin" - else - if [[ "$refresh_brew_failure_mode" == fatal ]]; then - fatal_error "Homebrew is installed, but its bin directory could not be added to PATH. $(setup_recovery_brew_path)" - fi - setup_add_check_result \ - "homebrew" \ - false \ - "Homebrew is installed, but its bin directory could not be added to PATH." \ - "$(setup_recovery_brew_path)" - missing=1 + setup_add_parsed_check_result + return 0 fi + if [[ "$refresh_brew_failure_mode" == fatal ]]; then + fatal_error "Homebrew is installed, but its bin directory could not be added to PATH. $(setup_recovery_brew_path)" + fi + setup_add_check_result \ + "homebrew" \ + false \ + "Homebrew is installed, but its bin directory could not be added to PATH." \ + "$(setup_recovery_brew_path)" + return 1 + fi + + setup_add_parsed_check_result + return 1 +} + +setup_write_homebrew_check_probe() { + local brew_bin + local result_file="$1" + + if brew_bin="$(setup_find_brew_bin)"; then + setup_write_check_result_file \ + "$result_file" \ + "homebrew" \ + true \ + "Homebrew is installed." \ + "" \ + "Resolved Homebrew binary: $brew_bin" else - setup_add_check_result "homebrew" false "Homebrew is not installed." "$(setup_recovery_homebrew)" - missing=1 + setup_write_check_result_file \ + "$result_file" \ + "homebrew" \ + false \ + "Homebrew is not installed." \ + "$(setup_recovery_homebrew)" fi +} + +setup_write_xcode_check_probe() { + local result_file="$1" if setup_xcode_tools_installed; then - setup_add_check_result "xcode_command_line_tools" true "Xcode Command Line Tools are installed." + setup_write_check_result_file \ + "$result_file" \ + "xcode_command_line_tools" \ + true \ + "Xcode Command Line Tools are installed." else - setup_add_check_result \ + setup_write_check_result_file \ + "$result_file" \ "xcode_command_line_tools" \ false \ "Xcode Command Line Tools are not installed." \ "$(setup_recovery_xcode_tools)" - missing=1 fi +} + +setup_write_python_check_probe() { + local result_file="$1" if setup_python_installed; then - setup_add_check_result \ + setup_write_check_result_file \ + "$result_file" \ "python" \ true \ "Python formula '$(setup_python_formula)' is installed via Homebrew." else - setup_add_check_result \ + setup_write_check_result_file \ + "$result_file" \ "python" \ false \ "Python formula '$(setup_python_formula)' is not installed via Homebrew." \ "$(setup_recovery_python)" - missing=1 fi +} + +setup_write_virtualenv_check_probe() { + local result_file="$1" if setup_virtualenv_healthy; then - setup_add_check_result "base_virtualenv" true "$_BASE_SETUP_VENV_HEALTH_MESSAGE" + setup_write_check_result_file \ + "$result_file" \ + "base_virtualenv" \ + true \ + "$_BASE_SETUP_VENV_HEALTH_MESSAGE" else - setup_add_check_result \ + setup_write_check_result_file \ + "$result_file" \ "base_virtualenv" \ false \ "$_BASE_SETUP_VENV_HEALTH_MESSAGE" \ "$(setup_recovery_venv)" - missing=1 fi +} - if setup_base_python_package_installed "$pyyaml_package"; then - pyyaml_ok=true - else - missing=1 - fi - setup_add_check_result \ - "pyyaml" \ - "$pyyaml_ok" \ - "$(setup_base_python_package_check_message "$pyyaml_package" "$pyyaml_ok")" \ - "$(setup_recovery_base_python_package)" +setup_write_python_package_check_probe() { + local ok=false + local package="$3" + local result_file="$1" + local result_name="$2" - if setup_base_python_package_installed "$click_package"; then - click_ok=true - else - missing=1 + if setup_base_python_package_installed "$package"; then + ok=true fi - setup_add_check_result \ - "click" \ - "$click_ok" \ - "$(setup_base_python_package_check_message "$click_package" "$click_ok")" \ + + setup_write_check_result_file \ + "$result_file" \ + "$result_name" \ + "$ok" \ + "$(setup_base_python_package_check_message "$package" "$ok")" \ "$(setup_recovery_base_python_package)" +} + +setup_wait_for_base_check_probes() { + local failed=0 + local pid + + for pid in "$@"; do + wait "$pid" || failed=1 + done + + return "$failed" +} + +setup_collect_base_check_results() { + local click_package + local missing=0 + local probe_pids=() + local pyyaml_package + local refresh_brew_failure_mode="${1:-warn}" + local tmpdir + + setup_clear_check_results + setup_require_macos + click_package="$(setup_click_package)" + pyyaml_package="$(setup_pyyaml_package)" + setup_ensure_cached_paths + + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/base-check.XXXXXX")" || + fatal_error "Unable to create temporary directory for Base check probes." + + setup_write_homebrew_check_probe "$tmpdir/homebrew" & + probe_pids+=("$!") + setup_write_xcode_check_probe "$tmpdir/xcode" & + probe_pids+=("$!") + setup_write_python_check_probe "$tmpdir/python" & + probe_pids+=("$!") + setup_write_virtualenv_check_probe "$tmpdir/base_virtualenv" & + probe_pids+=("$!") + setup_write_python_package_check_probe "$tmpdir/pyyaml" "pyyaml" "$pyyaml_package" & + probe_pids+=("$!") + setup_write_python_package_check_probe "$tmpdir/click" "click" "$click_package" & + probe_pids+=("$!") + + setup_wait_for_base_check_probes "${probe_pids[@]}" || + fatal_error "One or more Base check probes failed before writing results." + + setup_read_homebrew_check_result_file "$tmpdir/homebrew" "$refresh_brew_failure_mode" || missing=1 + setup_read_check_result_file "$tmpdir/xcode" || missing=1 + setup_read_check_result_file "$tmpdir/python" || missing=1 + setup_read_check_result_file "$tmpdir/base_virtualenv" || missing=1 + setup_read_check_result_file "$tmpdir/pyyaml" || missing=1 + setup_read_check_result_file "$tmpdir/click" || missing=1 + + rm -rf "$tmpdir" return "$missing" } diff --git a/cli/bash/commands/basectl/tests/check.bats b/cli/bash/commands/basectl/tests/check.bats index 7ae65cb..6f24351 100644 --- a/cli/bash/commands/basectl/tests/check.bats +++ b/cli/bash/commands/basectl/tests/check.bats @@ -42,6 +42,38 @@ load ./setup_helpers.bash [ "$(grep -c '^click$' "$TEST_STATE_DIR/pip-show.log")" -eq 1 ] } +@test "basectl check preserves text order while base probes overlap" { + local click_line homebrew_line python_line pyyaml_line venv_line xcode_line + local venv_dir="$TEST_HOME/.base.d/base/.venv" + + create_brew_stub + create_xcode_stubs + touch "$TEST_STATE_DIR/xcode-installed" + mkdir -p "$TEST_TMPDIR/CommandLineTools" + touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/pyyaml-installed" + touch "$TEST_STATE_DIR/click-installed" + create_base_venv_stub "$venv_dir" + + run_base_command \ + BASE_SETUP_TEST_XCODE_WAIT_FOR_PIP_SHOW=true \ + BASE_SETUP_TEST_XCODE_PIP_WAIT_SECONDS=2 \ + check + + [ "$status" -eq 0 ] + homebrew_line="$(printf '%s\n' "$output" | grep -n "Homebrew is installed." | head -n 1 | cut -d: -f1)" + xcode_line="$(printf '%s\n' "$output" | grep -n "Xcode Command Line Tools are installed." | head -n 1 | cut -d: -f1)" + python_line="$(printf '%s\n' "$output" | grep -n "Python formula 'python@3.13' is installed via Homebrew." | head -n 1 | cut -d: -f1)" + venv_line="$(printf '%s\n' "$output" | grep -n "Virtual environment is healthy at '$venv_dir'." | head -n 1 | cut -d: -f1)" + pyyaml_line="$(printf '%s\n' "$output" | grep -n "Python package 'PyYAML' is installed in the Base virtual environment." | head -n 1 | cut -d: -f1)" + click_line="$(printf '%s\n' "$output" | grep -n "Python package 'click' is installed in the Base virtual environment." | head -n 1 | cut -d: -f1)" + [ "$homebrew_line" -lt "$xcode_line" ] + [ "$xcode_line" -lt "$python_line" ] + [ "$python_line" -lt "$venv_line" ] + [ "$venv_line" -lt "$pyyaml_line" ] + [ "$pyyaml_line" -lt "$click_line" ] +} + @test "basectl check ignores inherited setup dry-run and recreate state" { local venv_dir="$TEST_HOME/.base.d/base/.venv" @@ -233,6 +265,49 @@ load ./setup_helpers.bash [ "${stderr:-}" = "" ] } +@test "basectl check --format json preserves finding order while base probes overlap" { + local click_line homebrew_line python_line pyyaml_line venv_line xcode_line + local venv_dir="$TEST_HOME/.base.d/base/.venv" + + create_brew_stub + create_xcode_stubs + touch "$TEST_STATE_DIR/xcode-installed" + mkdir -p "$TEST_TMPDIR/CommandLineTools" + touch "$TEST_STATE_DIR/python-installed" + touch "$TEST_STATE_DIR/pyyaml-installed" + touch "$TEST_STATE_DIR/click-installed" + create_base_venv_stub "$venv_dir" + + run --separate-stderr env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:$TEST_BASH_BIN_DIR:/usr/bin:/bin:/usr/sbin:/sbin" \ + OSTYPE="darwin24" \ + BASE_SETUP_BREW_BIN="$TEST_MOCKBIN/brew" \ + BASE_SETUP_TEST_STATE_DIR="$TEST_STATE_DIR" \ + BASE_SETUP_TEST_MOCKBIN="$TEST_MOCKBIN" \ + BASE_SETUP_TEST_PYTHON_PREFIX="$TEST_TMPDIR/python-prefix" \ + BASE_SETUP_TEST_XCODE_WAIT_FOR_PIP_SHOW=true \ + BASE_SETUP_TEST_XCODE_PIP_WAIT_SECONDS=2 \ + BASE_SETUP_XCODE_COMMAND_LINE_TOOLS_DIR="$TEST_TMPDIR/CommandLineTools" \ + "$BASE_REPO_ROOT/bin/basectl" check --format json + + [ "$status" -eq 0 ] + [[ "$output" == *'"schema_version": 1'* ]] + [[ "$output" == *'"status": "ok"'* ]] + homebrew_line="$(printf '%s\n' "$output" | grep -n '"id":"BASE-D001","status":"ok","name":"homebrew"' | cut -d: -f1)" + xcode_line="$(printf '%s\n' "$output" | grep -n '"id":"BASE-D002","status":"ok","name":"xcode_command_line_tools"' | cut -d: -f1)" + python_line="$(printf '%s\n' "$output" | grep -n '"id":"BASE-D003","status":"ok","name":"python"' | cut -d: -f1)" + venv_line="$(printf '%s\n' "$output" | grep -n '"id":"BASE-D004","status":"ok","name":"base_virtualenv"' | cut -d: -f1)" + pyyaml_line="$(printf '%s\n' "$output" | grep -n '"id":"BASE-D005","status":"ok","name":"pyyaml"' | cut -d: -f1)" + click_line="$(printf '%s\n' "$output" | grep -n '"id":"BASE-D006","status":"ok","name":"click"' | cut -d: -f1)" + [ "$homebrew_line" -lt "$xcode_line" ] + [ "$xcode_line" -lt "$python_line" ] + [ "$python_line" -lt "$venv_line" ] + [ "$venv_line" -lt "$pyyaml_line" ] + [ "$pyyaml_line" -lt "$click_line" ] + [ "${stderr:-}" = "" ] +} + @test "basectl check --format json reports broken Base virtualenv integrity" { local missing_home="$TEST_TMPDIR/missing-python-home" local venv_dir="$TEST_HOME/.base.d/base/.venv" diff --git a/cli/bash/commands/basectl/tests/setup_helpers.bash b/cli/bash/commands/basectl/tests/setup_helpers.bash index e21e3db..da99665 100644 --- a/cli/bash/commands/basectl/tests/setup_helpers.bash +++ b/cli/bash/commands/basectl/tests/setup_helpers.bash @@ -24,6 +24,15 @@ installed_file="$state_dir/xcode-installed" case "${1:-}" in -p) if [[ -f "$installed_file" ]]; then + if [[ "${BASE_SETUP_TEST_XCODE_WAIT_FOR_PIP_SHOW:-}" == true ]]; then + waited=0 + wait_seconds="${BASE_SETUP_TEST_XCODE_PIP_WAIT_SECONDS:-5}" + while [[ ! -s "$state_dir/pip-show.log" && "$waited" -lt "$wait_seconds" ]]; do + sleep 1 + waited=$((waited + 1)) + done + [[ -s "$state_dir/pip-show.log" ]] || exit 1 + fi mkdir -p "$tools_dir/usr/bin" touch "$tools_dir/usr/bin/clang" printf '%s\n' "$tools_dir" diff --git a/docs/superpowers/plans/2026-06-09-parallel-base-check-probes.md b/docs/superpowers/plans/2026-06-09-parallel-base-check-probes.md new file mode 100644 index 0000000..d67a73b --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-parallel-base-check-probes.md @@ -0,0 +1,146 @@ +# Parallel Base Check Probes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Run independent Base environment probes concurrently for +`basectl check` while preserving deterministic text and JSON output. + +**Architecture:** Keep the existing result arrays and renderers. Add +result-file helpers plus background probe helpers inside +`setup_common.sh`; the collector waits for probes, reads files in the old order, +and then appends to the existing arrays. + +**Tech Stack:** Bash 4-compatible shell functions, existing BATS test helpers, +and the current Base check/doctor rendering functions. + +--- + +## File Structure + +- Modify `cli/bash/commands/basectl/subcommands/setup_common.sh`: result file + writer/parser, background Base probe helpers, and parallel + `setup_collect_base_check_results()`. +- Modify `cli/bash/commands/basectl/tests/setup_helpers.bash`: test-only Xcode + wait hook used to prove probes overlap. +- Modify `cli/bash/commands/basectl/tests/check.bats`: RED coverage for + deterministic ordered text and JSON output while probes overlap. +- Add `docs/superpowers/specs/2026-06-09-parallel-base-check-probes-design.md`: + design record. +- Add `docs/superpowers/plans/2026-06-09-parallel-base-check-probes.md`: this + plan. + +## Task 1: RED Text Ordering and Parallelism Test + +**Files:** +- Modify: `cli/bash/commands/basectl/tests/setup_helpers.bash` +- Modify: `cli/bash/commands/basectl/tests/check.bats` + +- [ ] Add a test-only wait hook to `create_xcode_stubs()` so `xcode-select -p` + waits for the package probe marker when + `BASE_SETUP_TEST_XCODE_WAIT_FOR_PIP_SHOW=true`. +- [ ] Add a `basectl check` text-mode test that enables the hook, sets all + dependencies as installed, and expects status 0. +- [ ] Assert the output line order remains Homebrew, Xcode, Python, venv, + PyYAML, click. +- [ ] Run the focused BATS file and verify the new test fails on the current + serial collector. + +Command: + +```bash +bats cli/bash/commands/basectl/tests/check.bats +``` + +Expected RED result: the new text test fails because serial collection checks +Xcode before any package probe can create `pip-show.log`. + +## Task 2: RED JSON Ordering and Parallelism Test + +**Files:** +- Modify: `cli/bash/commands/basectl/tests/check.bats` + +- [ ] Add a `basectl check --format json` test using the same Xcode wait hook. +- [ ] Assert status 0 and empty stderr. +- [ ] Assert JSON finding order remains BASE-D001 through BASE-D006. +- [ ] Run the focused BATS file and verify the new JSON test fails on the + current serial collector. + +Command: + +```bash +bats cli/bash/commands/basectl/tests/check.bats +``` + +Expected RED result: the JSON test fails for the same serial-order reason. + +## Task 3: Result File Helpers + +**Files:** +- Modify: `cli/bash/commands/basectl/subcommands/setup_common.sh` + +- [ ] Add `setup_write_check_result_file()` that writes `name`, `ok`, + `message`, `recovery`, and `debug` lines to a probe result file. +- [ ] Add `setup_parse_check_result_file()` that loads those fields into + internal scratch variables. +- [ ] Add `setup_add_parsed_check_result()` that appends parsed fields to the + existing `_BASE_SETUP_CHECK_*` arrays. +- [ ] Add a fatal error when a probe result file is missing or malformed. + +## Task 4: Probe Helpers + +**Files:** +- Modify: `cli/bash/commands/basectl/subcommands/setup_common.sh` + +- [ ] Add one result-producing helper for each Base environment check: + Homebrew, Xcode, Python formula, Base venv, PyYAML, and click. +- [ ] Make each probe write exactly one result file and return 0 for expected + missing-dependency outcomes. +- [ ] Keep `setup_refresh_brew_path` out of the Homebrew background probe. + +## Task 5: Parallel Collector + +**Files:** +- Modify: `cli/bash/commands/basectl/subcommands/setup_common.sh` + +- [ ] Replace the serial body of `setup_collect_base_check_results()` with a + temporary directory, six background probes, `wait`, ordered result loading, + and cleanup. +- [ ] Read the Homebrew result first and run `setup_refresh_brew_path` in the + parent when Homebrew exists. +- [ ] Preserve fatal refresh behavior for text `basectl check`. +- [ ] Preserve warning-style refresh behavior for JSON/doctor callers. +- [ ] Preserve existing finding names, messages, recovery text, debug messages, + and return status. + +## Task 6: Validation + +- [ ] Run focused BATS coverage: + +```bash +bats cli/bash/commands/basectl/tests/check.bats +``` + +- [ ] Run full Base validation: + +```bash +env -u BASE_HOME ./bin/base-test +``` + +- [ ] Run whitespace validation: + +```bash +git diff --check +``` + +## Task 7: Publish + +- [ ] Commit the implementation. +- [ ] Push `enhancement/510-20260609-parallel-base-check-probes`. +- [ ] Open a PR closing #510. +- [ ] Watch CI. +- [ ] Merge when checks are green. +- [ ] Sync local `master`. +- [ ] Remove the #510 worktree and local branch. diff --git a/docs/superpowers/specs/2026-06-09-parallel-base-check-probes-design.md b/docs/superpowers/specs/2026-06-09-parallel-base-check-probes-design.md new file mode 100644 index 0000000..3d522aa --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-parallel-base-check-probes-design.md @@ -0,0 +1,115 @@ +# Parallel Base Check Probes Design + +Issue: #510 + +## Goal + +Make the Base environment portion of `basectl check` faster by running +independent probes in parallel while preserving the existing deterministic text +and JSON output order. + +## Scope + +This change is limited to the shared Base check collector in +`cli/bash/commands/basectl/subcommands/setup_common.sh`. + +The affected paths are: + +- `basectl check` +- `basectl check --format json` +- `basectl doctor --format json`, because it already reuses the same collector + +The text `basectl doctor` path prints findings directly through separate helper +functions. It is intentionally left out of this slice so #510 can stay focused +on high-frequency `check` behavior and avoid a broader doctor refactor. + +Prerequisite profile checks and project artifact checks remain serial after the +Base environment collector has finished. + +## Current Behavior + +`setup_collect_base_check_results()` clears the result arrays, runs each Base +probe serially, and appends findings directly to the arrays consumed by text and +JSON renderers. + +The serial order is: + +1. Homebrew presence and path discovery +2. Xcode Command Line Tools +3. Homebrew Python formula +4. Base virtual environment integrity +5. PyYAML in the Base virtual environment +6. click in the Base virtual environment + +The output order is good, but the probe order is slower than necessary. + +## Design + +Add small result-producing probe helpers that write one structured result file +per check: + +```text +name=homebrew +ok=true +message=Homebrew is installed. +recovery= +debug=Resolved Homebrew binary: /opt/homebrew/bin/brew +``` + +The parent collector will: + +1. create a temporary directory +2. start one background probe per independent Base check +3. wait for all probes to finish +4. read result files in the existing display order +5. append findings to the existing `_BASE_SETUP_CHECK_*` arrays +6. remove the temporary directory + +The renderers do not change. They continue to print from the result arrays in +array order, which keeps text and JSON deterministic even though probes complete +out of order. + +## Homebrew PATH Boundary + +`setup_refresh_brew_path` mutates the parent shell's `PATH`, so it must not run +inside a background probe. The Homebrew probe will only discover whether a brew +binary exists and record the debug path. + +After all probes complete, the parent reads the Homebrew result first. If +Homebrew exists, the parent calls `setup_refresh_brew_path` before appending the +Homebrew finding. If refresh fails: + +- `basectl check` text mode keeps the existing fatal behavior when the collector + is called with `fatal` +- JSON/doctor collector callers keep the existing warning-style result + +## Probe Independence + +The first implementation parallelizes only probes that can produce independent +read-only observations: + +- Homebrew discovery +- Xcode Command Line Tools presence +- Homebrew Python formula presence +- Base virtual environment health +- PyYAML package presence +- click package presence + +The package probes may report missing packages when the venv is absent, but +they must not emit stderr or abort the collector. This preserves the existing +behavior where a missing venv and missing packages are both reported as +findings. + +## Tests + +Add BATS coverage in `cli/bash/commands/basectl/tests/check.bats` that proves: + +- text output remains in the current order while probes are allowed to overlap +- JSON output remains in the current order while probes are allowed to overlap +- missing Homebrew still allows the other Base findings to be reported +- missing venv/package checks still produce structured output without stderr + +Use a test-only Xcode stub mode that waits for a pip-show marker. Serial probe +collection fails that scenario because Xcode runs before package checks; +parallel collection passes because package probes can create the marker while +the Xcode probe is waiting.