Files
FabledGrimoire/plugin/hooks/grimoire_sync_processes.sh
T
bvandeusenandClaude Opus 5.5 c5eeec471f
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m2s
CI & Build / Python tests (push) Failing after 1m26s
CI & Build / Build & push image (push) Skipped
rename(547): the plugin is grimoire — manifest, MCP key, hooks, env, skills
Step 3 of the Scribe -> Grimoire rename.

- Plugin `grimoire` in marketplace `grimoire-plugin`, MCP server key
  `grimoire`, so tools reach a session as mcp__plugin_grimoire_grimoire__*.
- hooks/scribe_* -> hooks/grimoire_*, their shell functions and the TMPDIR
  ledgers with them; SCRIBE_URL / SCRIBE_TOKEN / SCRIBE_HOOK_DIR /
  SCRIBE_LEDGER_DIRS -> GRIMOIRE_*; the project marker is `.grimoire`.
- skills/using-scribe -> using-grimoire, /grimoire:sync, stored processes
  arrive as grimoire-proc-<slug>; moment_actions names them the same way.
- Migration 0124 rewrites stored skill names (rule_moments,
  rule_moment_judgments, moment_mappings) from scribe:/scribe-proc- to
  grimoire:/grimoire-proc-, so a rule mounted on a skill keeps firing.
- Settings' install commands and MCP-only defaults, the docs and the
  tests follow.
- scripts/check_plugin.py drops the smoke event for a report-check hook
  that has no file in plugin/hooks.

Kept on purpose: the secret-sealing label, and the names that let backups
written as Scribe restore and be counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:52:55 -04:00

86 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Grimoire plugin — sync stored Processes into auto-surfacing local skills.
#
# Fetches `GET /api/plugin/processes` and writes one
# ~/.claude/skills/grimoire-proc-<slug>/SKILL.md
# per Process. The stub's frontmatter `description` is the auto-surface trigger;
# its body tells Claude to call get_process(<name>) and follow the LIVE procedure
# from Grimoire (the DB stays the single source of truth — the stub is a pointer).
#
# WHY LOCAL ~/.claude/skills (not the plugin dir): the plugin is git-cloned and
# identical on every install, so instance-specific stubs can't live inside it.
# Personal skills in ~/.claude/skills are live-detected by Claude Code within the
# session, so a freshly written stub auto-surfaces without a restart.
#
# TRIGGERS: this runs at SessionStart (alongside the context hook) so stubs stay
# fresh each session, and on demand via the `/grimoire:sync` command.
#
# FAIL-OPEN & SILENT: never blocks a session; emits NOTHING on stdout (so it's
# safe as a second SessionStart hook). On any fetch failure it exits without
# touching existing stubs — a transient outage must not wipe the user's skills.
# Config mirrors the context hook: CLAUDE_PLUGIN_OPTION_API_ENDPOINT /
# CLAUDE_PLUGIN_OPTION_API_TOKEN (userConfig key UPPERCASED by Claude Code),
# with GRIMOIRE_URL / GRIMOIRE_TOKEN as the override.
set -uo pipefail
# shellcheck source=plugin/hooks/grimoire_defs.sh
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
command -v curl >/dev/null 2>&1 || exit 0
grimoire_config || exit 0
body=$(curl -fsS --max-time 8 \
-H "Authorization: Bearer ${token}" \
"${url%/}/api/plugin/processes" 2>/dev/null) || exit 0
[ -n "$body" ] || exit 0
body_flat=$(printf '%s' "$body" | grimoire_json_flat)
# The array's LENGTH, which is "" when `.processes` is not an array at all —
# a different answer from 0, and the one that means the response was not what
# this hook expects.
count=$(grimoire_json_len "$body_flat" '.processes')
case "$count" in ''|*[!0-9]*) exit 0 ;; esac
skills_dir="${HOME}/.claude/skills"
mkdir -p "$skills_dir" 2>/dev/null || exit 0
# Slugs written this run — anything else under grimoire-proc-* is pruned below.
managed=" "
i=0
while [ "$i" -lt "$count" ]; do
name=$(grimoire_json_pick "$body_flat" ".processes[$i].name")
slug=$(grimoire_json_pick "$body_flat" ".processes[$i].slug")
desc=$(grimoire_json_pick "$body_flat" ".processes[$i].description")
i=$((i + 1))
[ -n "$slug" ] && [ -n "$name" ] || continue
dir="${skills_dir}/grimoire-proc-${slug}"
mkdir -p "$dir" 2>/dev/null || continue
# description folded to one line — YAML scalar must not contain a newline.
desc=$(printf '%s' "$desc" | tr '\n' ' ')
{
printf -- '---\n'
printf 'name: grimoire-proc-%s\n' "$slug"
printf 'description: %s\n' "$desc"
printf -- '---\n\n'
printf '<!-- GENERATED by the Grimoire plugin (grimoire_sync_processes.sh). Do not edit here; edit the Process in Grimoire and re-sync with /grimoire:sync. -->\n\n'
printf 'This is a saved **Grimoire Process**: `%s`.\n\n' "$name"
printf 'Do not improvise it. Call `get_process("%s")` via the Grimoire MCP server to fetch the live procedure, then follow the returned body verbatim — including any "clarify first" steps it contains.\n' "$name"
} > "${dir}/SKILL.md" 2>/dev/null || continue
managed="${managed}${slug} "
done
# Prune stubs whose Process no longer exists (scoped to our grimoire-proc-* prefix).
for d in "${skills_dir}"/grimoire-proc-*; do
[ -d "$d" ] || continue
slug=$(basename "$d"); slug=${slug#grimoire-proc-}
case "$managed" in
*" $slug "*) : ;;
*) rm -rf "$d" 2>/dev/null ;;
esac
done
exit 0