Bug Description
_check_homebrew_update_available() in src/basic_memory/cli/auto_update.py decides "outdated or not" purely from brew outdated's stdout and discards result.returncode entirely. Every failure mode of brew outdated produces the same shape — non-zero exit, empty stdout, message on stderr — which the function reads as "not outdated". The caller then reports AutoUpdateStatus.UP_TO_DATE and prints:
Basic Memory is up to date (0.21.5).
A hard failure to determine the latest version is reported to the user as success. For an update checker this is the worst failure mode available: it silently pins Homebrew users to whatever version they happen to have, and the tool actively tells them there is nothing to do.
The code
src/basic_memory/cli/auto_update.py:
def _check_homebrew_update_available(silent: bool) -> tuple[bool, str | None]:
"""Check whether Homebrew reports an outdated basic-memory formula."""
result = _run_subprocess(
["brew", "outdated", "--quiet", PACKAGE_NAME],
timeout_seconds=BREW_OUTDATED_TIMEOUT_SECONDS,
silent=silent,
capture_output=True,
)
# Trigger: brew outdated exits 1 when the formula IS outdated (with name on stdout).
# Why: non-zero exit here means "outdated", not "error".
# Outcome: check stdout for the package name to determine outdated status.
stdout = (result.stdout or "").strip()
is_outdated = PACKAGE_NAME in stdout
return is_outdated, None
The comment is correct that exit 1 can mean "outdated" — but it is used to justify ignoring the return code altogether, so the error case and the up-to-date case become indistinguishable. There are three real outcomes (up to date / outdated / could-not-determine) collapsed into two.
run_auto_update() then does:
if not update_available:
return AutoUpdateResult(status=AutoUpdateStatus.UP_TO_DATE, ...)
so "brew blew up" lands in the up-to-date branch.
Steps To Reproduce
Observed on macOS with a Homebrew install from the project's own tap (basicmachines-co/homebrew-basic-memory).
- Install via Homebrew. Installed version: 0.21.5 (Jun 1). The tap formula has pointed at v0.22.1 since tap commit dated 2026-06-13.
- Put
brew outdated into any failing state (see below).
- Run
bm update.
- Output:
Basic Memory is up to date (0.21.5). — for ~2.5 months, while an upgrade was available the whole time.
The specific trigger seen in the wild was newer Homebrew's third-party tap trust requirement:
$ brew outdated --quiet basic-memory
Error: Refusing to load formula basicmachines-co/basic-memory/basic-memory from untrusted tap basicmachines-co/basic-memory.
$ echo $?
1
exit 1, stdout empty, message on stderr. PACKAGE_NAME in stdout is False → "up to date".
The untrusted tap is only one trigger. Any brew outdated failure has the identical shape. Reproduced live on the same machine with a formula brew cannot resolve:
$ out=$(brew outdated --quiet basic-memory-nope 2>/tmp/e.txt); echo "exit=$?"; echo "stdout=[$out]"; echo "stderr=[$(cat /tmp/e.txt)]"
exit=1
stdout=[]
stderr=[Error: No available formula with the name "basic-memory-nope". ...]
The same shape is produced by: an untapped/removed tap, a stale or broken tap checkout, a network failure during brew update, brew not on PATH (FileNotFoundError), or the 60s timeout expiring. All of them currently print "up to date".
For contrast, the working case on that machine after the tap was trusted:
$ brew outdated --quiet basic-memory
basicmachines-co/basic-memory/basic-memory
$ echo $?
1
exit 1 with the name on stdout — the case the existing tests cover.
Expected Behavior
When the update check cannot be completed, say so. Never claim the user is current based on a failed check. Surface brew's stderr, and/or fall back to a check that can actually answer the question.
Actual Behavior
Basic Memory is up to date (<installed version>).
Secondary defect on the same path
_check_homebrew_update_available() returns latest_version=None unconditionally. So even when it correctly detects an outdated formula, the message is:
Update available (latest: unknown). Run `brew upgrade basic-memory`.
Scope
- Homebrew installs only. The uv-tool path is unaffected: it goes through
_check_pypi_update_available(), which fetches the PyPI version and does a real Version comparison, raising on failure so the error reaches the FAILED branch. Confirmed empirically — a second machine on a uv-tool install tracked 0.22.1 correctly while the Homebrew machine sat on 0.21.5 reporting itself up to date.
- Affects both
bm update and the periodic check in maybe_run_periodic_auto_update().
Environment
- OS: macOS 15 (darwin 25.5.0), Apple Silicon
- Basic Memory version: 0.21.5 (Homebrew), diagnosis verified against current
main
- Installation method: Homebrew,
basicmachines-co/basic-memory tap
Possible Solution
Distinguish three outcomes instead of two:
returncode == 0 and stdout empty → up to date
PACKAGE_NAME in stdout → outdated
- anything else (non-zero exit with empty stdout, stderr carrying an error, timeout,
FileNotFoundError for brew) → unknown, not "up to date"
For the unknown case, falling back to _check_pypi_update_available() gives a real answer and a real latest_version, fixing the "latest: unknown" nit — as long as the remediation hint still says brew upgrade basic-memory for Homebrew installs. If the fallback is also unavailable, report FAILED with brew's stderr rather than a false all-clear.
Happy to send a PR.
Bug Description
_check_homebrew_update_available()insrc/basic_memory/cli/auto_update.pydecides "outdated or not" purely frombrew outdated's stdout and discardsresult.returncodeentirely. Every failure mode ofbrew outdatedproduces the same shape — non-zero exit, empty stdout, message on stderr — which the function reads as "not outdated". The caller then reportsAutoUpdateStatus.UP_TO_DATEand prints:A hard failure to determine the latest version is reported to the user as success. For an update checker this is the worst failure mode available: it silently pins Homebrew users to whatever version they happen to have, and the tool actively tells them there is nothing to do.
The code
src/basic_memory/cli/auto_update.py:The comment is correct that exit 1 can mean "outdated" — but it is used to justify ignoring the return code altogether, so the error case and the up-to-date case become indistinguishable. There are three real outcomes (up to date / outdated / could-not-determine) collapsed into two.
run_auto_update()then does:so "brew blew up" lands in the up-to-date branch.
Steps To Reproduce
Observed on macOS with a Homebrew install from the project's own tap (
basicmachines-co/homebrew-basic-memory).brew outdatedinto any failing state (see below).bm update.Basic Memory is up to date (0.21.5).— for ~2.5 months, while an upgrade was available the whole time.The specific trigger seen in the wild was newer Homebrew's third-party tap trust requirement:
exit 1, stdout empty, message on stderr.
PACKAGE_NAME in stdoutisFalse→ "up to date".The untrusted tap is only one trigger. Any
brew outdatedfailure has the identical shape. Reproduced live on the same machine with a formula brew cannot resolve:The same shape is produced by: an untapped/removed tap, a stale or broken tap checkout, a network failure during
brew update, brew not onPATH(FileNotFoundError), or the 60s timeout expiring. All of them currently print "up to date".For contrast, the working case on that machine after the tap was trusted:
exit 1 with the name on stdout — the case the existing tests cover.
Expected Behavior
When the update check cannot be completed, say so. Never claim the user is current based on a failed check. Surface brew's stderr, and/or fall back to a check that can actually answer the question.
Actual Behavior
Basic Memory is up to date (<installed version>).Secondary defect on the same path
_check_homebrew_update_available()returnslatest_version=Noneunconditionally. So even when it correctly detects an outdated formula, the message is:Scope
_check_pypi_update_available(), which fetches the PyPI version and does a realVersioncomparison, raising on failure so the error reaches theFAILEDbranch. Confirmed empirically — a second machine on a uv-tool install tracked 0.22.1 correctly while the Homebrew machine sat on 0.21.5 reporting itself up to date.bm updateand the periodic check inmaybe_run_periodic_auto_update().Environment
mainbasicmachines-co/basic-memorytapPossible Solution
Distinguish three outcomes instead of two:
returncode == 0and stdout empty → up to datePACKAGE_NAME in stdout→ outdatedFileNotFoundErrorfor brew) → unknown, not "up to date"For the unknown case, falling back to
_check_pypi_update_available()gives a real answer and a reallatest_version, fixing the "latest: unknown" nit — as long as the remediation hint still saysbrew upgrade basic-memoryfor Homebrew installs. If the fallback is also unavailable, reportFAILEDwith brew's stderr rather than a false all-clear.Happy to send a PR.