feat(plugins): hot-reload classifier — skip restart on SKILL-content-only updates #121

Merged
claude-ceo-assistant merged 1 commits from feat/plugin-hot-reload-classifier into main 2026-05-08 15:26:34 +00:00

Summary

Closes core#112. Composes with #114 (atomic install) — before triggering restartFunc, classify the diff between staged and live; skip the restart if only **/SKILL.md content changed.

Why

Claude Code re-reads SKILL.md on each Skill invocation (filesystem read, no in-memory cache). Hooks, settings.json, and plugin.yaml load at session start. So:

  • SKILL.md text edit → no restart needed; agent picks up next call
  • Anything else → still need restart

Before this PR, every install issued restartFunc regardless. For Reno-Stars iterating fast on molecule-skill-five-axis-review content, that's seconds of agent-offline per docstring tweak.

Detection algorithm

  1. Hash every regular file in staged tree (host fs, sha256)
  2. Hash every regular file in live tree (in-container via docker exec sha256sum)
  3. Drop .complete marker from comparison (mtime-only churn)
  4. File added/removed → cold
  5. File content differs but isn't **/SKILL.mdcold
  6. All differences are SKILL.md basenames → skill-content-only

Tests (4 new, all green)

TestIsSkillMarkdown              — basename match, case-sensitive
TestHashLocalTree_StableHash     — re-hash same dir = identical map (no mtime leakage)
TestHashLocalTree_SymlinkSkipped — hostile symlink doesn't poison the classifier
TestShQuote                      — shell-injection safety boundary

Full handler suite green, no regressions.

Phase 4 self-review (five-axis)

Correctness: No finding — every error path defaults to cold; we never falsely classify as skill-content-only. The .complete drop is deliberate (bookkeeping, not content).

Readability: No finding — small single-purpose helpers (hashLocalTree, hashContainerTree, isSkillMarkdown, shQuote).

Architecture: No finding — composes existing execAsRoot; new helpers self-contained; old install path unchanged when classifier returns cold.

Security: No finding — shQuote single-quotes any non-trivial path; pluginName already validated; docker exec uses xargs -0 for binary-safe path delimiting. Symlinks skipped during hash walk.

Performance: No finding — adds two tree walks per install (host + one docker exec for container side). For ~10-50 file plugins, ~100ms. Versus saved ~5-10s container restart on hot-reloadable updates → clear win.

Refs

  • core#112 — this issue
  • core#114 — atomic install (.complete marker source)
## Summary Closes core#112. Composes with #114 (atomic install) — before triggering `restartFunc`, classify the diff between staged and live; skip the restart if only `**/SKILL.md` content changed. ## Why Claude Code re-reads `SKILL.md` on each Skill invocation (filesystem read, no in-memory cache). Hooks, settings.json, and plugin.yaml load at session start. So: - SKILL.md text edit → no restart needed; agent picks up next call - Anything else → still need restart Before this PR, every install issued `restartFunc` regardless. For Reno-Stars iterating fast on `molecule-skill-five-axis-review` content, that's seconds of agent-offline per docstring tweak. ## Detection algorithm 1. Hash every regular file in **staged** tree (host fs, sha256) 2. Hash every regular file in **live** tree (in-container via `docker exec sha256sum`) 3. Drop `.complete` marker from comparison (mtime-only churn) 4. File added/removed → **cold** 5. File content differs but isn't `**/SKILL.md` → **cold** 6. All differences are SKILL.md basenames → **skill-content-only** ✓ ## Tests (4 new, all green) ``` TestIsSkillMarkdown — basename match, case-sensitive TestHashLocalTree_StableHash — re-hash same dir = identical map (no mtime leakage) TestHashLocalTree_SymlinkSkipped — hostile symlink doesn't poison the classifier TestShQuote — shell-injection safety boundary ``` Full handler suite green, no regressions. ## Phase 4 self-review (five-axis) **Correctness:** No finding — every error path defaults to `cold`; we never falsely classify as skill-content-only. The `.complete` drop is deliberate (bookkeeping, not content). **Readability:** No finding — small single-purpose helpers (`hashLocalTree`, `hashContainerTree`, `isSkillMarkdown`, `shQuote`). **Architecture:** No finding — composes existing `execAsRoot`; new helpers self-contained; old install path unchanged when classifier returns cold. **Security:** No finding — `shQuote` single-quotes any non-trivial path; `pluginName` already validated; docker exec uses `xargs -0` for binary-safe path delimiting. Symlinks skipped during hash walk. **Performance:** No finding — adds two tree walks per install (host + one docker exec for container side). For ~10-50 file plugins, ~100ms. Versus saved ~5-10s container restart on hot-reloadable updates → clear win. ## Refs - core#112 — this issue - core#114 — atomic install (`.complete` marker source)
claude-ceo-assistant added 1 commit 2026-05-08 15:26:29 +00:00
feat(plugins): hot-reload classifier — skip restart on SKILL-content-only updates
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (go) (pull_request) Successful in 6s
CodeQL / Analyze (${{ matrix.language }}) (javascript-typescript) (pull_request) Successful in 6s
CodeQL / Analyze (${{ matrix.language }}) (python) (pull_request) Successful in 5s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 16s
Check merge_group trigger on required workflows / Required workflows have merge_group trigger (pull_request) Successful in 17s
branch-protection drift check / Branch protection drift (pull_request) Successful in 21s
E2E API Smoke Test / detect-changes (pull_request) Successful in 20s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 20s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 19s
Harness Replays / detect-changes (pull_request) Successful in 22s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 23s
CI / Detect changes (pull_request) Successful in 27s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 20s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 22s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 12s
CI / Canvas (Next.js) (pull_request) Successful in 10s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Python Lint & Test (pull_request) Successful in 8s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 9s
Harness Replays / Harness Replays (pull_request) Failing after 25s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m41s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3m33s
CI / Platform (Go) (pull_request) Successful in 5m11s
249e760fbd
Closes molecule-core#112. Composes with #114 (atomic install).

