Skip to content

fix(hud): sync hooks/lib in installers (#1490) — v5.6.2 hotfix#1490

Merged
JeremyDev87 merged 10 commits into
masterfrom
fix/hud-installer-lib-sync
Apr 11, 2026
Merged

fix(hud): sync hooks/lib in installers (#1490) — v5.6.2 hotfix#1490
JeremyDev87 merged 10 commits into
masterfrom
fix/hud-installer-lib-sync

Conversation

@JeremyDev87
Copy link
Copy Markdown
Owner

Summary

Critical hotfix for the v5.6.0 / v5.6.1 HUD statusLine regression. Every user who upgraded saw only ◕‿◕ CodingBuddy instead of the full Wave 1/2/3 status line. The installer copied the script but never the hooks/lib/ modules it depends on.

  • Root cause: _install_statusline ran shutil.copy on codingbuddy-hud.py and never touched the sibling lib/. The v5.6.0 refactor (b0fb332) extracted 11 hud_*.py modules + tiny_actor_presets.py to hooks/lib/, so every upgraded user ended up with ~/.claude/hud/codingbuddy-hud.py present and ~/.claude/hud/lib/ missing. The script's try: from hud_buddy import BUDDY_FACE failed and the outer try/except printed only the fallback face.
  • Sister bug: _install_hook_with_lib used copytree(dirs_exist_ok=True) which writes new files but never removes files that existed before but are gone now — renamed/removed modules from prior versions could linger in ~/.claude/hooks/lib/.
  • Fix: new _atomic_sync_with_lib helper performs rmtree + copytree on the lib directory so both installers are stale-safe.

Before / After

# Before (5.6.1)
$ statusline output
◕‿◕ CodingBuddy

# After (5.6.2)
$ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh
[verify-install-simulation] stdout='◕‿◕ CB v5.6.2 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6'
[verify-install-simulation] PASS: full status line rendered (v5.6.2)

Commits (TDD, 8 atomic)

  1. feat(hooks): add _atomic_sync_with_lib helper — new install primitive with 9 RED→GREEN unit tests
  2. fix(hud): sync hooks/lib in _install_statusline — HUD installer migration + 8 unit + 4 E2E parametrized tests (clean/partial/stale/fresh scenarios)
  3. test(hooks): regression gate for _install_hook_with_lib stale-safe sync — sister-bug gate (4 tests)
  4. feat(health): add check_hud_installation diagnostic — HealthChecker check Add Backend Developer Agent #11 with subprocess smoke test (5 tests)
  5. ci: add HUD installer regression gate to canary + e2e workflows — gate on both PR-level (e2e-plugin.yml) and post-merge (canary.yml)
  6. chore(scripts): add verify-install-simulation.sh pre-release gate — shell script exercising the exact user code path
  7. chore(release): prepare v5.6.2 — bump-version + CHANGELOG
  8. chore(scripts): isolate HOME and assert exact version in verify-install-simulation — simulation hardening uncovered while running v5.6.2

Regression gates (why this cannot recur)

Gate What it catches
test_session_start_hud.py::TestHudInstallE2ERegressionGate (4 parametrized) Runs installed script as real subprocess; asserts output ≠ fallback face across clean/partial/stale/fresh starting states
test_install_hook_with_lib.py (4 tests) Stale-safe behavior for UserPromptSubmit hook sister-bug
test_health_check.py::TestCheckHudInstallation (5 tests) Diagnostic finds the failure mode from user's existing workflow
verify-install-simulation.sh Exact-version assertion, HOME-isolated, CI-invoked — would have caught v5.6.0 ship
e2e-plugin.yml + canary.yml gates PR-level + post-merge CI gates block broken installers

Test results

102 passed in 0.52s
  - tests/test_atomic_sync_with_lib.py (9)
  - tests/test_session_start_hud.py (20)
  - tests/test_install_hook_with_lib.py (4)
  - tests/test_health_check.py (35)
  - hooks/test_session_start.py (34, no regressions)

Pre-existing master failures (test_hud.py::TestIntegration, test_user_prompt_submit.py::TestMcp*) are unrelated flakes — verified by checking out master and reproducing the same 5 failures.

Test plan

  • yarn workspace codingbuddy-claude-plugin lint — pass
  • yarn workspace codingbuddy-claude-plugin typecheck — pass
  • yarn workspace codingbuddy-claude-plugin build — pass
  • python3 -m pytest tests/test_atomic_sync_with_lib.py tests/test_session_start_hud.py tests/test_install_hook_with_lib.py tests/test_health_check.py hooks/test_session_start.py — 102 passed
  • bash packages/claude-code-plugin/scripts/verify-install-simulation.sh — PASS (v5.6.2)
  • CI: e2e-plugin-hooks job green (HUD regression gate + simulation)
  • CI: plugin-hooks-tests job green (post-merge gate)
  • Post-release: user receives 5.6.2 cache → first session-start auto-renders full status line (no manual action)

Refs #1490

Introduces the canonical install primitive for asset+lib syncs used by
both the UserPromptSubmit hook installer and the HUD statusLine
installer. The helper guarantees:

1. The script file is copied and chmod 0755.
2. The sibling lib/ directory is **atomically replaced** (rmtree +
   copytree) so renamed or removed modules from prior plugin versions
   cannot linger in the target directory.
3. __pycache__, *.pyc, *.pyo, .pytest_cache, test_*.py, and *.egg-info
   are excluded from the runtime lib so sys.path stays clean.

Why rmtree-then-copytree (not dirs_exist_ok=True): copytree's
dirs_exist_ok mode only writes; it does not remove files that existed
before but are gone now. A renamed module (e.g. hud_old.py →
hud_new.py) would remain in the target lib and could be imported
first, causing subtle regressions. session-start runs once per
Claude Code session, so the rmtree cost is negligible.

This commit only introduces the helper and migrates
_install_hook_with_lib to use it (no behavior change for the
UserPromptSubmit hook beyond stale-safe lib sync). The HUD installer
migration follows in the next commit, gated on its own test suite.

TDD: 9 RED tests in tests/test_atomic_sync_with_lib.py written first,
then GREEN by implementing the helper. Existing
TestHookLibCopy/TestRegisterHookInSettings/TestEnsureMcpJson regression
suites all still pass (34 prior tests + 9 new = 43).

Refs #1490
The v5.6.0 refactor extracted 11 hud_*.py modules + tiny_actor_presets
into hooks/lib/, but _install_statusline still ran a single shutil.copy
on codingbuddy-hud.py and never touched the sibling lib/ directory.

End-state on every upgraded user machine:
  ~/.claude/hud/codingbuddy-hud.py  ← updated
  ~/.claude/hud/lib/                 ← MISSING

The script tries `from hud_buddy import BUDDY_FACE` first; the
ImportError trips its outer try/except, which prints the bare
fallback `◕‿◕ CodingBuddy`. Every Wave 1/2/3 status line feature
shipped in v5.6.0 / v5.6.1 was therefore invisible to all users.

Fix:
  - Replace shutil.copy(...) with _atomic_sync_with_lib(source, hud_dir)
    so the script and the entire sibling lib/ are synced as a single
    unit on every session start.
  - Write ~/.claude/hud/.version stamp so health_check (next commit)
    can detect drift.
  - Honour CODINGBUDDY_HUD_DEBUG=1 to surface installer errors on
    stderr (default still silent so session start is never blocked).

Test coverage (test_session_start_hud.py):
  - TestSyncHudAssets (8 tests): copies all 12 required modules,
    excludes __pycache__/*.pyc/.pytest_cache/test_*.py, replaces
    stale renamed modules, idempotent re-invocation, writes version
    stamp, gracefully skips lib when absent, preserves settings.json
    update.
  - TestHudInstallE2ERegressionGate (4 parametrized scenarios):
    runs the installed script as a real subprocess and asserts the
    output is NOT '◕‿◕ CodingBuddy'. Scenarios cover clean install,
    partial (current v5.6.1 user state), stale lib (renamed module),
    and fresh idempotent re-run. This is the single regression gate
    that would have caught the v5.6.0 / v5.6.1 ship.

Total: 6 prior + 14 new = 20 statusline tests. All green.

Refs #1490
…nc (#1490)

Sister-bug to the HUD installer fix: _install_hook_with_lib used
copytree(dirs_exist_ok=True), which writes new files but never
removes files that existed before but are gone now. A renamed
module (e.g. mode_engine.py → mode_engine_v2.py) would leave the
old file in ~/.claude/hooks/lib/ indefinitely, where Python's
import system could pick it up first.

Commit 1 already migrated _install_hook_with_lib to delegate to
_atomic_sync_with_lib (which performs an atomic rmtree + copytree).
This commit adds the explicit regression gate so future refactors
cannot reintroduce the stale-write pattern unnoticed.

Coverage (test_install_hook_with_lib.py, 4 cases):
  - test_replaces_stale_lib_modules — stale renamed module purged
  - test_excludes_pycache_pyc_pytest_cache_and_test_files —
    pollutants from source lib never enter target lib
  - test_renames_source_to_target_filename — user-prompt-submit.py
    source lands at codingbuddy-mode-detect.py target, source name
    does NOT linger
  - test_idempotent_double_invocation — two consecutive installs
    yield identical state

Refs #1490
Adds check #11 to HealthChecker so the v5.6.0/v5.6.1 failure mode is
detectable from the user's existing health check workflow without
having to read installer source.

What it verifies:
  1. ~/.claude/hud/codingbuddy-hud.py is present
  2. ~/.claude/hud/lib/ exists and contains the seven critical
     modules: hud_buddy, hud_state, hud_helpers, tiny_actor_presets,
     hud_version, hud_rate_limits, hud_layout
  3. Subprocess render smoke test catches the case where every file
     looks present but imports still fail at runtime (permission
     issues, partial copy, .pyc clash). The smoke test fails if
     the script returns the bare '◕‿◕ CodingBuddy' fallback.
  4. ~/.claude/hud/.version stamp is reported in the PASS message
     so users can confirm which plugin version installed the assets.

Coverage (test_health_check.py, +5 cases):
  - test_fail_when_script_missing
  - test_fail_when_lib_directory_missing
  - test_fail_when_required_modules_missing — names the missing module
  - test_fail_when_smoke_test_returns_fallback — uses a stub script
    that always prints the fallback face
  - test_pass_with_real_plugin_install — end-to-end: runs the real
    _install_statusline against the real codingbuddy-hud.py source
    in tmpdir, then asserts check_hud_installation returns PASS

run_all now returns 11 results (was 10); test renamed accordingly.

Refs #1490
)

Pre-release regression script that simulates a fresh user receiving
cache 5.6.x and starting Claude Code for the first time. Runs the real
_install_statusline against the in-tree hooks/ source in an isolated
tmpdir, then executes the installed script as a subprocess and asserts
the output is the full status line — not the bare '◕‿◕ CodingBuddy'
fallback face.

Why this complements pytest:
  - The pytest E2E gate runs inside the test runner's process and
    fixtures. This shell script exercises the exact code path a user
    hits, including filesystem layout, chmod, and a fresh subprocess.
  - It can be run by maintainers locally before pushing release
    branches without needing to set up a Python venv with pytest.
  - It is invoked from canary CI in the next commit so PRs cannot
    merge with a broken installer.

Exit codes:
  0 — full status line rendered, all assertions passed
  1 — fallback face detected or required tokens missing
  2 — install crashed before render

Local verification at commit time:
  $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh
  [verify-install-simulation] PASS: full status line rendered
  exit=0
  Output: '◕‿◕ CB v5.6.1 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6'

Refs #1490
Adds the v5.6.2 regression gate to two CI workflows so a broken
installer cannot reach canary or release:

1. canary.yml — new ``plugin-hooks-tests`` job (Python pytest only,
   no node/yarn build needed) blocks ``publish-canary`` via ``needs``.
   Triggered on master/stag-** push, so master merges are gated
   before any canary npm publish.

2. e2e-plugin.yml — adds the same pytest invocation + simulation
   script step to the existing ``e2e-plugin-hooks`` job, and extends
   ``paths`` to include ``packages/claude-code-plugin/tests/**`` and
   ``scripts/verify-install-simulation.sh`` so PR-level changes
   trigger the gate. e2e-plugin runs on non-master branches, so
   PRs see the gate before merge.

Together the two jobs cover both PR review (e2e-plugin) and
post-merge canary publish (canary), so the v5.6.0/v5.6.1 failure
mode (installer ships without lib sync, all users see the fallback
face) cannot recur silently.

Test invocations cover all five v5.6.2 hotfix test files:
  - tests/test_atomic_sync_with_lib.py    (9 tests)
  - tests/test_session_start_hud.py       (20 tests)
  - tests/test_install_hook_with_lib.py   (4 tests)
  - tests/test_health_check.py            (35 tests)
  - scripts/verify-install-simulation.sh  (subprocess render gate)

actions/setup-python pin reused from existing e2e-plugin.yml entry
(SHA a26af69be951a213d495a4c3e4e4022e16d87065 = v5.6.0).

Refs #1490
Bump the workspace version from 5.6.1 to 5.6.2 and publish the
CHANGELOG entry for the HUD installer hotfix.

v5.6.2 closes the v5.6.0 / v5.6.1 HUD statusLine installer regression
(#1490). Every user who upgraded to 5.6.0 or 5.6.1 saw only the
fallback face '◕‿◕ CodingBuddy' instead of the full Wave 1/2/3 status
line, because the installer never copied the hooks/lib/ modules the
script depends on. v5.6.2 routes both the HUD installer and the
UserPromptSubmit hook installer through a new _atomic_sync_with_lib
helper that copies the script and atomically replaces the entire
lib/ directory (rmtree + copytree), guaranteeing renamed/removed
modules from prior plugin versions cannot linger.

Bump surface:
- apps/mcp-server/package.json, src/shared/version.ts
- packages/rules/package.json
- packages/claude-code-plugin/package.json (+ peerDependencies),
  .claude-plugin/plugin.json, README.md
- .claude-plugin/marketplace.json
- yarn.lock
- CHANGELOG.md (new [5.6.2] section)

Refs #1490
…ll-simulation (#1490)

Two correctness fixes uncovered while running the script after the
v5.6.2 bump:

1. **HOME isolation** — the subprocess that ran the installed HUD
   inherited the developer's real HOME, so `read_installed_version`
   tier-1 lookup picked up `~/.claude/plugins/installed_plugins.json`
   from the developer machine (5.6.1) instead of the version we are
   about to ship. Output read `CB v5.6.1` even though plugin.json
   was already 5.6.2. The script now passes an explicit `env=` to
   subprocess.run with HOME pointing at the tmpdir.

2. **Fake installed_plugins.json** — Claude Code writes
   `~/.claude/plugins/installed_plugins.json` after every plugin
   install/update, and `hud_version.get_fresh_version` reads it as
   tier 1. The simulation now writes a stub manifest in the isolated
   tmpdir so the installed HUD's version-resolution path matches what
   a real user will hit on first session-start after `/plugin update`.

3. **Exact version assertion** — the script now reads the version
   from `plugin.json`, asserts `CB v<expected_version>` appears in
   the rendered status line, and verifies `~/.claude/hud/.version`
   contains the same value. This auto-tracks bump-version.sh and
   removes the previous risk of shipping a release where the
   version segment renders empty without anyone noticing.

Local verification:
  $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh
  [verify-install-simulation] stdout='◕‿◕ CB v5.6.2 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6'
  [verify-install-simulation] PASS: full status line rendered (v5.6.2)
  exit=0

Refs #1490
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
codingbuddy-landing Ready Ready Preview, Comment Apr 11, 2026 5:33pm

…egment (#1490)

Two Critical issues surfaced in CI (e2e-plugin-hooks/3.11) that the
local pre-push check missed because the developer/runner HOME leaked
into pytest subprocess invocations:

1. **TestHudInstallE2ERegressionGate all 4 scenarios FAILED on CI**
   with `assert "CB v" in out` because
   ``hud_version.get_fresh_version``'s tier-1 lookup reads
   ``~/.claude/plugins/installed_plugins.json`` and CI has none. The
   render therefore produced ``CB | Ready 🟢 | ...`` — a silent
   half-broken state where every module imports successfully but the
   version segment is empty. Locally the same test passed only
   because the developer's real ``~/.claude/plugins/installed_plugins.json``
   (v5.6.1) leaked into the subprocess.

   Fix: the test now writes a stub
   ``installed_plugins.json`` into the tmpdir fake home and invokes
   the subprocess with ``env={"HOME": str(home), ...}`` so all three
   version-resolution tiers (tier 1 installed_plugins.json, tier 2
   plugin.json, tier 3 hud_state) see the isolated environment.
   Assertion is strengthened to ``CB v<expected_version>`` with
   ``expected_version`` read from ``plugin.json`` so future
   bump-version.sh runs auto-gate. A ``.version`` stamp assertion
   is also added per scenario.

   The simulation shell script (commit 8) was already hardened this
   way; this commit brings the pytest E2E suite to parity so CI and
   local runs agree.

2. **check_hud_installation smoke test did not detect empty version**
   — it only compared against the literal fallback face, so the
   ``CB | Ready ...`` half-broken state returned PASS even though
   the user would see a status line with a missing version segment.

   Fix: ``check_hud_installation`` now detects ``"CB "`` without
   ``"CB v"`` in the rendered output and returns FAIL with a clear
   message ("HUD rendered empty version segment — hud_version
   fallback chain broken"). The subprocess call also pins
   ``HOME=self._home_dir`` so the diagnostic honours the
   HealthChecker's configured home directory rather than leaking the
   CI runner's real home. A new regression test
   ``test_fail_when_version_segment_is_empty`` stubs a script that
   prints the half-broken line and asserts the check FAILs.

3. **test_pass_with_real_plugin_install** (health_check green-path
   gate) now writes the same fake ``installed_plugins.json`` so the
   subprocess check_hud_installation invokes can resolve the plugin
   version inside the isolated environment.

Local verification:
  $ python3 -m pytest tests/test_session_start_hud.py tests/test_health_check.py -v
  ... 56 passed in 0.90s
  $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh
  [verify-install-simulation] PASS: full status line rendered (v5.6.2)

Refs #1490
…#1490)

Addresses two EVAL findings from the PR #1490 review:

**HIGH #1 — race window during lib sync**
The previous implementation was rmtree-then-copytree:
    if target_lib.exists():
        shutil.rmtree(target_lib)
    shutil.copytree(source_lib, target_lib, ...)

which left ``target_lib`` missing for the full duration of the
copytree (hundreds of milliseconds for a multi-megabyte lib). Any
concurrent HUD subprocess that statted the installed script during
that window would see ``ImportError`` and render the fallback face —
ironic given this PR exists to fix exactly that symptom.

Replaced with a staging/rename pattern:
  1. ``copytree(source_lib, .lib.staging-<uuid>, ignore=...)``
  2. ``rename(target_lib, .lib.old-<uuid>)``   ← atomic syscall
  3. ``rename(.lib.staging-<uuid>, target_lib)`` ← atomic syscall
  4. ``rmtree(.lib.old-<uuid>)`` in ``finally``

The window during which ``target_lib`` does not exist now shrinks
from ``O(copytree)`` to ``O(rename)`` — effectively atomic on POSIX.
Concurrent installers from multiple simultaneous Claude Code
sessions are safe because each uses a uuid-scoped staging dir.

Rollback: if ``copytree`` raises mid-sync the except block removes
the staging dir and restores ``target_lib`` from the archive, so an
existing working install is never lost to a partial sync.

**HIGH #2 — redundant chmod after rename in _install_hook_with_lib**
``_atomic_sync_with_lib`` already chmod's the synced script to 0o755
before ``_install_hook_with_lib`` renames it to HOOK_FILENAME, and
POSIX rename preserves permission bits. The second chmod was
defensive but triggered an unnecessary silent-failure path on
filesystems that reject mode changes (NFS, read-only mounts).
Removed with a code comment explaining why.

**New regression tests (+2)**:
  - ``test_no_staging_leftovers_after_success`` — no stray
    ``.lib.staging-*`` / ``.lib.old-*`` dirs after a successful sync
  - ``test_rollback_preserves_old_lib_when_copytree_fails`` —
    monkeypatches ``shutil.copytree`` to raise OSError, then asserts
    the pre-existing target lib survives and no staging/archive
    dirs leak

Local verification: 105 pytest suites pass (100 + 5 from prior
commit + 2 new race-safety gates).

Refs #1490
@JeremyDev87 JeremyDev87 merged commit af0d12b into master Apr 11, 2026
29 checks passed
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
Introduces the canonical install primitive for asset+lib syncs used by
both the UserPromptSubmit hook installer and the HUD statusLine
installer. The helper guarantees:

1. The script file is copied and chmod 0755.
2. The sibling lib/ directory is **atomically replaced** (rmtree +
   copytree) so renamed or removed modules from prior plugin versions
   cannot linger in the target directory.
3. __pycache__, *.pyc, *.pyo, .pytest_cache, test_*.py, and *.egg-info
   are excluded from the runtime lib so sys.path stays clean.

Why rmtree-then-copytree (not dirs_exist_ok=True): copytree's
dirs_exist_ok mode only writes; it does not remove files that existed
before but are gone now. A renamed module (e.g. hud_old.py →
hud_new.py) would remain in the target lib and could be imported
first, causing subtle regressions. session-start runs once per
Claude Code session, so the rmtree cost is negligible.

This commit only introduces the helper and migrates
_install_hook_with_lib to use it (no behavior change for the
UserPromptSubmit hook beyond stale-safe lib sync). The HUD installer
migration follows in the next commit, gated on its own test suite.

TDD: 9 RED tests in tests/test_atomic_sync_with_lib.py written first,
then GREEN by implementing the helper. Existing
TestHookLibCopy/TestRegisterHookInSettings/TestEnsureMcpJson regression
suites all still pass (34 prior tests + 9 new = 43).

Refs #1490
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
The v5.6.0 refactor extracted 11 hud_*.py modules + tiny_actor_presets
into hooks/lib/, but _install_statusline still ran a single shutil.copy
on codingbuddy-hud.py and never touched the sibling lib/ directory.

End-state on every upgraded user machine:
  ~/.claude/hud/codingbuddy-hud.py  ← updated
  ~/.claude/hud/lib/                 ← MISSING

The script tries `from hud_buddy import BUDDY_FACE` first; the
ImportError trips its outer try/except, which prints the bare
fallback `◕‿◕ CodingBuddy`. Every Wave 1/2/3 status line feature
shipped in v5.6.0 / v5.6.1 was therefore invisible to all users.

Fix:
  - Replace shutil.copy(...) with _atomic_sync_with_lib(source, hud_dir)
    so the script and the entire sibling lib/ are synced as a single
    unit on every session start.
  - Write ~/.claude/hud/.version stamp so health_check (next commit)
    can detect drift.
  - Honour CODINGBUDDY_HUD_DEBUG=1 to surface installer errors on
    stderr (default still silent so session start is never blocked).

Test coverage (test_session_start_hud.py):
  - TestSyncHudAssets (8 tests): copies all 12 required modules,
    excludes __pycache__/*.pyc/.pytest_cache/test_*.py, replaces
    stale renamed modules, idempotent re-invocation, writes version
    stamp, gracefully skips lib when absent, preserves settings.json
    update.
  - TestHudInstallE2ERegressionGate (4 parametrized scenarios):
    runs the installed script as a real subprocess and asserts the
    output is NOT '◕‿◕ CodingBuddy'. Scenarios cover clean install,
    partial (current v5.6.1 user state), stale lib (renamed module),
    and fresh idempotent re-run. This is the single regression gate
    that would have caught the v5.6.0 / v5.6.1 ship.

Total: 6 prior + 14 new = 20 statusline tests. All green.

Refs #1490
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
…nc (#1490)

Sister-bug to the HUD installer fix: _install_hook_with_lib used
copytree(dirs_exist_ok=True), which writes new files but never
removes files that existed before but are gone now. A renamed
module (e.g. mode_engine.py → mode_engine_v2.py) would leave the
old file in ~/.claude/hooks/lib/ indefinitely, where Python's
import system could pick it up first.

Commit 1 already migrated _install_hook_with_lib to delegate to
_atomic_sync_with_lib (which performs an atomic rmtree + copytree).
This commit adds the explicit regression gate so future refactors
cannot reintroduce the stale-write pattern unnoticed.

Coverage (test_install_hook_with_lib.py, 4 cases):
  - test_replaces_stale_lib_modules — stale renamed module purged
  - test_excludes_pycache_pyc_pytest_cache_and_test_files —
    pollutants from source lib never enter target lib
  - test_renames_source_to_target_filename — user-prompt-submit.py
    source lands at codingbuddy-mode-detect.py target, source name
    does NOT linger
  - test_idempotent_double_invocation — two consecutive installs
    yield identical state

Refs #1490
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
Adds check #11 to HealthChecker so the v5.6.0/v5.6.1 failure mode is
detectable from the user's existing health check workflow without
having to read installer source.

What it verifies:
  1. ~/.claude/hud/codingbuddy-hud.py is present
  2. ~/.claude/hud/lib/ exists and contains the seven critical
     modules: hud_buddy, hud_state, hud_helpers, tiny_actor_presets,
     hud_version, hud_rate_limits, hud_layout
  3. Subprocess render smoke test catches the case where every file
     looks present but imports still fail at runtime (permission
     issues, partial copy, .pyc clash). The smoke test fails if
     the script returns the bare '◕‿◕ CodingBuddy' fallback.
  4. ~/.claude/hud/.version stamp is reported in the PASS message
     so users can confirm which plugin version installed the assets.

Coverage (test_health_check.py, +5 cases):
  - test_fail_when_script_missing
  - test_fail_when_lib_directory_missing
  - test_fail_when_required_modules_missing — names the missing module
  - test_fail_when_smoke_test_returns_fallback — uses a stub script
    that always prints the fallback face
  - test_pass_with_real_plugin_install — end-to-end: runs the real
    _install_statusline against the real codingbuddy-hud.py source
    in tmpdir, then asserts check_hud_installation returns PASS

run_all now returns 11 results (was 10); test renamed accordingly.

Refs #1490
@JeremyDev87 JeremyDev87 deleted the fix/hud-installer-lib-sync branch April 11, 2026 17:36
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
)

Pre-release regression script that simulates a fresh user receiving
cache 5.6.x and starting Claude Code for the first time. Runs the real
_install_statusline against the in-tree hooks/ source in an isolated
tmpdir, then executes the installed script as a subprocess and asserts
the output is the full status line — not the bare '◕‿◕ CodingBuddy'
fallback face.

Why this complements pytest:
  - The pytest E2E gate runs inside the test runner's process and
    fixtures. This shell script exercises the exact code path a user
    hits, including filesystem layout, chmod, and a fresh subprocess.
  - It can be run by maintainers locally before pushing release
    branches without needing to set up a Python venv with pytest.
  - It is invoked from canary CI in the next commit so PRs cannot
    merge with a broken installer.

Exit codes:
  0 — full status line rendered, all assertions passed
  1 — fallback face detected or required tokens missing
  2 — install crashed before render

Local verification at commit time:
  $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh
  [verify-install-simulation] PASS: full status line rendered
  exit=0
  Output: '◕‿◕ CB v5.6.1 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6'

Refs #1490
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
Adds the v5.6.2 regression gate to two CI workflows so a broken
installer cannot reach canary or release:

1. canary.yml — new ``plugin-hooks-tests`` job (Python pytest only,
   no node/yarn build needed) blocks ``publish-canary`` via ``needs``.
   Triggered on master/stag-** push, so master merges are gated
   before any canary npm publish.

2. e2e-plugin.yml — adds the same pytest invocation + simulation
   script step to the existing ``e2e-plugin-hooks`` job, and extends
   ``paths`` to include ``packages/claude-code-plugin/tests/**`` and
   ``scripts/verify-install-simulation.sh`` so PR-level changes
   trigger the gate. e2e-plugin runs on non-master branches, so
   PRs see the gate before merge.

Together the two jobs cover both PR review (e2e-plugin) and
post-merge canary publish (canary), so the v5.6.0/v5.6.1 failure
mode (installer ships without lib sync, all users see the fallback
face) cannot recur silently.

Test invocations cover all five v5.6.2 hotfix test files:
  - tests/test_atomic_sync_with_lib.py    (9 tests)
  - tests/test_session_start_hud.py       (20 tests)
  - tests/test_install_hook_with_lib.py   (4 tests)
  - tests/test_health_check.py            (35 tests)
  - scripts/verify-install-simulation.sh  (subprocess render gate)

actions/setup-python pin reused from existing e2e-plugin.yml entry
(SHA a26af69be951a213d495a4c3e4e4022e16d87065 = v5.6.0).

Refs #1490
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
Bump the workspace version from 5.6.1 to 5.6.2 and publish the
CHANGELOG entry for the HUD installer hotfix.

v5.6.2 closes the v5.6.0 / v5.6.1 HUD statusLine installer regression
(#1490). Every user who upgraded to 5.6.0 or 5.6.1 saw only the
fallback face '◕‿◕ CodingBuddy' instead of the full Wave 1/2/3 status
line, because the installer never copied the hooks/lib/ modules the
script depends on. v5.6.2 routes both the HUD installer and the
UserPromptSubmit hook installer through a new _atomic_sync_with_lib
helper that copies the script and atomically replaces the entire
lib/ directory (rmtree + copytree), guaranteeing renamed/removed
modules from prior plugin versions cannot linger.

Bump surface:
- apps/mcp-server/package.json, src/shared/version.ts
- packages/rules/package.json
- packages/claude-code-plugin/package.json (+ peerDependencies),
  .claude-plugin/plugin.json, README.md
- .claude-plugin/marketplace.json
- yarn.lock
- CHANGELOG.md (new [5.6.2] section)

Refs #1490
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
…ll-simulation (#1490)

Two correctness fixes uncovered while running the script after the
v5.6.2 bump:

1. **HOME isolation** — the subprocess that ran the installed HUD
   inherited the developer's real HOME, so `read_installed_version`
   tier-1 lookup picked up `~/.claude/plugins/installed_plugins.json`
   from the developer machine (5.6.1) instead of the version we are
   about to ship. Output read `CB v5.6.1` even though plugin.json
   was already 5.6.2. The script now passes an explicit `env=` to
   subprocess.run with HOME pointing at the tmpdir.

2. **Fake installed_plugins.json** — Claude Code writes
   `~/.claude/plugins/installed_plugins.json` after every plugin
   install/update, and `hud_version.get_fresh_version` reads it as
   tier 1. The simulation now writes a stub manifest in the isolated
   tmpdir so the installed HUD's version-resolution path matches what
   a real user will hit on first session-start after `/plugin update`.

3. **Exact version assertion** — the script now reads the version
   from `plugin.json`, asserts `CB v<expected_version>` appears in
   the rendered status line, and verifies `~/.claude/hud/.version`
   contains the same value. This auto-tracks bump-version.sh and
   removes the previous risk of shipping a release where the
   version segment renders empty without anyone noticing.

Local verification:
  $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh
  [verify-install-simulation] stdout='◕‿◕ CB v5.6.2 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6'
  [verify-install-simulation] PASS: full status line rendered (v5.6.2)
  exit=0

Refs #1490
JeremyDev87 added a commit that referenced this pull request Apr 11, 2026
…egment (#1490)

Two Critical issues surfaced in CI (e2e-plugin-hooks/3.11) that the
local pre-push check missed because the developer/runner HOME leaked
into pytest subprocess invocations:

1. **TestHudInstallE2ERegressionGate all 4 scenarios FAILED on CI**
   with `assert "CB v" in out` because
   ``hud_version.get_fresh_version``'s tier-1 lookup reads
   ``~/.claude/plugins/installed_plugins.json`` and CI has none. The
   render therefore produced ``CB | Ready 🟢 | ...`` — a silent
   half-broken state where every module imports successfully but the
   version segment is empty. Locally the same test passed only
   because the developer's real ``~/.claude/plugins/installed_plugins.json``
   (v5.6.1) leaked into the subprocess.

   Fix: the test now writes a stub
   ``installed_plugins.json`` into the tmpdir fake home and invokes
   the subprocess with ``env={"HOME": str(home), ...}`` so all three
   version-resolution tiers (tier 1 installed_plugins.json, tier 2
   plugin.json, tier 3 hud_state) see the isolated environment.
   Assertion is strengthened to ``CB v<expected_version>`` with
   ``expected_version`` read from ``plugin.json`` so future
   bump-version.sh runs auto-gate. A ``.version`` stamp assertion
   is also added per scenario.

   The simulation shell script (commit 8) was already hardened this
   way; this commit brings the pytest E2E suite to parity so CI and
   local runs agree.

2. **check_hud_installation smoke test did not detect empty version**
   — it only compared against the literal fallback face, so the
   ``CB | Ready ...`` half-broken state returned PASS even though
   the user would see a status line with a missing version segment.

   Fix: ``check_hud_installation`` now detects ``"CB "`` without
   ``"CB v"`` in the rendered output and returns FAIL with a clear
   message ("HUD rendered empty version segment — hud_version
   fallback chain broken"). The subprocess call also pins
   ``HOME=self._home_dir`` so the diagnostic honours the
   HealthChecker's configured home directory rather than leaking the
   CI runner's real home. A new regression test
   ``test_fail_when_version_segment_is_empty`` stubs a script that
   prints the half-broken line and asserts the check FAILs.

3. **test_pass_with_real_plugin_install** (health_check green-path
   gate) now writes the same fake ``installed_plugins.json`` so the
   subprocess check_hud_installation invokes can resolve the plugin
   version inside the isolated environment.

Local verification:
  $ python3 -m pytest tests/test_session_start_hud.py tests/test_health_check.py -v
  ... 56 passed in 0.90s
  $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh
  [verify-install-simulation] PASS: full status line rendered (v5.6.2)

Refs #1490
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant