From a694d9a1f7b23fca4f91e6090918a07bed2efb78 Mon Sep 17 00:00:00 2001 From: Manu Lorente Date: Thu, 21 May 2026 14:04:01 -0600 Subject: [PATCH] fix(BUG-021+BUG-022): pre-export DOTFILES_REPO_DIR + race-free healthcheck probe (cross-OS) Two micro-fixes surfaced by user's post-merge setup run: ## BUG-021: doctor + diff-check warn/fail when profile hasn't reloaded BUG-020 (PR #86) added the DOTFILES_REPO_DIR export to profile.ps1 + .bashrc + .zshrc, but the shell already running setup has the OLD profile loaded in memory. doctor + diff-check (invoked by healthcheck sec 12) see the var unset until the user opens a fresh shell. Fix: pre-export DOTFILES_REPO_DIR in setup-{windows.ps1,linux.sh} RIGHT BEFORE invoking doctor/healthcheck. Same pre-export pattern as SCRIPTS_DIR / GEMINI_HOME / COPILOT_HOME / OPENCODE_HOME. ## BUG-022: healthcheck BUG-015 probe itself races via `break; }; done` The probe added by BUG-015 (PR #81) to detect upstream hook resolution failures uses the SAME `{ printf; ls; printf; } | while ... break` cascade pattern that BUG-017 patched in the upstream hooks themselves. The probe hits the EPIPE race when invoked from Windows pwsh-spawned bash (Git Bash subprocess sandbox), reporting false-positive FAIL even when the install is healthy. Fix: apply the same Option A `done | head -n1` to the probe in both healthcheck.sh and healthcheck.ps1. The probe now drains the upstream pipe fully, then head -n1 takes the first match. No EPIPE. Note: on Linux, SIGPIPE is silent so the race rarely fires, but cross-OS parity with the Windows fix matters for consistency. Combined diff: 27 LOC (14 setup-windows.ps1 + 4 setup-linux.sh + 7 healthcheck.ps1 + 9 healthcheck.sh + small minus markers). Below spec-gate threshold (50 LOC). Skip SDD per "mechanical cross-OS mirror" rule. Empirical post-fix behavior: - doctor sec env-vars: 10/10 ok (DOTFILES_REPO_DIR no longer warn) - healthcheck sec 4 BUG-015: PASS reports actual resolved path (no spurious FAIL) - healthcheck sec 12 diff-check: exit 0 or 1 (real drift state), no more exit 2 "setup error" Cross-OS scope justified: setup-linux.sh had the same gap (export block at line 950-954); healthcheck.sh probe had the same race-prone pattern (line 201). Both fixed in this PR to keep cross-OS contract symmetric. --- scripts/healthcheck.ps1 | 7 ++++++- scripts/healthcheck.sh | 9 +++++++-- setup-linux.sh | 4 ++++ setup-windows.ps1 | 14 ++++++++++---- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/scripts/healthcheck.ps1 b/scripts/healthcheck.ps1 index 8aca08c..88fbffc 100644 --- a/scripts/healthcheck.ps1 +++ b/scripts/healthcheck.ps1 @@ -266,7 +266,12 @@ if (Test-Path -LiteralPath $marketplaceReal -PathType Container) { # UserPromptSubmit "printf: write error: Permission denied" symptom. $bashCmd = Get-Command bash -ErrorAction SilentlyContinue if ($bashCmd) { - $cmHookProbe = '_C="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"; _E="${PLUGIN_ROOT:-}"; _P=$({ [ -n "$_E" ] && printf ''%s\n'' "$_E"; ls -dt "$_C/plugins/cache/thedotmack/claude-mem"/[0-9]*/ 2>/dev/null; printf ''%s\n'' "$_C/plugins/marketplaces/thedotmack-claude-mem/plugin"; } | while IFS= read -r _R; do _R="${_R%/}"; [ -d "$_R/plugin/scripts" ] && _Q="$_R/plugin" || _Q="$_R"; [ -f "$_Q/scripts/bun-runner.js" ] && [ -f "$_Q/scripts/worker-service.cjs" ] && { printf ''%s\n'' "$_Q"; break; }; done); [ -n "$_P" ] && printf ''%s'' "$_P" || exit 1' + # BUG-022 (2026-05-21): the probe itself used the same `break; }; done` + # cascade pattern as the upstream hook, hitting the EPIPE race when + # called from setup. Apply the same `head -n1` fix (Option A from + # claude-mem#2607) so the probe is race-free and reports the actual + # state of the install rather than spurious failures. + $cmHookProbe = '_C="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"; _E="${PLUGIN_ROOT:-}"; _P=$({ [ -n "$_E" ] && printf ''%s\n'' "$_E"; ls -dt "$_C/plugins/cache/thedotmack/claude-mem"/[0-9]*/ 2>/dev/null; printf ''%s\n'' "$_C/plugins/marketplaces/thedotmack-claude-mem/plugin"; } | while IFS= read -r _R; do _R="${_R%/}"; [ -d "$_R/plugin/scripts" ] && _Q="$_R/plugin" || _Q="$_R"; [ -f "$_Q/scripts/bun-runner.js" ] && [ -f "$_Q/scripts/worker-service.cjs" ] && printf ''%s\n'' "$_Q"; done | head -n1); [ -n "$_P" ] && printf ''%s'' "$_P" || exit 1' $resolved = & bash -c $cmHookProbe 2>&1 if ($LASTEXITCODE -eq 0 -and $resolved) { Write-Pass "claude-mem hook path resolves to: $resolved (BUG-015)" diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh index 0e36c40..888c2dd 100755 --- a/scripts/healthcheck.sh +++ b/scripts/healthcheck.sh @@ -191,6 +191,11 @@ fi # user encounters the intermittent UserPromptSubmit fail. _cmhook_C="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" _cmhook_E="${PLUGIN_ROOT:-}" +# BUG-022 (2026-05-21): probe itself used the same `break; }; done` cascade +# as the upstream hooks (which BUG-017 patched away). Apply the same +# `head -n1` fix locally so the probe is race-free and reports actual state +# instead of spurious failures. On Linux SIGPIPE is silent so the race rarely +# fires here, but cross-OS parity with healthcheck.ps1 matters. _cmhook_P=$({ [ -n "$_cmhook_E" ] && printf '%s\n' "$_cmhook_E" ls -dt "$_cmhook_C/plugins/cache/thedotmack/claude-mem"/[0-9]*/ 2>/dev/null @@ -198,8 +203,8 @@ _cmhook_P=$({ } | while IFS= read -r _r; do _r="${_r%/}" [ -d "$_r/plugin/scripts" ] && _q="$_r/plugin" || _q="$_r" - [ -f "$_q/scripts/bun-runner.js" ] && [ -f "$_q/scripts/worker-service.cjs" ] && { printf '%s\n' "$_q"; break; } -done) + [ -f "$_q/scripts/bun-runner.js" ] && [ -f "$_q/scripts/worker-service.cjs" ] && printf '%s\n' "$_q" +done | head -n1) if [ -n "$_cmhook_P" ]; then pass "claude-mem hook path resolves to: $_cmhook_P (BUG-015)" else diff --git a/setup-linux.sh b/setup-linux.sh index f136593..2e868e3 100755 --- a/setup-linux.sh +++ b/setup-linux.sh @@ -952,6 +952,10 @@ export SCRIPTS_DIR="${SCRIPTS_DIR:-$DOTFILES_DIR/scripts}" export GEMINI_HOME="${GEMINI_HOME:-$HOME/.gemini}" export COPILOT_HOME="${COPILOT_HOME:-$HOME/.copilot}" export OPENCODE_HOME="${OPENCODE_HOME:-$HOME/.config/opencode}" +# BUG-021 (2026-05-21): pre-export DOTFILES_REPO_DIR so doctor + diff-check.sh +# (REFACTOR-003 sec 12) see it set even when the running shell's profile +# hasn't been re-evaluated post-deploy. Mirror of the setup-windows.ps1 fix. +export DOTFILES_REPO_DIR="${DOTFILES_REPO_DIR:-$HOME/Projects/dotfiles}" DOCTOR_SCRIPT="$DOTFILES_DIR/scripts/doctor.sh" if [ -x "$DOCTOR_SCRIPT" ] && command -v jq >/dev/null 2>&1; then diff --git a/setup-windows.ps1 b/setup-windows.ps1 index 9e7d533..77b473d 100644 --- a/setup-windows.ps1 +++ b/setup-windows.ps1 @@ -1164,10 +1164,16 @@ if ($existingTask -and ($existingTaskArgument -eq $expectedTaskArgument)) { # deployed profile entirely. Values must match the corresponding lines in # powershell/profile.ps1. PowerShell propagates parent $env: to child procs. -if (-not $env:SCRIPTS_DIR) { $env:SCRIPTS_DIR = "$env:DOTFILES_DIR\scripts" } -if (-not $env:GEMINI_HOME) { $env:GEMINI_HOME = "$env:USERPROFILE\.gemini" } -if (-not $env:COPILOT_HOME) { $env:COPILOT_HOME = "$env:USERPROFILE\.copilot" } -if (-not $env:OPENCODE_HOME) { $env:OPENCODE_HOME = "$env:USERPROFILE\.config\opencode" } +if (-not $env:SCRIPTS_DIR) { $env:SCRIPTS_DIR = "$env:DOTFILES_DIR\scripts" } +if (-not $env:GEMINI_HOME) { $env:GEMINI_HOME = "$env:USERPROFILE\.gemini" } +if (-not $env:COPILOT_HOME) { $env:COPILOT_HOME = "$env:USERPROFILE\.copilot" } +if (-not $env:OPENCODE_HOME) { $env:OPENCODE_HOME = "$env:USERPROFILE\.config\opencode" } +# BUG-021 (2026-05-21): added DOTFILES_REPO_DIR pre-export -- BUG-020 (PR #86) +# added the export to profile.ps1 but the running shell doesn't reload profile, +# so doctor + diff-check.ps1 (via healthcheck sec 12) still see the var unset +# until next shell restart. Mirror the same pre-export pattern as the other +# 4 vars to surface PASS immediately in the post-setup checks. +if (-not $env:DOTFILES_REPO_DIR) { $env:DOTFILES_REPO_DIR = "$env:USERPROFILE\Projects\dotfiles" } $doctorScript = "$ScriptsDir\doctor.ps1" if (Test-Path $doctorScript) {