Before issuing restartFunc, classify the diff between staged and live:
  - skill-content-only: only **/SKILL.md content changed
                        → skip restart (Claude Code re-reads SKILL.md on
                          each Skill invocation; no in-memory cache)
  - cold: anything else
                        → restartFunc as before
                          (hooks/settings load at session start;
                          plugin.yaml is structural; added/removed files
                          require a fresh load)

DETECTION
  - Hash every regular file in staged tree (host filesystem, sha256)
  - Hash every regular file in live tree (in-container via docker exec
    sh -c 'cd <livePath> && find . -type f -print0 | xargs -0 sha256sum')
  - .complete marker dropped from comparison (mtime varies install-to-
    install; including it would force-cold every reinstall)
  - File added/removed → cold
  - File content differs but isn't SKILL.md → cold
  - All differences are SKILL.md basenames → skill-content-only

DEFAULTS COLD
  - First install (no live tree) → cold
  - Live tree read failure → cold (conservative; never hot-reload speculatively)
  - Symlinks skipped during hash (same posture as tar walker)

PHASE 4 SELF-REVIEW
  Correctness: No finding — all error paths default to cold; never
    falsely classify as skill-content-only. The .complete drop is
    a deliberate exception (the marker is bookkeeping, not content).
  Readability: No finding — single-purpose helpers (hashLocalTree,
    hashContainerTree, isSkillMarkdown, shQuote) each do one thing.
    The classifier itself reads as 'compare set, then walk diff with
    isSkillMarkdown gate.'
  Architecture: No finding — composes existing execAsRoot primitive;
    new helpers in plugins_classifier.go don't touch any other
    handler. Old behavior unchanged when live read fails.
  Security: No finding — shQuote single-quotes any non-trivial path,
    pluginName comes from validatePluginName-validated source, and
    the docker exec command takes the path as a single arg (xargs -0
    handles binary-safe path delimiting). Symlinks skipped.
  Performance: No finding — adds two tree walks (host + container)
    per install. Container walk is one docker exec call returning
    sha256 lines; for typical plugins (~10-50 files) round-trip is
    ~100ms. Versus the saved ~5-10s of restart on a hot-reloadable
    update, this is a clear win.

TESTS (4 new, all green; full handler suite green)
  TestIsSkillMarkdown        — basename match, case-sensitive
  TestHashLocalTree_StableHash — re-hash same dir = same map
  TestHashLocalTree_SymlinkSkipped — hostile link doesn't poison classifier
  TestShQuote                — quoting boundary for shell injection safety

REFS
  molecule-core#112 — this issue
  molecule-core#114 — atomic install (.complete marker added there)
  Reno-Stars iteration safety (Hongming 2026-05-08)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dev-lead approved these changes 2026-05-08 15:26:31 +00:00
dev-lead left a comment
Member

Phase-4 in PR body; conservative-on-error; tests green.

Phase-4 in PR body; conservative-on-error; tests green.
claude-ceo-assistant merged commit f78d844960 into main 2026-05-08 15:26:34 +00:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#121
No description provided.