BOHICA-LABS / BOHICA-LABS/vsdd-factory
feat(activate): version-drift guard — block factory commands after a plugin update until re-activation
- Dominant language
- Rust
- Stars
- 2
- Forks
- 1
- Avg merge
- 6h 43m
- Merged PRs (30d)
- 29
Description
## Summary
After the plugin auto-updates to a new version, nothing stops the operator from running factory commands against a project that was **activated under the old version**. The on-disk binaries, `hooks.json`, templates, and state contracts are now the new version's, but `.claude/settings.local.json` still records the old `activated_plugin_version`, and the project's `.factory/` state was initialized under the old version.
Proposal: a **version-drift guard** that fires before the first factory command runs, compares the currently-installed plugin version against the recorded `activated_plugin_version`, and — when they differ in a way that matters — **blocks the command and instructs the user to run `/vsdd-factory:activate`** before any factory command proceeds.
## What already exists (this is half-built)
- **`activate` already records the version.** `skills/activate/SKILL.md` step 5 writes:
```json
"vsdd-factory": {
"activated_platform": "",
"activated_at": "",
"activated_plugin_version": ""
}
```
- **`activate` already does a *platform* drift check** (step 4) — but only on re-activation, and only for platform, never for plugin version.
- **The enforcement primitive exists.** PreToolUse hooks are dispatched through the `factory-dispatcher` binary, matched by tool, with `on_error = "block"` — so a hook can hard-block a command (e.g. `block-ai-attribution`, `check-factory-commit`, `destructive-command-guard`).
- **Both version sources are readable by a hook:**
- Current installed version → `${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json` (`"version"`).
- Activated version → `.claude/settings.local.json` (`vsdd-factory.activated_plugin_version`). The existing `session-start-telemetry` hook is already granted `read_file` on `.claude/settings.local.json`, so reading it from a hook is established.
## The gap
**No hook compares the running plugin version against `activated_plugin_version` and blocks.** The only drift check today is platform, and only during re-activation (reactive, not a pre-command guard). So a project activated under `1.0.0-rc.18` will silently run factory commands under `1.0.0-rc.20` binaries/hooks/templates.
## Critical design wrinkle — the guard cannot depend on activation-applied state
`activate` copies a **per-platform `hooks.json` (gitignored, generated at activation time)** and verifies a **version-specific dispatcher binary** into place (steps 6). This creates a chicken-and-egg for the guard:
- If the guard is wired only through the activation-applied `hooks/hooks.json`, then immediately after an update — before re-activation — the new version's hooks may not be in place, so the guard that is supposed to catch "you haven't re-activated" might not fire.
Therefore the guard likely needs a **committed / always-present entry point**, not one that only exists post-activation. Recommended shape:
- **SessionStart hook (early warning):** on a fresh session, detect drift and print a prominent notice ("plugin updated rc.18 → rc.20; run `/vsdd-factory:activate` to re-sync").
- **PreToolUse hook (hard block):** before any factory command/tool runs, block with `on_error = "block"` until re-activation clears the drift.
> **Open question for the maintainer:** what is the exact post-update hook-boot behavior? Is `hooks/hooks.json` regenerated/shipped for a freshly-installed version, or only written at activation? The answer determines where the guard must live to be guaranteed present after an update.
## Gating heuristic — don't block on every delta
External research (Perplexity `sonar-reasoning`) note: *its cited sources came back irrelevant, so the following is general best practice, not source-backed.*
- vsdd-factory is at **`1.0.0-rc.x`** — pre-release versions carry no compatibility guarantee, so **treat rc→rc bumps as potentially breaking and block by default** for now.
- But hard-blocking on *every* patch will wedge users. Introduce a **compatibility contract separate from the marketing version**, e.g. in `plugin.json`:
```json
{
"version": "1.0.0-rc.20",
"state_schema_version": "2.0",
"min_compatible_activation_version": "1.0.0-rc.18"
}
```
The fast pre-command hook compares **`state_schema_version` / `min_compatible_activation_version`**, not the raw version — so trivial bumps pass and only contract-affecting changes block. (This mirrors how DB migration tools, Terraform state versions, and Kubernetes API versions separate schema/compat version from product version.)
- **Decision rule:** block if `activated_plugin_version < min_compatible_activation_version`, OR the recorded state schema version ≠ the current plugin's `state_schema_version`, OR (interim, while pre-1.0) any version string mismatch with `state_schema_version` unset.
## Block UX
When blocking, the message should include:
- The two versions (activated vs current).
- A one-line reason ("hooks/binaries/state contract changed").
- The fix: `Run /vsdd-factory:activate to re-sync this project to rc.20, then retry.`
- An escape hatch for stale-guard / emergency recovery (e.g. an explicit `--force`/override or deactivate→reactivate), logged.
## Re-activation as the fix path
`activate` is already the natural remediation — it re-copies the correct per-platform `hooks.json`, re-verifies the dispatcher binary, and rewrites `activated_plugin_version`. Extend it so re-activation is an **idempotent re-sync**, plus optional **forward migrations** for persisted `.factory/` state that can't simply be regenerated (versioned, transactional, resumable). A `--migrate`/dry-run option would let operators preview.
## Acceptance criteria (draft)
- [ ] After a plugin update, the first factory command in a project activated under an incompatible older version is **blocked** with an actionable message pointing to `/vsdd-factory:activate`.
- [ ] The guard is present and effective **even before re-activation** (does not rely solely on activation-applied `hooks.json`).
- [ ] Blocking is governed by a compatibility contract (`state_schema_version` / `min_compatible_activation_version`), not a raw equality check — trivial patches don't wedge the operator.
- [ ] A SessionStart notice warns about drift early; a PreToolUse guard enforces the hard block.
- [ ] `/vsdd-factory:activate` clears the drift idempotently (re-sync + version rewrite), and is the documented fix.
- [ ] An override/escape hatch exists for stale-guard recovery and is logged.
- [ ] Single-version steady-state usage (no update) incurs zero new friction.
## Open questions
- Post-update hook-boot behavior (above) — where must the guard live to be guaranteed present?
- Should the guard block **all** tools or only factory skills/commands (and how to scope "factory command" in a PreToolUse matcher)?
- Should re-activation auto-run forward migrations, or only re-sync and leave state migration to an explicit `--migrate`?
- For multi-repo projects (`.factory-project/`), is the guard per-repo, per-project, or both?
## Relationships
- Composes with #170 (single-writer factory lock/lease) — both touch the activation/state-guard surface and the "is this project safe to operate right now?" pre-command check.
- Related to the broader observation that the plugin ships version-specific compiled artifacts (per-platform `hooks.json` + dispatcher binary), which makes activation-version coherence load-bearing.
---
*Filed after tracing the `activate` flow and hook/dispatcher wiring in `vsdd-factory@1.0.0-rc.20`, with the gating design validated against general version-migration practice. Happy to prototype the SessionStart warning + PreToolUse block + `min_compatible_activation_version` contract.*
Contributor guide
Assessment
This issue has not been assessed yet.