Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "codingbuddy",
"source": "./packages/claude-code-plugin",
"description": "PLAN/ACT/EVAL workflow, specialist agents, and reusable skills for systematic TDD development",
"version": "5.6.1",
"version": "5.6.2",
"category": "development"
}
]
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,41 @@ permissions:
contents: read

jobs:
plugin-hooks-tests:
# Regression gate for #1490 — HUD installer must sync hooks/lib
# alongside the script. This job runs the Python pytest suites for
# the plugin hooks (no node/yarn build needed) so a broken installer
# cannot ship to canary or release.
if: github.repository == 'JeremyDev87/codingbuddy'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.11'

- name: Install pytest
run: python3 -m pip install --quiet pytest

- name: Run plugin hook test suites (#1490 regression gate)
working-directory: packages/claude-code-plugin
run: |
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 \
-v --tb=short

- name: HUD install simulation (#1490 end-to-end gate)
run: bash packages/claude-code-plugin/scripts/verify-install-simulation.sh

publish-canary:
needs: plugin-hooks-tests
if: github.repository == 'JeremyDev87/codingbuddy'
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/e2e-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- stag-**
paths:
- 'packages/claude-code-plugin/hooks/**'
- 'packages/claude-code-plugin/tests/**'
- 'packages/claude-code-plugin/scripts/verify-install-simulation.sh'
- 'tests/e2e/plugin-hooks/**'
- .github/workflows/e2e-plugin.yml

Expand Down Expand Up @@ -50,6 +52,19 @@ jobs:
- name: Run E2E plugin hook tests
run: python3 -m pytest tests/e2e/plugin-hooks/ -v --timeout=30 --tb=short

- name: Plugin hook unit/E2E regression suites (#1490)
working-directory: packages/claude-code-plugin
run: |
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 \
-v --tb=short

- name: HUD install simulation (#1490 end-to-end gate)
run: bash packages/claude-code-plugin/scripts/verify-install-simulation.sh

e2e-plugin-docker:
if: github.repository == 'JeremyDev87/codingbuddy'
runs-on: ubuntu-latest
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.6.2] - 2026-04-12

Critical hotfix for the v5.6.0 / v5.6.1 HUD statusLine installer
regression. Every user who upgraded to v5.6.0 or v5.6.1 saw only the
fallback face `◕‿◕ CodingBuddy` instead of the full Wave 1/2/3 status
line, regardless of how many statusbar features had shipped, because
the installer never copied the `hooks/lib/` modules the script
depends on.

### Fixed
- **HUD installer never synced `hooks/lib/`** ([#1490](https://github.com/JeremyDev87/codingbuddy/issues/1490)) — `_install_statusline` only ran `shutil.copy` on `codingbuddy-hud.py` and never touched the sibling `lib/` directory. 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 on the missing module and the outer `try/except` printed only the bare fallback `◕‿◕ CodingBuddy`. Every Wave 1-A version, Wave 1-B heal, Wave 1-C rate limit icons, Wave 1-D adaptive layout, Wave 2-A breathing face, Wave 2-B velocity, Wave 2-C cache savings, Wave 2-D rainbow, Wave 2-E smart context bar, and Wave 3 / 3b integration was therefore invisible. Fixed by routing the install through a new `_atomic_sync_with_lib` helper that copies the script and atomically replaces the entire `lib/` directory (rmtree + copytree).
- **Sister bug in `_install_hook_with_lib`** — the UserPromptSubmit hook installer used `shutil.copytree(dirs_exist_ok=True)`, which writes new files but never removes files that existed before but are gone now. A renamed module from a prior version would have lingered in `~/.claude/hooks/lib/` and could be imported first by Python's import system. Now also routes through `_atomic_sync_with_lib` so renamed/removed modules are purged on every sync.

### Added
- **`_atomic_sync_with_lib(source_script, target_dir, extra_ignore)`** — canonical install primitive used by both `_install_statusline` and `_install_hook_with_lib`. Performs an atomic `rmtree + copytree` on the lib directory and excludes `__pycache__`, `*.pyc`, `*.pyo`, `.pytest_cache`, `test_*.py`, `*.egg-info` so the runtime `sys.path` stays clean. Optional `CODINGBUDDY_HUD_DEBUG=1` env var surfaces install errors on stderr (default still silent so session start is never blocked).
- **`~/.claude/hud/.version` stamp** — `_install_statusline` now writes the plugin version it just installed so `health_check.check_hud_installation` can detect drift and the user can confirm which release they are running.
- **`HealthChecker.check_hud_installation` (check #11)** — new diagnostic verifies `~/.claude/hud/codingbuddy-hud.py` exists, `~/.claude/hud/lib/` contains the seven critical modules, and a subprocess render smoke test does not return the fallback face. Detects all known failure modes of #1490 from the user's existing health-check workflow.
- **`scripts/verify-install-simulation.sh`** — pre-release shell gate that simulates a fresh user receiving cache 5.6.x and starting Claude Code. Runs `_install_statusline` against the in-tree source in an isolated tmpdir, then executes the installed script as a real subprocess and asserts the output is the full status line. Run locally before pushing release branches; invoked from CI in `e2e-plugin.yml` and `canary.yml`.

### Test Coverage
- **9 new tests** in `tests/test_atomic_sync_with_lib.py` covering script copy, executable bit, lib copy, no-lib silent skip, ignore patterns, stale module replacement, idempotent re-invocation, and `extra_ignore` arg.
- **14 new tests** in `tests/test_session_start_hud.py`:
- 8 unit cases (`TestSyncHudAssets`) — copies all 12 required modules, excludes pollutants, replaces stale modules, idempotent, writes version stamp, gracefully skips when lib absent, preserves settings.json update.
- 4 parametrized E2E cases (`TestHudInstallE2ERegressionGate`) — runs the installed script as a real subprocess across `clean`, `partial`, `stale`, and `fresh` starting states and asserts the output is **not** `◕‿◕ CodingBuddy`. This is the single regression gate that would have caught the v5.6.0 / v5.6.1 ship.
- **4 new tests** in `tests/test_install_hook_with_lib.py` — sister-bug regression gate for `_install_hook_with_lib` (stale module replacement, ignore patterns, source-name rename, idempotency).
- **5 new tests** in `tests/test_health_check.py::TestCheckHudInstallation` — script missing, lib missing, modules missing, smoke test fallback detection, end-to-end PASS with real `_install_statusline` invocation.
- **CI gates** — both `e2e-plugin.yml` (PR-level) and `canary.yml` (post-merge) now run all four pytest suites and `verify-install-simulation.sh` so a broken installer cannot reach release.

## [5.6.1] - 2026-04-11

Hotfix closing the remaining gaps in the v5.6.0 HUD Statusbar Wave cycle:
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codingbuddy",
"version": "5.6.1",
"version": "5.6.2",
"description": "Multi-AI Rules MCP Server - One source of truth for AI coding rules across all AI assistants",
"author": "JeremyDev87",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp-server/src/shared/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
* Single source of truth for the runtime package version.
* Updated automatically by scripts/bump-version.sh on each release.
*/
export const VERSION = '5.6.1';
export const VERSION = '5.6.2';
2 changes: 1 addition & 1 deletion packages/claude-code-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codingbuddy",
"version": "5.6.1",
"version": "5.6.2",
"description": "PLAN/ACT/EVAL workflow with auto-detection, specialist agents, and reusable skills for systematic TDD development",
"author": {
"name": "JeremyDev87",
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-code-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# CodingBuddy Claude Code Plugin

> Version 5.6.1
> Version 5.6.2

Multi-AI Rules for consistent coding practices - PLAN/ACT/EVAL workflow, specialist agents, and reusable skills for systematic development.

Expand Down
131 changes: 130 additions & 1 deletion packages/claude-code-plugin/hooks/lib/health_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,139 @@ def check_standalone_readiness(self) -> Dict[str, str]:
)
return _result("standalone_readiness", "WARN", f"Standalone not ready: {', '.join(issues)}")

# ------------------------------------------------------------------
# Check 11: HUD asset installation (#1490 prevention)
# ------------------------------------------------------------------
def check_hud_installation(self) -> Dict[str, str]:
"""Verify HUD asset installation integrity.

Detects the v5.6.0/v5.6.1 failure mode where ``~/.claude/hud/lib``
is missing or stale and the statusLine renders only the
fallback ``◕‿◕ CodingBuddy`` face.

Performs three layers of verification:
1. Script presence at ``~/.claude/hud/codingbuddy-hud.py``
2. Lib directory presence + the seven critical modules:
``hud_buddy``, ``hud_state``, ``hud_helpers``,
``tiny_actor_presets``, ``hud_version``, ``hud_rate_limits``,
``hud_layout``
3. A subprocess render smoke test that catches the case where
everything looks present but imports still fail at runtime
(e.g. permission issues, partial copy)
"""
import subprocess

hud_dir = os.path.join(self._claude_dir, "hud")
script = os.path.join(hud_dir, "codingbuddy-hud.py")
lib = os.path.join(hud_dir, "lib")
stamp = os.path.join(hud_dir, ".version")

if not os.path.isfile(script):
return _result(
"hud_installation",
"FAIL",
"HUD script missing at ~/.claude/hud/codingbuddy-hud.py",
)

if not os.path.isdir(lib):
return _result(
"hud_installation",
"FAIL",
"HUD lib/ directory missing — statusLine renders fallback only",
)

required_modules = [
"hud_buddy.py",
"hud_state.py",
"hud_helpers.py",
"tiny_actor_presets.py",
"hud_version.py",
"hud_rate_limits.py",
"hud_layout.py",
]
missing = [
m for m in required_modules if not os.path.isfile(os.path.join(lib, m))
]
if missing:
return _result(
"hud_installation",
"FAIL",
f"HUD lib missing modules: {', '.join(missing)}",
)

# Subprocess render smoke — catches runtime import failures
# AND partially-rendered status lines (e.g. empty version
# segment when hud_version's 3-tier fallback all fail).
# HOME is pinned to self._home_dir so the subprocess's
# tier-1 version lookup (~/.claude/plugins/installed_plugins.json)
# resolves against the same environment the diagnostic was
# configured with, rather than leaking the CI runner's real
# home.
isolated_env = {
"HOME": self._home_dir,
"PATH": os.environ.get("PATH", ""),
"LANG": os.environ.get("LANG", ""),
"LC_ALL": os.environ.get("LC_ALL", ""),
}
try:
r = subprocess.run(
["python3", script],
input='{"session_id":"healthcheck","model":{"display_name":"Test"}}',
capture_output=True,
text=True,
timeout=5,
env=isolated_env,
)
rendered = r.stdout.strip()
if rendered == "◕‿◕ CodingBuddy":
return _result(
"hud_installation",
"FAIL",
"HUD smoke test produced fallback face — lib import failing at runtime",
)
# Version segment must not be empty. ``CB `` without the
# trailing ``v`` indicates all three version-resolution
# tiers (installed_plugins.json, plugin.json, hud_state)
# returned the empty string — a silent half-broken state
# that would otherwise ship unnoticed.
if "CB " in rendered and "CB v" not in rendered:
return _result(
"hud_installation",
"FAIL",
"HUD rendered empty version segment — hud_version fallback chain broken",
)
except subprocess.TimeoutExpired:
return _result(
"hud_installation",
"WARN",
"HUD smoke test timed out (5s)",
)
except Exception as e:
return _result(
"hud_installation",
"WARN",
f"HUD smoke test crashed: {e}",
)

version_msg = ""
if os.path.isfile(stamp):
try:
with open(stamp, "r", encoding="utf-8") as f:
version_msg = f" (v{f.read().strip()})"
except OSError:
pass

return _result(
"hud_installation",
"PASS",
f"HUD assets installed and rendering full status line{version_msg}",
)

# ------------------------------------------------------------------
# Aggregate
# ------------------------------------------------------------------
def run_all(self) -> List[Dict[str, str]]:
"""Run all 10 diagnostic checks and return results."""
"""Run all 11 diagnostic checks and return results."""
return [
self.check_hooks_json(),
self.check_hook_files(),
Expand All @@ -262,6 +390,7 @@ def run_all(self) -> List[Dict[str, str]]:
self.check_mcp_connection(),
self.check_runtime_mode(),
self.check_standalone_readiness(),
self.check_hud_installation(),
]

@staticmethod
Expand Down
Loading
Loading