From xh
Authoritative reference for the io.xh:hoist-core Grails/Groovy framework -- use when writing, modifying, debugging, or explaining Groovy/Java backend code that touches hoist-core, or installing/refreshing the hoist-core MCP/CLI tooling. Why this matters: hoist-core's base classes and infrastructure types have conventions that differ from plain Spring/Grails for injection, clustering, and config. Guessing produces code that compiles and runs single-node, then misbehaves under cluster failover -- invalidations that don't propagate, timers that double-fire, scheduled tasks that miss their slot. TRIGGER when the user mentions a hoist-core symbol (`BaseService`, `BaseController`, `Cache`, `Timer`, `ConfigService`, `ClusterService`, `MonitorService`, `JSONClient`); a hoist-core concept (caching, cluster-aware, distributed cache, config-driven, monitoring, scheduled task, conventions); a service/controller under `grails-app/`/`src/`; doing a backend task the framework way or using our conventions; installing/upgrading the hoist-core MCP server, CLI tools, `bin/hoist-core-*` launchers, or `installHoistCoreTools`; `hoistCoreVersion`; "hoist-core"/"io.xh.hoist"/"our framework". SKIP for plain Spring without hoist-core, Grails not importing `io.xh.hoist`, Liquibase/Flyway migrations, and build-tooling-only changes. Reach for the reference tools (MCP `mcp__hoist-core__*` or CLI `./bin/hoist-core-docs`/`./bin/hoist-core-symbols`) first.
How this skill is triggered — by the user, by Claude, or both
Slash command
/xh:using-hoist-core-referenceThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You're about to write or modify Groovy/Java backend code that consumes the `io.xh:hoist-core` framework, OR the user has asked you to install/upgrade the hoist-core developer tools in their app. Consult the reference tools before authoring - hoist-core's API surface is large, base-class semantics are easy to misremember, and a wrong guess produces code that compiles but fails at runtime. If the...
You're about to write or modify Groovy/Java backend code that consumes the io.xh:hoist-core framework, OR the user has asked you to install/upgrade the hoist-core developer tools in their app. Consult the reference tools before authoring - hoist-core's API surface is large, base-class semantics are easy to misremember, and a wrong guess produces code that compiles but fails at runtime. If the tools aren't installed yet, jump to Installing the MCP server and CLI tools.
Each workflow step has two interfaces. Use the column that matches what's in your tool context.
| Step | MCP tool | CLI command |
|---|---|---|
| Sanity check | mcp__hoist-core__hoist-core-ping | ./bin/hoist-core-docs ping |
| Search docs | mcp__hoist-core__hoist-core-search-docs | ./bin/hoist-core-docs search "<query>" |
| List docs by category | mcp__hoist-core__hoist-core-list-docs | ./bin/hoist-core-docs list --category <category> |
| Read a specific doc | mcp__hoist-core__hoist-core-read-doc | ./bin/hoist-core-docs read <docId> |
| Read the docs index | mcp__hoist-core__hoist-core-list-docs (no args) | ./bin/hoist-core-docs index |
| Read coding conventions | mcp__hoist-core__hoist-core-read-doc with id: "docs/coding-conventions.md" | ./bin/hoist-core-docs conventions |
| Search symbols / members | mcp__hoist-core__hoist-core-search-symbols | ./bin/hoist-core-symbols search "<query>" |
| Get symbol details | mcp__hoist-core__hoist-core-get-symbol | ./bin/hoist-core-symbols symbol <name> |
| List class members | mcp__hoist-core__hoist-core-get-members | ./bin/hoist-core-symbols members <name> |
If mcp__hoist-core__* tools are listed in your tool context, prefer them - they amortize the Groovy AST index across calls. The CLI launchers pay a ~2-3s cold-start hit on the first symbols invocation per process. Both surfaces share the same formatters, so output is byte-identical apart from formatting; --json on any CLI subcommand emits the same shape an MCP client would receive.
The CLI launchers are project-local (created at <project>/bin/hoist-core-* by the installHoistCoreTools Gradle task). Always invoke them as ./bin/hoist-core-... from the app project root, not npx-style.
Skip this if you're working through MCP only. For the CLI surface, before the first ./bin/hoist-core-* call in a session, verify the launchers are present and functional:
Check that ./bin/hoist-core-docs, ./bin/hoist-core-symbols, and ./bin/hoist-core-mcp exist at the project root.
Sanity-check one of them:
./bin/hoist-core-docs ping
Expect: hoist-core CLI is running.
Snippet-currency check (low-cost, informational). Grep build.gradle for the install snippet's drift marker:
grep -E '^// hoist-ai-snippet: hoist-core-install/v[0-9]+' build.gradle
The current canonical version is v1. Three possible outcomes:
v1: snippet is current. No action.v0): snippet has drifted from the canonical. Jump to Refreshing a stale install snippet and offer to update.If any launcher file is missing, jump to Installing the MCP server and CLI tools and run the full install procedure.
If the files exist but ping fails (most common cause: ./gradlew clean wiped the JAR the absolute path in each launcher points at, but the launcher itself was retained on disk), re-run only ./gradlew installHoistCoreTools — it's idempotent and re-resolves the JAR. No need to re-edit build.gradle or .mcp.json.
Briefly mention the refresh in your next user-facing message (e.g. "refreshed hoist-core launchers after the JAR was cleaned").
The preflight runs once per session — once you've confirmed (or fixed) the launchers, you don't need to re-check on every CLI call. For the snippet-currency check specifically: surface it once per session. If the user defers the refresh, do not re-prompt during the session.
Standard sequence for any hoist-core authoring task:
mcp__hoist-core__hoist-core-list-docs (or ./bin/hoist-core-docs index) when you're new to the area. Find the right doc.get-symbol for full details, or get-members to list a class's properties and methods with types and annotations.filePath when symbol names collide.Member-indexed classes (those whose public members are individually searchable): BaseService, BaseController, RestController, HoistUser, Cache, CachedValue, Timer, Filter, FieldFilter, CompoundFilter, JSONClient, ClusterService, ConfigService, MonitorResult, LogSupport, HttpException, IdentitySupport. Use search-symbols with a member name (e.g. getOrCreate, expireTime) to find which of these classes provides it.
Look up the methods on BaseService.
mcp__hoist-core__hoist-core-get-members with name: "BaseService"../bin/hoist-core-symbols members BaseServiceFind which class provides a method like getOrCreate.
mcp__hoist-core__hoist-core-search-symbols with query: "getOrCreate" - returns matching symbols and matching members with their owning class../bin/hoist-core-symbols search getOrCreateFind the convention for something cross-cutting (caching, monitoring, cluster).
mcp__hoist-core__hoist-core-search-docs with query: "cache cluster" (multi-word AND match)../bin/hoist-core-docs search "cache cluster"Read the docs index.
mcp__hoist-core__hoist-core-list-docs (with no category param)../bin/hoist-core-docs indexLook up an annotation or trait by name.
mcp__hoist-core__hoist-core-get-symbol with name: "<TraitName>". Use kind: trait on search-symbols to filter../bin/hoist-core-symbols search "<TraitName>" --kind trait then ./bin/hoist-core-symbols symbol <TraitName>.Cache, CachedValue, and Timer. Search for them before writing your own.BaseController provides renderJSON, renderJSONP, exception handling, and identity support. Look it up via get-members instead of using raw Grails render.ClusterService with conventions for monitoring, replication, and identity. Use it through that wrapper.Cache configurations are local; some are distributed. Read the Cache docs and use the right form.Trigger this section when:
mcp__hoist-core__* tools nor the ./bin/hoist-core-* launchers are present.hoistCoreVersion and the existing bin/ launchers are now stale (they're version-locked to the JAR they wrap).Both surfaces ship from the same fat JAR (io.xh:hoist-core-mcp:<version>:all). One Gradle task installs project-local launchers for both.
build.gradle exists at the project root, hoist-core is a dependency (direct or transitively via a client plugin). The install snippet resolves the active hoist-core version from the runtime classpath, so it works regardless of how the dependency is declared.hoistCoreVersion resolves to a release that includes the install task and bundled-content fat JAR. Floor: v39.0 (first release containing the bundled JAR + CLI subsystem). For pre-release work against develop, use 39.0-SNAPSHOT from a Sonatype snapshot repo or mavenLocal() (after ./gradlew :mcp:publishToMavenLocal in a hoist-core checkout).If hoistCoreVersion is below the floor, stop and recommend /xh:hoist-upgrade first. Do not attempt to back-port the install task to older versions.
Read the canonical install snippet. Either:
mcp__hoist-core__hoist-core-read-doc with id: "mcp/README.md" and look for the App-Side Distribution section, OR./bin/hoist-core-docs read mcp/README.md (only if the launchers are already present from a prior install — typical when upgrading), ORmcp/README.md directly from a sibling hoist-core checkout if available.The README is the source of truth. The snippet below is a current-as-of-v39.0 mirror; if it diverges from what the README shows, trust the README.
Add the install snippet to the app's build.gradle. Insert at the top level (outside any subproject block):
// hoist-ai-snippet: hoist-core-install/v1 -- DO NOT REMOVE (drift marker, see hoist-core/mcp/README.md)
configurations {
hoistCoreCli
}
dependencies {
// Resolve the active hoist-core version lazily from the runtime classpath. Works whether
// hoist-core is declared directly, via gradle.properties, or pulled in transitively via a
// client plugin -- the install task stays aligned with whatever hoist-core version actually
// resolves at build time.
hoistCoreCli providers.provider {
def hc = configurations.runtimeClasspath.incoming.resolutionResult.allComponents
.find { it.moduleVersion?.group == 'io.xh' && it.moduleVersion?.name == 'hoist-core' }
if (!hc) throw new GradleException(
'hoist-core not found on the runtimeClasspath -- cannot install hoist-core tools.')
"io.xh:hoist-core-mcp:${hc.moduleVersion.version}:all@jar"
}
}
tasks.register('installHoistCoreTools', Sync) {
description = 'Install version-locked launchers for the hoist-core MCP server and CLI tools.'
group = 'hoist'
from configurations.hoistCoreCli
into layout.buildDirectory.dir('hoist-core-tools/lib')
doLast {
def jar = fileTree(layout.buildDirectory.dir('hoist-core-tools/lib')).singleFile
def binDir = file('bin')
binDir.mkdirs()
def launcherNames = []
['mcp', 'docs', 'symbols'].each { topic ->
// mcp mode: pass --source bundled so the server reads JAR-embedded content. Without
// this, HoistCoreMcpServer defaults to local mode and fails when the app
// has no hoist-core checkout sibling.
// docs/symbols: dispatched via `cli`, which routes to picocli (defaults to bundled).
def args = topic == 'mcp' ? '--source bundled' : "cli ${topic}"
// The bash launcher resolves `java` from PATH, falling back to JAVA_HOME, so it
// works under MCP clients launched from non-interactive shells where a JDK version
// manager (mise, asdf, jenv) may not have activated.
new File(binDir, "hoist-core-${topic}").with {
text = """\
#!/usr/bin/env bash if command -v java >/dev/null 2>&1; then JAVA=java elif [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/java" ]; then JAVA="$JAVA_HOME/bin/java" else cat >&2 <<'EOF' [hoist-core] ERROR: 'java' not on PATH and JAVA_HOME is unset or invalid.
If you use a JDK version manager (mise, asdf, jenv), it likely activates only in interactive shells - but this script runs in the non-interactive shell launched by your MCP client. Either export JAVA_HOME from a non-interactive init file (e.g. ~/.bash_profile, ~/.zshenv) so it is visible here, or activate the version manager from one of those files. Pointing JAVA_HOME at any JDK 17+ install is sufficient. EOF exit 1 fi exec "$JAVA" -jar "${jar.absolutePath}" ${args} "$@" """ setExecutable(true) } new File(binDir, "hoist-core-${topic}.bat").text = "@echo off\r\njava -jar "${jar.absolutePath}" ${args} %*\r\n" launcherNames << "hoist-core-${topic}" launcherNames << "hoist-core-${topic}.bat" } // Generated launchers embed an absolute path to the resolved JAR, so they are // machine-specific and must not be committed. Write a scoped bin/.gitignore that // excludes only the generated launchers, leaving the rest of bin/ untouched. new File(binDir, '.gitignore').text = launcherNames.sort().join('\n') + '\n' } } // end hoist-ai-snippet ```
Run the install task.
./gradlew installHoistCoreTools
Expect the task to download (or resolve from cache) the fat JAR into build/hoist-core-tools/lib/ and write six launchers under bin/: three POSIX (hoist-core-mcp, hoist-core-docs, hoist-core-symbols) and three Windows .bat mirrors.
Sanity-check the CLI immediately. This is fast (no Claude restart needed) and catches snapshot-resolution / Java-version issues:
./bin/hoist-core-docs ping
./bin/hoist-core-symbols members BaseService
The first symbols invocation pays a ~2-3s AST cold-start. If ping doesn't return cleanly, surface the error to the user before proceeding.
Wire up .mcp.json. The MCP server now launches via the local launcher (./bin/hoist-core-mcp), not via a bootstrap.sh or any version-suffixed JAR path. Edit (or create) .mcp.json at the project root:
{
"mcpServers": {
"hoist-core": {
"command": "./bin/hoist-core-mcp"
}
}
}
If .mcp.json already exists, preserve other entries (e.g. hoist-react) and only update or add the hoist-core entry. Replace any prior start-hoist-core-mcp.sh or mcp/bootstrap.sh command with the new launcher path.
Commit bin/.gitignore. The install task writes a scoped bin/.gitignore excluding only the generated launchers (each launcher embeds an absolute path to the resolved JAR and is therefore machine-specific -- a committed launcher silently breaks for every other developer). Commit bin/.gitignore once; subsequent regenerations stay out of source control automatically. The rest of bin/ (if the project uses it for other things) is untouched. The build/hoist-core-tools/ directory is normally already covered by the project's top-level build/ ignore.
Tell the user to restart Claude Code so it picks up the new .mcp.json entry. Until restart, only the CLI surface will be available.
./bin/hoist-core-docs ping returns cleanly. ./bin/hoist-core-symbols members BaseService lists BaseService's public methods/properties.mcp__hoist-core__hoist-core-ping returns cleanly. mcp__hoist-core__hoist-core-list-docs returns the doc registry.If both surfaces work, the routing table at the top of this skill is now usable in either column.
When hoistCoreVersion is bumped, re-run ./gradlew installHoistCoreTools to refresh the launchers (they embed an absolute path to a version-suffixed JAR; stale launchers point at a deleted JAR and will fail at exec time). The snippet in build.gradle rarely needs to change on a hoistCoreVersion bump -- the Provider resolves the new version automatically. The snippet itself is versioned independently via its hoist-ai-snippet: marker; see Refreshing a stale install snippet for that separate concern.
Triggered when the preflight's snippet-currency check finds a missing or outdated // hoist-ai-snippet: hoist-core-install/v<N> marker in build.gradle -- the working launchers will keep working, but the snippet has been improved since the user last pasted it (e.g. updated Gradle APIs, improved launcher script, new install-task behavior).
This is an in-place edit of the user's source-controlled build.gradle, not a regenerated file copy. Be careful:
Read the current canonical the same way as a fresh install -- step 1 of the install procedure above (mcp__hoist-core__hoist-core-read-doc with id: "mcp/README.md", or ./bin/hoist-core-docs read mcp/README.md, or a sibling hoist-core checkout). Trust the README's snippet over the mirror in this SKILL.md if they diverge -- but they shouldn't if both repos have been kept in lockstep.
Locate the existing snippet boundaries in build.gradle:
grep -nE '^// (hoist-ai-snippet|end hoist-ai-snippet)' build.gradle
If both an opening and closing marker are present, you have a clean bounded block. If only the opening is present, find the end manually -- the } that closes tasks.register('installHoistCoreTools', Sync). If neither is present (pre-stamp install), find the install fragments by their well-known identifiers: the configurations { hoistCoreCli } block, the hoistCoreCli ... line inside dependencies { ... }, and the tasks.register('installHoistCoreTools', ...) block.
Detect hand-edits. Compare the in-place snippet against the canonical. Common harmless local modifications: tweaked description = ..., added logging, additional launcher topics, custom into location, extra .gitignore entries. Preserve these in the refresh. If you see edits you cannot explain as harmless, stop and ask the user before editing.
Present the diff to the user and get explicit confirmation before applying. Frame it as: "Your install snippet is at ; current canonical is . The diff below shows what would change. Proceed?"
Apply the refresh via Edit -- replace the bounded block byte-for-byte with the canonical (merging in any preserved hand-edits). If the snippet was unstamped, add the markers as part of the refresh.
Re-run the install task to regenerate launchers with the new logic:
./gradlew installHoistCoreTools
Verify as in the standard install procedure -- ./bin/hoist-core-docs ping should still return cleanly.
If the user defers the refresh, do not re-prompt during the session. They can re-trigger by asking for the install task to be re-run or by starting a new session.
If neither MCP tools (mcp__hoist-core__*) nor the CLI launchers (./bin/hoist-core-*) are present in your context:
hoistCoreVersion < 39.0 and they don't want to upgrade right now, do not improvise hoist-core APIs from training data - class names and member signatures evolve, and stale guesses produce real bugs. Tell the user the limitation explicitly.Read to consult its docs/ or source. Surface this as a workaround, not a steady state.npx claudepluginhub xh/hoist-ai --plugin xhProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.