Orchestrata Ultimate › Gates › plugin-drift
Gate: plugin-drift
At a glance
| Purpose | Refuse a plugins/orchestrata mirror that differs from what claude/scripts/gen_plugin.py derives from its source trees. |
| Where it runs | The scripts/gates.py battery (registered as plugin-drift); the "Plugin currency" process gate runs the same command. |
| Trigger | A battery run whose fingerprint changed, or whose last cached verdict was not ok. |
| Exact command | python claude/scripts/gen_plugin.py --check |
| Blocks | Exit 1 on drift, 0 when the mirror is current. |
| Ruling / ADR | ADR-0092 — the plugin is self-contained, pinned by a tag, installed by one verifying command; hook placement ADR-0070 / RUL-035. |
| Last recorded verdict | ok: false in the primary checkout's .cache/gates.json (file last written 2026-09-22 10:01 local; the cause is not recorded there). wt-MAP-PRECOMMIT has no .cache/gates.json. |
What it refuses
A hook, command, skill, agent, tool, or infra file added to (or removed from) the
claude/ source trees without regenerating the plugins/orchestrata plugin — a
stale delivery path shipping silently.
Why it exists
From claude/scripts/gen_plugin.py's docstring:
"Nobody hand-places a file into
plugins/orchestrata/— ever. Everything the plugin ships is DERIVED, on every run, from theclaude/tree that is already the source of truth for the deployed (non-plugin) install… There is deliberately no hand-maintained list of individual files anywhere in this module — every such list in this repo has drifted from the thing it described, which is the entire reason this script exists."
And from the gates.py registry comment:
"This gate is what makes that generation ENFORCED rather than available: a hook, command, skill, agent, or tool added to claude/ without regenerating fails the battery instead of silently shipping a stale plugin. Same failure class as settings.hooks.json (declared, read by nothing, drifted for months)."
The plugin also re-emits the repo's own .claude/settings.json guards, because
"in a session launched anywhere else all 25 of them silently did not exist (measured
2026-08-23)" — and that registration list is "DERIVED, never retyped."
How it runs
- Commands:
python claude/scripts/gen_plugin.py --check # report drift, write nothing; exit 1 if stale python claude/scripts/gen_plugin.py # regenerate the plugin python claude/scripts/gen_plugin.py --root DIR # write under DIR (testing) - When it fires: the
scripts/gates.pybattery (registered asplugin-drift); also the "Plugin currency" process gate — the identical mechanism invoked as a workflow checkpoint. - What generation covers: the plugin manifest (from
pyproject.toml),hooks/hooks.json(fromclaude/settings.hooks.json, with every home-path script rewritten to"${CLAUDE_PLUGIN_ROOT}"— "Every declared hook command is preserved; none is invented or dropped"), the marketplace entry, and everyCOMPONENTSsource→dest tree ("A file added under a component's source and not yet mirrored, or a file left behind in the plugin whose source is gone, is DRIFT"). - Inputs: fingerprint roots
("claude", "plugins", ".claude-plugin", "pyproject.toml")(theGATESentry inscripts/gates.py, read through the AST toolkit on 2026-10-23). An earlier version of this page listed three more roots (.claude,deploy/systemd,infra) said to have been added by a 2026-08-28 audit; the registry does not carry them, and a pickaxe search for that root list inscripts/gates.pyhistory finds no commit. The generator'sCOMPONENTStable still reads from.claude/hooks(project-hooks),deploy/systemd(systemd) andinfra/(cloud-init and terraform), so those sources are outside the fingerprint. - Caching — two blind spots, derived from the code and not measured by a run.
_fingerprint()hashes only files whose suffix is.py,.toml,.pl,.json,.mdor.sh(_interesting()); there is no per-gate suffix mask (this page previously described an empty one;git log --all -S GATE_MASKS -- scripts/gates.pyfinds no commit that ever contained aGATE_MASKStable). The generator compares every file byte for byte whatever its suffix. So after a cached PASS: (1) an edit under.claude/hooks,deploy/systemdorinfra/does not re-run the gate; (2) an edit to a file the suffix filter skips does not either —claude/holds 5.j2files and 2.envrcfiles, and the component sources outside the roots hold.service(13),.timer(4),.tf(16),.hcl(3),.tftpl(2) and.yaml/.yml(4) files (counted withfindonwt-MAP-PRECOMMIT, 2026-10-23).
Typical failures and how to fix them
- Exit code:
--checkexits 1 on drift, 0 when the mirror is current. - What a failure looks like (representative): per component, the files missing from the mirror and the files orphaned in it (source gone).
- Un-blocking: run the generator (
python claude/scripts/gen_plugin.py) and commit the regenerated plugin — never hand-editplugins/orchestrata/. - A unit, terraform or project-hook edit that the battery reports as unchanged.
Because of the blind spots above, run the gate fresh after touching
.claude/hooks,deploy/systemd,infra/or a.j2/.envrcfile:python scripts/gates.py --only plugin-drift --all, or the--checkcommand directly. - Last recorded state: the primary checkout's cache records
ok: false(2026-09-22); the drift it found is not stored there.
Wiring
- Invokers:
scripts/gates.py(battery); the process-gates family ("Plugin currency", complete-picture table row 12). - Consumers of the output: every Claude Code session outside this repo loads the
plugin — the
_dispatch.shdelivery story on the session-hooks family page. - Tests:
tests/claude/test_gen_plugin.py, plustests/claude/test_plugin_path_independence.pyfor path independence of the delivered hooks/tools.
Related
- Gates master index
- Development workflow
- ADR-0092
- ADR-0070 and RUL-035 — hooks at user level only
- Process gates family — the "Plugin currency" checkpoint
- wiring — registered here, mirrored there: W1 refuses the unregistered hook, this refuses the unmirrored one
- coverage-claude — "the RATCHET half of the same 'claude/ is production, not scaffolding' ruling"
- lexicon-drift — the same generated-artifact-currency doctrine in miniature