rename(547): the plugin is grimoire — manifest, MCP key, hooks, env, skills
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
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
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>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "scribe",
|
||||
"description": "Grimoire for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Grimoire skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting, family-canon), and syncs your saved Grimoire Processes as skills (/scribe:sync).",
|
||||
"name": "grimoire",
|
||||
"description": "Grimoire for Claude Code: connects the grimoire MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Grimoire skills (using-grimoire, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting, family-canon), and syncs your saved Grimoire Processes as skills (/grimoire:sync).",
|
||||
"version": "2026.10.21.2344",
|
||||
"author": {
|
||||
"name": "Bryan Van Deusen"
|
||||
},
|
||||
"mcpServers": {
|
||||
"scribe": {
|
||||
"grimoire": {
|
||||
"type": "http",
|
||||
"url": "${user_config.api_endpoint}/mcp",
|
||||
"headers": {
|
||||
@@ -18,7 +18,7 @@
|
||||
"api_endpoint": {
|
||||
"type": "string",
|
||||
"title": "Grimoire base URL",
|
||||
"description": "Base URL of your Grimoire instance, no trailing slash (e.g. https://scribe.example.com)"
|
||||
"description": "Base URL of your Grimoire instance, no trailing slash (e.g. https://grimoire.example.com)"
|
||||
},
|
||||
"api_token": {
|
||||
"type": "string",
|
||||
|
||||
+18
-18
@@ -31,27 +31,27 @@ another one means adding files, not moving or rewriting any.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `.claude-plugin/plugin.json` | Manifest: `mcpServers.scribe` (HTTP, `Authorization: Bearer ${user_config.api_token}`), `userConfig` for the base URL and key, version (minted, never hand-edited; see README). |
|
||||
| `.claude-plugin/plugin.json` | Manifest: `mcpServers.grimoire` (HTTP, `Authorization: Bearer ${user_config.api_token}`), `userConfig` for the base URL and key, version (minted, never hand-edited; see README). |
|
||||
| `../.claude-plugin/marketplace.json` (repo root) | The marketplace entry pointing at `./plugin`. |
|
||||
| `hooks/hooks.json` | Wires the scripts to Claude Code events. |
|
||||
| `hooks/scribe_session_context.sh` | SessionStart: adapter static text + running version + `GET /api/plugin/context`; a reload banner after compaction. |
|
||||
| `hooks/scribe_autoinject.sh` | UserPromptSubmit: `GET /api/plugin/retrieve` for the message. |
|
||||
| `hooks/scribe_prior_art.sh` | PreToolUse on editor writes: `GET /api/plugin/prior-art`. |
|
||||
| `hooks/scribe_after_write.sh` | PostToolUse on shell commands: the same check for code written through the shell. |
|
||||
| `hooks/scribe_tool_rules.sh` | PreToolUse on shell commands: `GET /api/plugin/tool-rules`. |
|
||||
| `hooks/scribe_moment.sh` | PreToolUse on every tool: the rules mounted on the moments the call reaches, `POST /api/plugin/moment`; skips tools `GET /api/plugin/moment-tools` says reach nothing mounted, and Grimoire's own (their responses carry `moment_rules`). |
|
||||
| `hooks/scribe_reply_check.sh` | Stop: sends the finished reply to `POST /api/plugin/reply-rules` — the rules mounted on the reply moments, and the reply against every rule's trigger, and — when the turn closed a task — the completion-section check; holds once per rule, with the reason the server returns, and never holds the rewrite. |
|
||||
| `hooks/scribe_shape_check.sh` | Stop: sends the definitions the turn wrote (the write hooks' `<sid>.written.ids` ledger) to `GET /api/plugin/shape-check`; blocks once, with the reason the server returns, so the agent judges what it built. |
|
||||
| `hooks/scribe_record_opened.sh` | PostToolUse on `get_rule`: notes the rule as read in the session ledger and `POST /api/plugin/rule-opened`. |
|
||||
| `hooks/scribe_record_outcome.sh` | PostToolUse on `rule_outcome`: notes the rule as acted on, locally, for the session-scoped readout. |
|
||||
| `hooks/scribe_claim_session.sh` | PostToolUse on `update_task` / `add_task_log`: `GET /api/plugin/claim-session`, so the task comes back after a compaction. |
|
||||
| `hooks/scribe_precompact_preserve.sh` | PreCompact: tells the summarizer what to keep literally. Local, and never blocks. |
|
||||
| `hooks/scribe_session_end.sh` | SessionEnd: `GET /api/plugin/release-session` releases the session's claims. |
|
||||
| `hooks/scribe_sync_processes.sh` + `commands/sync.md` | `GET /api/plugin/processes` → `~/.claude/skills/scribe-proc-*` stubs; `/scribe:sync` on demand. |
|
||||
| `hooks/scribe_defs.sh` | Shared shell helpers: config, dedup ledgers, outage line. |
|
||||
| `hooks/scribe_static_context.md` | The adapter static text. |
|
||||
| `hooks/grimoire_session_context.sh` | SessionStart: adapter static text + running version + `GET /api/plugin/context`; a reload banner after compaction. |
|
||||
| `hooks/grimoire_autoinject.sh` | UserPromptSubmit: `GET /api/plugin/retrieve` for the message. |
|
||||
| `hooks/grimoire_prior_art.sh` | PreToolUse on editor writes: `GET /api/plugin/prior-art`. |
|
||||
| `hooks/grimoire_after_write.sh` | PostToolUse on shell commands: the same check for code written through the shell. |
|
||||
| `hooks/grimoire_tool_rules.sh` | PreToolUse on shell commands: `GET /api/plugin/tool-rules`. |
|
||||
| `hooks/grimoire_moment.sh` | PreToolUse on every tool: the rules mounted on the moments the call reaches, `POST /api/plugin/moment`; skips tools `GET /api/plugin/moment-tools` says reach nothing mounted, and Grimoire's own (their responses carry `moment_rules`). |
|
||||
| `hooks/grimoire_reply_check.sh` | Stop: sends the finished reply to `POST /api/plugin/reply-rules` — the rules mounted on the reply moments, and the reply against every rule's trigger, and — when the turn closed a task — the completion-section check; holds once per rule, with the reason the server returns, and never holds the rewrite. |
|
||||
| `hooks/grimoire_shape_check.sh` | Stop: sends the definitions the turn wrote (the write hooks' `<sid>.written.ids` ledger) to `GET /api/plugin/shape-check`; blocks once, with the reason the server returns, so the agent judges what it built. |
|
||||
| `hooks/grimoire_record_opened.sh` | PostToolUse on `get_rule`: notes the rule as read in the session ledger and `POST /api/plugin/rule-opened`. |
|
||||
| `hooks/grimoire_record_outcome.sh` | PostToolUse on `rule_outcome`: notes the rule as acted on, locally, for the session-scoped readout. |
|
||||
| `hooks/grimoire_claim_session.sh` | PostToolUse on `update_task` / `add_task_log`: `GET /api/plugin/claim-session`, so the task comes back after a compaction. |
|
||||
| `hooks/grimoire_precompact_preserve.sh` | PreCompact: tells the summarizer what to keep literally. Local, and never blocks. |
|
||||
| `hooks/grimoire_session_end.sh` | SessionEnd: `GET /api/plugin/release-session` releases the session's claims. |
|
||||
| `hooks/grimoire_sync_processes.sh` + `commands/sync.md` | `GET /api/plugin/processes` → `~/.claude/skills/grimoire-proc-*` stubs; `/grimoire:sync` on demand. |
|
||||
| `hooks/grimoire_defs.sh` | Shared shell helpers: config, dedup ledgers, outage line. |
|
||||
| `hooks/grimoire_static_context.md` | The adapter static text. |
|
||||
|
||||
Hook config arrives as `CLAUDE_PLUGIN_OPTION_API_ENDPOINT` / `CLAUDE_PLUGIN_OPTION_API_TOKEN` (uppercased by Claude Code), with `SCRIBE_URL` / `SCRIBE_TOKEN` as an override.
|
||||
Hook config arrives as `CLAUDE_PLUGIN_OPTION_API_ENDPOINT` / `CLAUDE_PLUGIN_OPTION_API_TOKEN` (uppercased by Claude Code), with `GRIMOIRE_URL` / `GRIMOIRE_TOKEN` as an override.
|
||||
|
||||
## How other clients would map
|
||||
|
||||
|
||||
+22
-22
@@ -4,7 +4,7 @@ Turns a self-hosted [Grimoire](https://git.fabledsword.com/bvandeusen/FabledScri
|
||||
instance into a first-class Claude Code extension:
|
||||
|
||||
- **MCP tools** over your notes, tasks, projects, milestones, systems, and
|
||||
rulebook (the `scribe` server).
|
||||
rulebook (the `grimoire` server).
|
||||
- **Session-start push channel** — a `SessionStart` hook injects the
|
||||
active project's live state (from the server) and this adapter's short
|
||||
Claude Code guidance, so Grimoire surfaces *without being asked*. Rules are
|
||||
@@ -14,15 +14,15 @@ instance into a first-class Claude Code extension:
|
||||
path, and what resembles the code) and offers them before the helper is
|
||||
rewritten. Titles only, never blocks the edit.
|
||||
- **The shared Grimoire skills** — client-neutral Agent Skills, the same files
|
||||
any client's package would ship: using-scribe, writing-plans, reporting-back
|
||||
any client's package would ship: using-grimoire, writing-plans, reporting-back
|
||||
(reply to the operator in a shape that says where the work stands),
|
||||
systematic-debugging, verification, brainstorming, reusing-code (record and
|
||||
recall reusable code as snippets), shape-accounting and family-canon.
|
||||
Replaces superpowers.
|
||||
- **Your Grimoire Processes as skills** — saved Processes are synced into local
|
||||
`~/.claude/skills/scribe-proc-*` stubs that auto-surface by relevance; the
|
||||
`~/.claude/skills/grimoire-proc-*` stubs that auto-surface by relevance; the
|
||||
stub fetches the live procedure via `get_process`. Refreshed each session and
|
||||
on demand with `/scribe:sync`.
|
||||
on demand with `/grimoire:sync`.
|
||||
|
||||
It is designed so you can uninstall `superpowers` and depend on Grimoire instead
|
||||
of auto-memory — leave auto-memory at its default; Grimoire replaces its job by
|
||||
@@ -31,8 +31,8 @@ holding the one copy, not by switching it off.
|
||||
**How the pieces divide the work**: the Grimoire server orients
|
||||
every MCP client and serves live state; the skills in `skills/` state every
|
||||
reflex in full and name no client; this plugin is the Claude Code adapter —
|
||||
hooks that deliver at the right moment, `/scribe:sync`, and the few things only
|
||||
Claude Code needs said (`hooks/scribe_static_context.md`). Packaging Grimoire for
|
||||
hooks that deliver at the right moment, `/grimoire:sync`, and the few things only
|
||||
Claude Code needs said (`hooks/grimoire_static_context.md`). Packaging Grimoire for
|
||||
another client: see [PACKAGING.md](PACKAGING.md).
|
||||
|
||||
## Install
|
||||
@@ -42,25 +42,25 @@ you always get the plugin version that matches your Grimoire instance.
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledScribe.git
|
||||
/plugin install scribe@scribe-plugin
|
||||
/plugin install grimoire@grimoire-plugin
|
||||
```
|
||||
|
||||
On install you'll be asked for:
|
||||
|
||||
| Setting | What |
|
||||
|---|---|
|
||||
| **Grimoire base URL** | e.g. `https://scribe.example.com` (no trailing slash) |
|
||||
| **Grimoire base URL** | e.g. `https://grimoire.example.com` (no trailing slash) |
|
||||
| **Grimoire API key** | a write-scoped `fmcp_` key from **Settings → MCP Access** (stored in your OS keychain) |
|
||||
| **Active project id** | optional — numeric project id to scope the session-start context |
|
||||
|
||||
## What gets wired
|
||||
|
||||
- `plugin.json` `mcpServers` → the `scribe` MCP server at `<base URL>/mcp` (Bearer auth).
|
||||
- `hooks/hooks.json` → SessionStart hook (`hooks/scribe_session_context.sh`),
|
||||
- `plugin.json` `mcpServers` → the `grimoire` MCP server at `<base URL>/mcp` (Bearer auth).
|
||||
- `hooks/hooks.json` → SessionStart hook (`hooks/grimoire_session_context.sh`),
|
||||
**fail-open**: if Grimoire is unreachable it injects nothing and never blocks
|
||||
the session.
|
||||
- `hooks/hooks.json` → PreToolUse hook on `Write|Edit`
|
||||
(`hooks/scribe_prior_art.sh`) → `GET /api/plugin/prior-art`. Returns
|
||||
(`hooks/grimoire_prior_art.sh`) → `GET /api/plugin/prior-art`. Returns
|
||||
`additionalContext` with **no** permission decision, so it can inform the write
|
||||
but never stop it; silent when nothing is recorded, which is most of the time.
|
||||
Two framings: a REUSE menu (similar/nearby records), and a SYNC nudge when a
|
||||
@@ -74,12 +74,12 @@ On install you'll be asked for:
|
||||
checked"; an answer clears the marker. The local by-name arm needs no
|
||||
server and always runs. Toggle in **Settings → General → Knowledge
|
||||
auto-inject**.
|
||||
- `hooks/hooks.json` → UserPromptSubmit hook (`hooks/scribe_autoinject.sh`) →
|
||||
- `hooks/hooks.json` → UserPromptSubmit hook (`hooks/grimoire_autoinject.sh`) →
|
||||
`GET /api/plugin/retrieve`: the rules, preferences and records that match the
|
||||
message, as a short menu of titles and matched passages.
|
||||
- `hooks/hooks.json` → PreToolUse hook on `Bash` (`hooks/scribe_tool_rules.sh`)
|
||||
- `hooks/hooks.json` → PreToolUse hook on `Bash` (`hooks/grimoire_tool_rules.sh`)
|
||||
→ `GET /api/plugin/tool-rules`: the rules that match the command about to run.
|
||||
- `hooks/hooks.json` → PreToolUse hook on every tool (`hooks/scribe_moment.sh`)
|
||||
- `hooks/hooks.json` → PreToolUse hook on every tool (`hooks/grimoire_moment.sh`)
|
||||
→ `POST /api/plugin/moment`: the rules mounted on the moments the call
|
||||
reaches. It skips tools that `GET /api/plugin/moment-tools` says reach
|
||||
nothing mounted, and Grimoire's own tools, whose responses carry
|
||||
@@ -90,7 +90,7 @@ On install you'll be asked for:
|
||||
`POST /api/plugin/rule-opened`), and claim the task being worked
|
||||
(`GET /api/plugin/claim-session`) so it comes back after a compaction.
|
||||
- `hooks/hooks.json` → PostToolUse hook on `Bash`
|
||||
(`hooks/scribe_after_write.sh`): code written through sed/heredocs/scripts
|
||||
(`hooks/grimoire_after_write.sh`): code written through sed/heredocs/scripts
|
||||
never reaches the PreToolUse hook, so this one diffs the working tree after
|
||||
every Bash call (per-session path+blob snapshot; one `git status` when
|
||||
nothing changed) and runs the same arms on the definitions just written,
|
||||
@@ -98,8 +98,8 @@ On install you'll be asked for:
|
||||
only; never blocks, and shares the pre-write hook's once-per-outage "did not
|
||||
answer" line (8 s budget here — it runs after the tool, so it gates
|
||||
nothing). The extractor, the prose/data skip list, the local by-name
|
||||
duplicate arm and the outage line are shared in `hooks/scribe_defs.sh`.
|
||||
- `hooks/hooks.json` → Stop hook (`hooks/scribe_reply_check.sh`): sends the
|
||||
duplicate arm and the outage line are shared in `hooks/grimoire_defs.sh`.
|
||||
- `hooks/hooks.json` → Stop hook (`hooks/grimoire_reply_check.sh`): sends the
|
||||
reply that ends the turn to `POST /api/plugin/reply-rules`, which holds it
|
||||
once for an unopened rule mounted on the reply moments or matching the reply.
|
||||
When the turn closed a Grimoire task (`update_task`/`create_task` with status
|
||||
@@ -109,7 +109,7 @@ On install you'll be asked for:
|
||||
the rewrite came out; it never blocks twice, and never blocks when the
|
||||
instance did not record the check (unconfigured or unreachable). Outcomes
|
||||
land in the admin logs under category `plugin`, action `report_check`.
|
||||
- `hooks/hooks.json` → a second Stop hook (`hooks/scribe_shape_check.sh`): the
|
||||
- `hooks/hooks.json` → a second Stop hook (`hooks/grimoire_shape_check.sh`): the
|
||||
write hooks note every definition a write names (and every new file) in a
|
||||
session ledger; at the end of the turn this sends them to
|
||||
`GET /api/plugin/shape-check`, which answers with the ones nobody has judged.
|
||||
@@ -119,14 +119,14 @@ On install you'll be asked for:
|
||||
as the report check: never twice, never without a recorded check. Outcomes
|
||||
land under category `plugin`, action `shape_check`.
|
||||
- `skills/` → the universal process-skills, surfaced by description match.
|
||||
- `hooks/scribe_sync_processes.sh` (a 2nd SessionStart hook) + the `/scribe:sync`
|
||||
command → generate `~/.claude/skills/scribe-proc-*` stubs from your Grimoire
|
||||
- `hooks/grimoire_sync_processes.sh` (a 2nd SessionStart hook) + the `/grimoire:sync`
|
||||
command → generate `~/.claude/skills/grimoire-proc-*` stubs from your Grimoire
|
||||
Processes (via `GET /api/plugin/processes`); also **fail-open**, and pruned to
|
||||
match what exists in Grimoire.
|
||||
- `hooks/hooks.json` → PreCompact hook (`hooks/scribe_precompact_preserve.sh`):
|
||||
- `hooks/hooks.json` → PreCompact hook (`hooks/grimoire_precompact_preserve.sh`):
|
||||
tells the summarizer which parts of the conversation to keep literally. Local
|
||||
only, and it never blocks, because a blocked compaction is skipped outright.
|
||||
- `hooks/hooks.json` → SessionEnd hook (`hooks/scribe_session_end.sh`) →
|
||||
- `hooks/hooks.json` → SessionEnd hook (`hooks/grimoire_session_end.sh`) →
|
||||
`GET /api/plugin/release-session`: releases the session's task claims.
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -4,7 +4,7 @@ allowed-tools: Bash(bash:*), Bash(ls:*)
|
||||
---
|
||||
|
||||
Regenerate the local skill stubs for your Grimoire **Processes** so they auto-surface
|
||||
this session. Each Process becomes a `~/.claude/skills/scribe-proc-<slug>/SKILL.md`
|
||||
this session. Each Process becomes a `~/.claude/skills/grimoire-proc-<slug>/SKILL.md`
|
||||
whose body calls `get_process(<name>)` for the live procedure.
|
||||
|
||||
This normally runs automatically at session start; use it after adding or editing
|
||||
@@ -14,7 +14,7 @@ skill files within this session — no restart needed).
|
||||
Run the bundled sync script, then list what was synced:
|
||||
|
||||
```
|
||||
bash "${CLAUDE_PLUGIN_ROOT}/hooks/scribe_sync_processes.sh" && ls ~/.claude/skills 2>/dev/null | grep '^scribe-proc-' || echo "no Grimoire process skills (no Processes, or Grimoire unreachable)"
|
||||
bash "${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_sync_processes.sh" && ls ~/.claude/skills 2>/dev/null | grep '^grimoire-proc-' || echo "no Grimoire process skills (no Processes, or Grimoire unreachable)"
|
||||
```
|
||||
|
||||
Report which `scribe-proc-*` skills are now present.
|
||||
Report which `grimoire-proc-*` skills are now present.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grimoire plugin — PostToolUse write-path trigger on Bash.
|
||||
#
|
||||
# scribe_prior_art.sh fires before a Write/Edit TOOL CALL. Code written any
|
||||
# grimoire_prior_art.sh fires before a Write/Edit TOOL CALL. Code written any
|
||||
# other way — sed, heredocs, python edit scripts, `cat > file` — never reaches
|
||||
# it, and those are the edits a long session makes most. So after EVERY Bash
|
||||
# call it asks git what changed in the working
|
||||
@@ -16,9 +16,9 @@
|
||||
# Cheap when nothing changed: one `git status`. State per session, beside the
|
||||
# pre-write hook's (its three dedup channels are SHARED, so a family named by
|
||||
# one hook is not named again by the other):
|
||||
# ${TMPDIR:-/tmp}/scribe-afterwrite/<sid>.snap path<TAB>blob-hash of every
|
||||
# ${TMPDIR:-/tmp}/grimoire-afterwrite/<sid>.snap path<TAB>blob-hash of every
|
||||
# dirty/untracked file last seen
|
||||
# ${TMPDIR:-/tmp}/scribe-priorart/<sid>.* the dedup channels
|
||||
# ${TMPDIR:-/tmp}/grimoire-priorart/<sid>.* the dedup channels
|
||||
#
|
||||
# NEVER BLOCKS. It returns `additionalContext` only (no decision — there is
|
||||
# nothing left to decide, the write already happened). Any failure —
|
||||
@@ -27,27 +27,27 @@
|
||||
# Config (same as the other hooks):
|
||||
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
|
||||
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# GRIMOIRE_URL / GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
set -uo pipefail
|
||||
|
||||
command -v git >/dev/null 2>&1 || exit 0
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
# PostToolUse delivers { session_id, cwd, tool_name, tool_input, tool_response }.
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
tool_name=$(scribe_json_pick "$event_flat" '.tool_name')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
tool_name=$(grimoire_json_pick "$event_flat" '.tool_name')
|
||||
[ "$tool_name" = "Bash" ] || exit 0
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
work_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
|
||||
repo_root=$(git -C "$work_dir" rev-parse --show-toplevel 2>/dev/null) || exit 0
|
||||
[ -n "$repo_root" ] || exit 0
|
||||
|
||||
safe_sid=$(printf '%s' "${session_id:-nosession}" | tr -c 'A-Za-z0-9._-' '_')
|
||||
snap_dir="${TMPDIR:-/tmp}/scribe-afterwrite"
|
||||
snap_dir="${TMPDIR:-/tmp}/grimoire-afterwrite"
|
||||
mkdir -p "$snap_dir" 2>/dev/null || true
|
||||
snap="$snap_dir/${safe_sid}.snap"
|
||||
|
||||
@@ -98,18 +98,18 @@ while IFS=$'\t' read -r path sha; do
|
||||
*"${path}"$'\t'"${sha}"*) continue ;;
|
||||
esac
|
||||
fi
|
||||
scribe_skip_path "$path" && continue
|
||||
grimoire_skip_path "$path" && continue
|
||||
changed="${changed}${path}"$'\n'
|
||||
done <<< "$current"
|
||||
[ -n "$changed" ] || exit 0
|
||||
|
||||
scribe_config || : # sets url/token; the call below is guarded on them
|
||||
scope=$(scribe_scope_query "$repo_root")
|
||||
grimoire_config || : # sets url/token; the call below is guarded on them
|
||||
scope=$(grimoire_scope_query "$repo_root")
|
||||
repo_q=""
|
||||
[ -n "$scope" ] && repo_q="&${scope}"
|
||||
|
||||
# The dedup channels are the PRE-write hook's files, on purpose (see header).
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
idfile="$state_dir/${safe_sid}.ids"
|
||||
syncfile="$state_dir/${safe_sid}.sync.ids"
|
||||
@@ -137,15 +137,15 @@ while IFS= read -r rel_path; do
|
||||
fi
|
||||
[ -n "$code" ] || continue
|
||||
# `|| true` inside: an early-exiting `head` must not void its own output.
|
||||
names=$(printf '%s' "$code" | scribe_defs | sort -u | head -12 || true)
|
||||
names=$(printf '%s' "$code" | grimoire_defs | sort -u | head -12 || true)
|
||||
# What this write defined, for the end-of-turn question —
|
||||
# recorded before the arms below, which skip a write that defines nothing.
|
||||
printf '%s\n' "$names" | scribe_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
||||
printf '%s\n' "$names" | grimoire_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
||||
# Nothing DEFINED in what was written (prose, data, a call-site edit) →
|
||||
# nothing to say; the arms are about shapes.
|
||||
[ -n "$names" ] || continue
|
||||
|
||||
local_lines=$(scribe_local_dups "$repo_root" "$rel_path" <<< "$names") || local_lines=""
|
||||
local_lines=$(grimoire_local_dups "$repo_root" "$rel_path" <<< "$names") || local_lines=""
|
||||
local_context=""
|
||||
if [ -n "$local_lines" ]; then
|
||||
local_context="> Already defined elsewhere in this repo — \`${rel_path}\` (just written) adds another copy; check before keeping it (\`git grep\` shown; a nudge, not a gate):"$'\n'"${local_lines}"
|
||||
@@ -157,12 +157,12 @@ while IFS= read -r rel_path; do
|
||||
unreached_context=""
|
||||
if [ -n "$url" ] && [ -n "$token" ]; then
|
||||
q=$(printf '%s' "$code" | head -c 1200)
|
||||
path_enc=$(printf '%s' "$rel_path" | scribe_urlenc) || path_enc=""
|
||||
code_enc=$(printf '%s' "$q" | scribe_urlenc) || code_enc=""
|
||||
path_enc=$(printf '%s' "$rel_path" | grimoire_urlenc) || path_enc=""
|
||||
code_enc=$(printf '%s' "$q" | grimoire_urlenc) || code_enc=""
|
||||
shapes_q=""
|
||||
enc=$(printf '%s\n' "$names" \
|
||||
| awk -F'\t' 'NF>=2 {printf "%s%s:%s", (n++?",":""), $1, $2}' \
|
||||
| scribe_urlenc) || enc=""
|
||||
| grimoire_urlenc) || enc=""
|
||||
[ -n "$enc" ] && shapes_q="&shapes=${enc}"
|
||||
exclude_q=""; sync_exclude_q=""; derive_exclude_q=""
|
||||
if [ -f "$idfile" ]; then
|
||||
@@ -174,7 +174,7 @@ while IFS= read -r rel_path; do
|
||||
[ -n "$sync_seen" ] && sync_exclude_q="&exclude_sync_ids=${sync_seen}"
|
||||
fi
|
||||
if [ -f "$derivefile" ]; then
|
||||
derive_seen=$(tr '\n' ',' < "$derivefile" 2>/dev/null | sed 's/,$//' | scribe_urlenc) || derive_seen=""
|
||||
derive_seen=$(tr '\n' ',' < "$derivefile" 2>/dev/null | sed 's/,$//' | grimoire_urlenc) || derive_seen=""
|
||||
[ -n "$derive_seen" ] && derive_exclude_q="&exclude_derive=${derive_seen}"
|
||||
fi
|
||||
if [ -n "$path_enc" ]; then
|
||||
@@ -190,19 +190,19 @@ while IFS= read -r rel_path; do
|
||||
# shared marker with the pre-write hook, so one outage is one
|
||||
# line however the code was written.
|
||||
if [ "$reached" = 1 ]; then
|
||||
scribe_reached "$state_dir" "$safe_sid"
|
||||
grimoire_reached "$state_dir" "$safe_sid"
|
||||
else
|
||||
unreached_context=$(scribe_unreached "$state_dir" "$safe_sid" 8 "$rel_path")
|
||||
unreached_context=$(grimoire_unreached "$state_dir" "$safe_sid" 8 "$rel_path")
|
||||
fi
|
||||
fi
|
||||
body_flat=""
|
||||
if [ -n "$body" ]; then
|
||||
body_flat=$(printf '%s' "$body" | scribe_json_flat)
|
||||
context=$(scribe_json_pick "$body_flat" '.context')
|
||||
body_flat=$(printf '%s' "$body" | grimoire_json_flat)
|
||||
context=$(grimoire_json_pick "$body_flat" '.context')
|
||||
if [ -n "$context" ]; then
|
||||
scribe_json_list_minus "$body_flat" '.note_ids' '.sync_note_ids' >> "$idfile" || true
|
||||
scribe_json_list "$body_flat" '.sync_note_ids' >> "$syncfile" || true
|
||||
scribe_json_list "$body_flat" '.derive_keys' >> "$derivefile" || true
|
||||
grimoire_json_list_minus "$body_flat" '.note_ids' '.sync_note_ids' >> "$idfile" || true
|
||||
grimoire_json_list "$body_flat" '.sync_note_ids' >> "$syncfile" || true
|
||||
grimoire_json_list "$body_flat" '.derive_keys' >> "$derivefile" || true
|
||||
# Several files in one call may name the same family: keep each
|
||||
# token once, so the next request's exclude list stays exact.
|
||||
for f in "$idfile" "$syncfile" "$derivefile"; do
|
||||
@@ -217,7 +217,7 @@ while IFS= read -r rel_path; do
|
||||
# that did not answer; "nothing recorded" is a claim only an answer
|
||||
# can back.
|
||||
if [ -n "$local_lines" ] && [ "$reached" != 0 ]; then
|
||||
n_recorded=$(scribe_json_len "$body_flat" '.note_ids')
|
||||
n_recorded=$(grimoire_json_len "$body_flat" '.note_ids')
|
||||
if [ "${n_recorded:-0}" = "0" ] || [ "$n_recorded" = "" ]; then
|
||||
local_context="${local_context}"$'\n'"> None of those existing copies is recorded in Grimoire. If the version just written is the canonical one — or this edit is consolidating the copies — record it now with create_snippet so the next session is offered it instead of writing another copy."
|
||||
fi
|
||||
@@ -238,5 +238,5 @@ while IFS= read -r rel_path; do
|
||||
done <<< "$changed"
|
||||
|
||||
[ -n "$combined" ] || exit 0
|
||||
scribe_json_out PostToolUse "$combined"
|
||||
grimoire_json_out PostToolUse "$combined"
|
||||
exit 0
|
||||
@@ -23,20 +23,20 @@
|
||||
# static floor here. If the instance is unconfigured/unreachable, or anything
|
||||
# fails, the hook stays SILENT and exits 0 — it must never block a prompt.
|
||||
#
|
||||
# Config (same as scribe_session_context.sh), exported to the hook by Claude Code
|
||||
# Config (same as grimoire_session_context.sh), exported to the hook by Claude Code
|
||||
# with the userConfig key UPPERCASED (reading the lowercase spelling silently
|
||||
# disables this hook, and silence is indistinguishable from "nothing
|
||||
# cleared the threshold"):
|
||||
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
|
||||
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# GRIMOIRE_URL / GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
#
|
||||
# Session dedup: each surfaced note id is remembered in a per-session file so a
|
||||
# note is injected at most once per session. Passed back as exclude_ids.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
@@ -45,11 +45,11 @@ command -v curl >/dev/null 2>&1 || exit 0
|
||||
# the largest payload any hook reads, and re-parsing it per field is three
|
||||
# passes over the same text.
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
prompt=$(scribe_json_pick "$event_flat" '.prompt')
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
transcript=$(scribe_json_pick "$event_flat" '.transcript_path')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
prompt=$(grimoire_json_pick "$event_flat" '.prompt')
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
transcript=$(grimoire_json_pick "$event_flat" '.transcript_path')
|
||||
|
||||
# Nothing to retrieve against.
|
||||
[ -n "$prompt" ] || exit 0
|
||||
@@ -58,12 +58,12 @@ transcript=$(scribe_json_pick "$event_flat" '.transcript_path')
|
||||
# slash-command echo, a local command's caveat banner. Leaving before the
|
||||
# request matters more for the LOG than for the noise: a retrieval call
|
||||
# recorded against a notification inflates this surface's denominator and
|
||||
# seeds `near_misses` with demand nobody expressed. scribe_skip_prompt carries
|
||||
# seeds `near_misses` with demand nobody expressed. grimoire_skip_prompt carries
|
||||
# the reasoning.
|
||||
scribe_skip_prompt "$prompt" && exit 0
|
||||
grimoire_skip_prompt "$prompt" && exit 0
|
||||
|
||||
# Unconfigured install → silent (auto-inject is pure enrichment).
|
||||
scribe_config || exit 0
|
||||
grimoire_config || exit 0
|
||||
|
||||
# Cap the query length — a giant prompt makes a giant URL for no extra signal.
|
||||
# `head -c`, not `cut -c1-2000`: cut is line-oriented and caps EACH LINE, so a
|
||||
@@ -72,9 +72,9 @@ scribe_config || exit 0
|
||||
q=$(printf '%s' "$prompt" | head -c 2000)
|
||||
# Encoded whole, never line by line: per-line encoding joins the pieces with
|
||||
# raw newlines, an invalid URL, and the long multi-line prompts most worth
|
||||
# retrieving against would be the ones dropped. scribe_urlenc reads bytes and
|
||||
# retrieving against would be the ones dropped. grimoire_urlenc reads bytes and
|
||||
# has no notion of a line.
|
||||
q_enc=$(printf '%s' "$q" | scribe_urlenc) || exit 0
|
||||
q_enc=$(printf '%s' "$q" | grimoire_urlenc) || exit 0
|
||||
[ -n "$q_enc" ] || exit 0
|
||||
|
||||
# What the conversation is about, for a prompt too short to say. Sent
|
||||
@@ -82,28 +82,28 @@ q_enc=$(printf '%s' "$q" | scribe_urlenc) || exit 0
|
||||
# thin enough to need it, for the notes and rule arms alike. Capped at
|
||||
# 600 chars here as well as there, so the URL stays small whatever the reply.
|
||||
ctx_q=""
|
||||
ctx=$(scribe_recent_context "$transcript")
|
||||
ctx=$(grimoire_recent_context "$transcript")
|
||||
if [ -n "$ctx" ]; then
|
||||
ctx_enc=$(printf '%s' "$ctx" | scribe_urlenc) && [ -n "$ctx_enc" ] && ctx_q="&ctx=${ctx_enc}"
|
||||
ctx_enc=$(printf '%s' "$ctx" | grimoire_urlenc) && [ -n "$ctx_enc" ] && ctx_q="&ctx=${ctx_enc}"
|
||||
fi
|
||||
|
||||
# Scope to this directory's project — a `.scribe` marker, else the git remote.
|
||||
# Scope to this directory's project — a `.grimoire` marker, else the git remote.
|
||||
repo_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
|
||||
scope=$(scribe_scope_query "$repo_dir")
|
||||
scope=$(grimoire_scope_query "$repo_dir")
|
||||
repo_q=""
|
||||
[ -n "$scope" ] && repo_q="&${scope}"
|
||||
|
||||
# Per-session dedup: ids already injected this session are skipped.
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-autoinject"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-autoinject"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
# RULES DEDUP IN A DIFFERENT DIRECTORY, and it has to be this one. The rule
|
||||
# ledger is SHARED by every arm that can name a rule — the two PreToolUse
|
||||
# hooks already keep it under scribe-priorart — so that one session keeps ONE
|
||||
# hooks already keep it under grimoire-priorart — so that one session keeps ONE
|
||||
# list and a rule named here is not re-announced before the next Bash call.
|
||||
# A private copy here would make each arm's "already seen" mean something
|
||||
# different. The directory is named for the prior-art hook, which is not a
|
||||
# scope claim.
|
||||
rule_state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
rule_state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$rule_state_dir" 2>/dev/null || true
|
||||
idfile=""
|
||||
rulefile=""
|
||||
@@ -120,15 +120,15 @@ if [ -n "$session_id" ]; then
|
||||
fi
|
||||
# AGED, not read flat: an exclusion that never expires means a rule surfaced
|
||||
# once in a long session is silenced for the rest of it, even as the session
|
||||
# stops holding what it was told. scribe_rules_live carries the reasoning.
|
||||
rule_seen=$(scribe_rules_live "$rulefile")
|
||||
# stops holding what it was told. grimoire_rules_live carries the reasoning.
|
||||
rule_seen=$(grimoire_rules_live "$rulefile")
|
||||
[ -n "$rule_seen" ] && exclude_q="${exclude_q}&exclude_rule_ids=${rule_seen}"
|
||||
# What the session actually OPENED, as against what it was shown.
|
||||
exclude_q="${exclude_q}$(scribe_held_query "$rule_state_dir/${safe_sid}.opened.ids")"
|
||||
exclude_q="${exclude_q}$(grimoire_held_query "$rule_state_dir/${safe_sid}.opened.ids")"
|
||||
# Which reply shapes it already holds in full: the core is
|
||||
# sent whole once, and as its one-line reminder on every later turn.
|
||||
shapefile=$(scribe_shapes_file "$safe_sid")
|
||||
shapes_seen=$(scribe_shapes_seen "$shapefile")
|
||||
shapefile=$(grimoire_shapes_file "$safe_sid")
|
||||
shapes_seen=$(grimoire_shapes_seen "$shapefile")
|
||||
[ -n "$shapes_seen" ] && exclude_q="${exclude_q}&shapes_seen=${shapes_seen}"
|
||||
fi
|
||||
|
||||
@@ -137,25 +137,25 @@ body=$(curl -fsS --max-time 5 \
|
||||
"${url%/}/api/plugin/retrieve?q=${q_enc}${ctx_q}${repo_q}${exclude_q}" 2>/dev/null) || exit 0
|
||||
[ -n "$body" ] || exit 0
|
||||
|
||||
body_flat=$(printf '%s' "$body" | scribe_json_flat)
|
||||
context=$(scribe_json_pick "$body_flat" '.context')
|
||||
body_flat=$(printf '%s' "$body" | grimoire_json_flat)
|
||||
context=$(grimoire_json_pick "$body_flat" '.context')
|
||||
[ -n "$context" ] || exit 0
|
||||
|
||||
# Remember the surfaced ids so they aren't injected again this session.
|
||||
if [ -n "$idfile" ]; then
|
||||
scribe_json_list "$body_flat" '.note_ids' >> "$idfile" || true
|
||||
grimoire_json_list "$body_flat" '.note_ids' >> "$idfile" || true
|
||||
fi
|
||||
# Rules onto the SHARED ledger, stamped so they can age out. Only FRESH ids
|
||||
# come back in rule_ids — a rule rendered as a repeat is already on
|
||||
# the ledger, and re-appending it would keep pushing its stamp forward so it
|
||||
# never aged at all.
|
||||
if [ -n "$rulefile" ]; then
|
||||
scribe_json_list "$body_flat" '.rule_ids' \
|
||||
| scribe_rules_append "$rulefile"
|
||||
grimoire_json_list "$body_flat" '.rule_ids' \
|
||||
| grimoire_rules_append "$rulefile"
|
||||
fi
|
||||
if [ -n "$shapefile" ]; then
|
||||
scribe_json_list "$body_flat" '.shape_keys' | scribe_shapes_append "$shapefile"
|
||||
grimoire_json_list "$body_flat" '.shape_keys' | grimoire_shapes_append "$shapefile"
|
||||
fi
|
||||
|
||||
scribe_json_out UserPromptSubmit "$context"
|
||||
grimoire_json_out UserPromptSubmit "$context"
|
||||
exit 0
|
||||
@@ -9,7 +9,7 @@
|
||||
# The harness knows. This PostToolUse hook watches `update_task` and
|
||||
# `add_task_log` and reports the event's `session_id` against the task the
|
||||
# call named, so the claim can say "this session" rather than "someone,
|
||||
# recently". Same evidence class as scribe_record_opened.sh: a tool call
|
||||
# recently". Same evidence class as grimoire_record_opened.sh: a tool call
|
||||
# happened and the harness reported it; nothing here asks the model anything.
|
||||
#
|
||||
# It cannot CREATE a claim. The server binds the session only to a live claim
|
||||
@@ -21,24 +21,24 @@
|
||||
# turn a successful tool call into a hook error.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
scribe_config || exit 0
|
||||
grimoire_config || exit 0
|
||||
|
||||
event=$(cat 2>/dev/null || true)
|
||||
[ -n "$event" ] || exit 0
|
||||
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
[ -n "$session_id" ] || exit 0
|
||||
|
||||
task_id=$(scribe_json_pick "$event_flat" '.tool_input.task_id')
|
||||
task_id=$(grimoire_json_pick "$event_flat" '.tool_input.task_id')
|
||||
task_id=$(printf '%s' "$task_id" | tr -cd '0-9')
|
||||
[ -n "$task_id" ] || exit 0
|
||||
|
||||
sid_enc=$(printf '%s' "$session_id" | scribe_urlenc) || exit 0
|
||||
sid_enc=$(printf '%s' "$session_id" | grimoire_urlenc) || exit 0
|
||||
curl -fsS --max-time 4 \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
"${url%/}/api/plugin/claim-session?task_id=${task_id}&session_id=${sid_enc}" \
|
||||
@@ -2,42 +2,42 @@
|
||||
# shellcheck shell=bash
|
||||
# Grimoire plugin — the pieces the hooks share.
|
||||
#
|
||||
# scribe_prior_art.sh fires BEFORE a Write/Edit tool call; scribe_after_write.sh
|
||||
# grimoire_prior_art.sh fires BEFORE a Write/Edit tool call; grimoire_after_write.sh
|
||||
# fires AFTER a Bash tool call and diffs the working tree, so code written by
|
||||
# sed/heredocs/scripts gets the same prior-art and ledger checks. Both need the
|
||||
# same three things, kept here so they cannot drift apart:
|
||||
#
|
||||
# scribe_skip_path PATH formats that hold prose or data, not shapes
|
||||
# scribe_skip_prompt TEXT the turn was written by the CLIENT, not the
|
||||
# grimoire_skip_path PATH formats that hold prose or data, not shapes
|
||||
# grimoire_skip_prompt TEXT the turn was written by the CLIENT, not the
|
||||
# operator — skip it entirely
|
||||
# scribe_defs stdin code → "kind<TAB>name" per definition
|
||||
# scribe_local_dups ROOT REL "kind<TAB>name" lines on stdin → the by-name
|
||||
# grimoire_defs stdin code → "kind<TAB>name" per definition
|
||||
# grimoire_local_dups ROOT REL "kind<TAB>name" lines on stdin → the by-name
|
||||
# local-duplicate lines (ARM 1)
|
||||
# scribe_unreached STATE SID SECS REL the "Grimoire didn't answer" line, once
|
||||
# grimoire_unreached STATE SID SECS REL the "Grimoire didn't answer" line, once
|
||||
# per outage — or nothing, if said lately
|
||||
# scribe_reached STATE SID the server answered: the next outage speaks again
|
||||
# scribe_config sets `url` + `token` from the env, returns 0
|
||||
# grimoire_reached STATE SID the server answered: the next outage speaks again
|
||||
# grimoire_config sets `url` + `token` from the env, returns 0
|
||||
# only if BOTH are usable
|
||||
# scribe_rules_live FILE live rule ids from the exclusion ledger,
|
||||
# grimoire_rules_live FILE live rule ids from the exclusion ledger,
|
||||
# comma-joined; entries age out
|
||||
# scribe_rules_append FILE stdin ids -> the ledger, timestamped
|
||||
# scribe_json_flat stdin JSON -> IDX<TAB>PATH<TAB>VALUE lines,
|
||||
# queried with scribe_json_pick / _list / _len.
|
||||
# scribe_json_out writes the hook envelope,
|
||||
# scribe_urlenc percent-encodes. No jq.
|
||||
# scribe_scope_query DIR `project_id=N` or `repo=<enc>` for DIR — the
|
||||
# grimoire_rules_append FILE stdin ids -> the ledger, timestamped
|
||||
# grimoire_json_flat stdin JSON -> IDX<TAB>PATH<TAB>VALUE lines,
|
||||
# queried with grimoire_json_pick / _list / _len.
|
||||
# grimoire_json_out writes the hook envelope,
|
||||
# grimoire_urlenc percent-encodes. No jq.
|
||||
# grimoire_scope_query DIR `project_id=N` or `repo=<enc>` for DIR — the
|
||||
# project-scope key EVERY hook sends.
|
||||
# Helpers: scribe_marker_file, scribe_url_host,
|
||||
# scribe_marker_read (id<TAB>why-not),
|
||||
# scribe_marker_project (the id alone)
|
||||
# Helpers: grimoire_marker_file, grimoire_url_host,
|
||||
# grimoire_marker_read (id<TAB>why-not),
|
||||
# grimoire_marker_project (the id alone)
|
||||
#
|
||||
# Sourced, not executed: `. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"`.
|
||||
# Sourced, not executed: `. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"`.
|
||||
|
||||
# Skip formats that hold prose or data rather than reusable code. Purely to
|
||||
# avoid a pointless round-trip — the server would return nothing for these
|
||||
# anyway. Config formats are NOT skipped: a CI workflow or a compose file is
|
||||
# often exactly the thing worth reusing.
|
||||
scribe_skip_path() {
|
||||
grimoire_skip_path() {
|
||||
case "$1" in
|
||||
*.md|*.mdx|*.txt|*.rst|*.json|*.lock|*.log|*.csv|*.tsv|*.svg|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf)
|
||||
return 0 ;;
|
||||
@@ -73,9 +73,9 @@ scribe_skip_path() {
|
||||
# NOT LISTED, ON PURPOSE: the compaction-resume injection. It is machine
|
||||
# written too, but it is a summary of real work rather than plumbing, and a
|
||||
# resumed session is exactly where recalling a rule earns its keep.
|
||||
scribe_skip_prompt() {
|
||||
_scribe_prompt=${1#"${1%%[![:space:]]*}"}
|
||||
case "$_scribe_prompt" in
|
||||
grimoire_skip_prompt() {
|
||||
_grimoire_prompt=${1#"${1%%[![:space:]]*}"}
|
||||
case "$_grimoire_prompt" in
|
||||
"<task-notification>"*|\
|
||||
"<agent-message>"*|"<agent-message "*|\
|
||||
"<command-name>"*|"<command-message>"*|"<command-args>"*|\
|
||||
@@ -93,33 +93,33 @@ scribe_skip_prompt() {
|
||||
# exactly like one that ran and had nothing to say. awk, sed, tr and od are
|
||||
# POSIX; every one of them is already required by code above this line.
|
||||
#
|
||||
# scribe_json.awk does the parsing and carries the format. These are the four
|
||||
# grimoire_json.awk does the parsing and carries the format. These are the four
|
||||
# jobs the hooks actually had jq for:
|
||||
#
|
||||
# READ scribe_json_flat / _flat_lines turn stdin into `IDX<TAB>PATH<TAB>
|
||||
# VALUE` lines ONCE, and scribe_json_pick / _list / _len query that
|
||||
# READ grimoire_json_flat / _flat_lines turn stdin into `IDX<TAB>PATH<TAB>
|
||||
# VALUE` lines ONCE, and grimoire_json_pick / _list / _len query that
|
||||
# text. Parse once, query many: a hook reads four fields off one
|
||||
# event, and re-parsing per field is four passes over a payload that
|
||||
# may be an entire source file.
|
||||
# WRITE scribe_json_out emits the hookSpecificOutput envelope, which is the
|
||||
# WRITE grimoire_json_out emits the hookSpecificOutput envelope, which is the
|
||||
# one piece of JSON these hooks produce.
|
||||
# ENCODE scribe_urlenc replaces `jq -sRr '@uri'`, byte-exact for UTF-8.
|
||||
# DECODE scribe_json_unescape turns a raw JSON string body into text.
|
||||
# ENCODE grimoire_urlenc replaces `jq -sRr '@uri'`, byte-exact for UTF-8.
|
||||
# DECODE grimoire_json_unescape turns a raw JSON string body into text.
|
||||
|
||||
_scribe_self=${BASH_SOURCE[0]:-$0}
|
||||
_grimoire_self=${BASH_SOURCE[0]:-$0}
|
||||
# shellcheck disable=SC1007 # `CDPATH= cd` scopes one variable to one command
|
||||
SCRIBE_HOOK_DIR=$(CDPATH= cd -- "$(dirname -- "$_scribe_self")" 2>/dev/null && pwd) \
|
||||
|| SCRIBE_HOOK_DIR=$(dirname -- "$_scribe_self")
|
||||
GRIMOIRE_HOOK_DIR=$(CDPATH= cd -- "$(dirname -- "$_grimoire_self")" 2>/dev/null && pwd) \
|
||||
|| GRIMOIRE_HOOK_DIR=$(dirname -- "$_grimoire_self")
|
||||
|
||||
# One JSON value on stdin → flat lines. Empty output means it did not parse.
|
||||
scribe_json_flat() {
|
||||
awk -f "$SCRIBE_HOOK_DIR/scribe_json.awk" 2>/dev/null || true
|
||||
grimoire_json_flat() {
|
||||
awk -f "$GRIMOIRE_HOOK_DIR/grimoire_json.awk" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# JSONL on stdin → flat lines, IDX counting the records that PARSED. A line
|
||||
# that does not parse is dropped and the rest are still read.
|
||||
scribe_json_flat_lines() {
|
||||
awk -v mode=lines -f "$SCRIBE_HOOK_DIR/scribe_json.awk" 2>/dev/null || true
|
||||
grimoire_json_flat_lines() {
|
||||
awk -v mode=lines -f "$GRIMOIRE_HOOK_DIR/grimoire_json.awk" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# $1 transcript path → the tail of the last assistant REPLY text, decoded, on
|
||||
@@ -132,19 +132,19 @@ scribe_json_flat_lines() {
|
||||
# seconds. Fixed strings
|
||||
# with UNESCAPED quotes can only match at a record's own top level.
|
||||
# A sidechain is a subagent talking, not this session, so it is skipped.
|
||||
scribe_recent_context() {
|
||||
grimoire_recent_context() {
|
||||
[ -n "${1:-}" ] && [ -f "$1" ] || return 0
|
||||
tail -c 524288 "$1" 2>/dev/null \
|
||||
| grep -F '"role":"assistant"' 2>/dev/null \
|
||||
| grep -F '"type":"text"' 2>/dev/null \
|
||||
| tail -n 4 \
|
||||
| scribe_json_flat_lines \
|
||||
| grimoire_json_flat_lines \
|
||||
| awk -F'\t' '
|
||||
$2 == ".isSidechain" { side[$1] = $3; next }
|
||||
$2 ~ /^\.message\.content\[[0-9]+\]\.text$/ { txt[$1] = txt[$1] " " $3; if ($1 > last) last = $1 }
|
||||
END { for (i = last; i >= 0; i--) if ((i in txt) && side[i] != "true") { print txt[i]; exit } }
|
||||
' 2>/dev/null \
|
||||
| scribe_json_unescape \
|
||||
| grimoire_json_unescape \
|
||||
| tr '\n\t' ' ' \
|
||||
| awk '{ sub(/[ ]+$/, ""); n = length($0); if (n) print (n > 600 ? substr($0, n - 599) : $0) }'
|
||||
# A transcript with no reply yet is an answer ("nothing"), not a failure —
|
||||
@@ -156,16 +156,16 @@ scribe_recent_context() {
|
||||
#
|
||||
# `null` READS AS ABSENT. A field the server left null and one it never sent
|
||||
# mean the same thing to every caller here, and the shells downstream test `[ -n "$x" ]`.
|
||||
scribe_json_pick() {
|
||||
grimoire_json_pick() {
|
||||
printf '%s\n' "${1:-}" \
|
||||
| awk -F'\t' -v p="$2" '$2 == p { if ($3 != "null") print $3; exit }' 2>/dev/null \
|
||||
| scribe_json_unescape
|
||||
| grimoire_json_unescape
|
||||
}
|
||||
|
||||
# $1 flat text, $2 array path → each element decoded, one per line. Elements
|
||||
# are ids and keys here, so a value containing a newline is not a case that
|
||||
# arises; one would split into two lines.
|
||||
scribe_json_list() {
|
||||
grimoire_json_list() {
|
||||
printf '%s\n' "${1:-}" \
|
||||
| awk -F'\t' -v p="$2" '
|
||||
BEGIN { plen = length(p) }
|
||||
@@ -173,25 +173,25 @@ scribe_json_list() {
|
||||
rest = substr($2, plen + 1)
|
||||
if (rest ~ /^\[[0-9]+\]$/ && $3 != "null") print $3
|
||||
}' 2>/dev/null \
|
||||
| scribe_json_unescape
|
||||
| grimoire_json_unescape
|
||||
}
|
||||
|
||||
# $1 flat text, $2 array path, $3 array path to SUBTRACT → the elements of $2
|
||||
# that are not in $3, in order. The reuse and sync channels are two classes of
|
||||
# one answer and each has its own session ledger, so the ids that go in the
|
||||
# reuse file are precisely `note_ids` minus `sync_note_ids`.
|
||||
scribe_json_list_minus() {
|
||||
grimoire_json_list_minus() {
|
||||
local keep
|
||||
keep=$(scribe_json_list "$1" "$3")
|
||||
scribe_json_list "$1" "$2" \
|
||||
keep=$(grimoire_json_list "$1" "$3")
|
||||
grimoire_json_list "$1" "$2" \
|
||||
| awk -v drop="$keep" '
|
||||
BEGIN { n = split(drop, a, "\n"); for (i = 1; i <= n; i++) if (a[i] != "") s[a[i]] = 1 }
|
||||
NF && !($0 in s)' 2>/dev/null || true
|
||||
}
|
||||
|
||||
# $1 flat text, $2 array path → its length. "" when the path is not an array,
|
||||
# which is NOT the same as 0 — see the `[#]` note in scribe_json.awk.
|
||||
scribe_json_len() {
|
||||
# which is NOT the same as 0 — see the `[#]` note in grimoire_json.awk.
|
||||
grimoire_json_len() {
|
||||
printf '%s\n' "${1:-}" \
|
||||
| awk -F'\t' -v p="$2" '$2 == p "[#]" { print $3; exit }' 2>/dev/null
|
||||
}
|
||||
@@ -205,7 +205,7 @@ scribe_json_len() {
|
||||
# and mawk write the single byte n % 256, so "·" (U+00B7) would arrive as a
|
||||
# lone 0xB7 wherever the hook runs without a UTF-8 locale. Under LC_ALL=C every awk writes exactly the byte asked for, so the encoding
|
||||
# below is the only one that happens.
|
||||
scribe_json_unescape() {
|
||||
grimoire_json_unescape() {
|
||||
LC_ALL=C awk '
|
||||
function utf8(n) {
|
||||
if (n < 128) return sprintf("%c", n)
|
||||
@@ -269,7 +269,7 @@ scribe_json_unescape() {
|
||||
# quadruple-escape whose meaning varies between awks. Plain concatenation has
|
||||
# no such reading, and getting this wrong corrupts every injected context that
|
||||
# happens to contain a Windows path or a regex.
|
||||
scribe_json_escape() {
|
||||
grimoire_json_escape() {
|
||||
awk '
|
||||
function rep(str, sep, with, parts, cnt, i, o) {
|
||||
cnt = split(str, parts, sep)
|
||||
@@ -302,9 +302,9 @@ scribe_json_escape() {
|
||||
|
||||
# The one JSON document these hooks WRITE: $1 hook event name, $2 the context.
|
||||
# Every hook emitted this through `jq -n --arg c`, five copies of one shape.
|
||||
scribe_json_out() {
|
||||
grimoire_json_out() {
|
||||
local esc
|
||||
esc=$(printf '%s' "$2" | scribe_json_escape) || return 0
|
||||
esc=$(printf '%s' "$2" | grimoire_json_escape) || return 0
|
||||
printf '{"hookSpecificOutput":{"hookEventName":"%s","additionalContext":"%s"}}\n' \
|
||||
"$1" "$esc"
|
||||
}
|
||||
@@ -321,7 +321,7 @@ scribe_json_out() {
|
||||
#
|
||||
# Slightly more aggressive than jq's `@uri`, which leaves `!~*'()` alone. Those
|
||||
# are legal either way and the server decodes both identically.
|
||||
scribe_urlenc() {
|
||||
grimoire_urlenc() {
|
||||
od -An -v -tu1 2>/dev/null \
|
||||
| awk '{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
@@ -350,7 +350,7 @@ scribe_urlenc() {
|
||||
# symbol that does not exist. blank_spans() replaces every comment and string
|
||||
# span with its own newlines before a matcher sees a line — the same scan, in
|
||||
# the same order, as coverage.py::_blank_spans. Change one, change both.
|
||||
scribe_defs() {
|
||||
grimoire_defs() {
|
||||
awk '
|
||||
BEGIN {
|
||||
SQ = sprintf("%c", 39)
|
||||
@@ -510,7 +510,7 @@ scribe_defs() {
|
||||
# Definition-shaped patterns only. Grepping for bare occurrences would match
|
||||
# every CALL site and drown the real finding — and a hint that is mostly noise
|
||||
# is one people learn to skip, which is worse than none. ALL code, not a
|
||||
# language shortlist: the same keyword family scribe_defs announces.
|
||||
# language shortlist: the same keyword family grimoire_defs announces.
|
||||
#
|
||||
# $1 repo root, $2 repo-relative path of the file being written (excluded from
|
||||
# the grep — it would always match itself on an Edit). Definitions on stdin.
|
||||
@@ -527,7 +527,7 @@ scribe_defs() {
|
||||
# carrier for a reflex that has to change what the act IS, so this looks the
|
||||
# callers up instead.
|
||||
#
|
||||
# `scribe_exposed` — the names a CALLER can depend on, from a blob of code.
|
||||
# `grimoire_exposed` — the names a CALLER can depend on, from a blob of code.
|
||||
# Three kinds, because a contract breaks three ways and they look nothing
|
||||
# alike in the source:
|
||||
#
|
||||
@@ -538,15 +538,15 @@ scribe_defs() {
|
||||
# The third matters because a returned dict can change shape with no
|
||||
# signature changing: callers that read it inside a fail-open `except` go
|
||||
# silently inert, with nothing pointing at the cause.
|
||||
scribe_exposed() {
|
||||
# The `sym` half DELEGATES to scribe_defs rather than repeating its patterns.
|
||||
grimoire_exposed() {
|
||||
# The `sym` half DELEGATES to grimoire_defs rather than repeating its patterns.
|
||||
# Those patterns cover nine languages and carry their own edge cases (the Go
|
||||
# receiver form, the `type` import specifier, the dunder skip); a second copy
|
||||
# here would drift from them. One reader, called twice.
|
||||
local blob
|
||||
blob=$(cat)
|
||||
{
|
||||
printf '%s' "$blob" | scribe_defs
|
||||
printf '%s' "$blob" | grimoire_defs
|
||||
printf '%s' "$blob" | awk '
|
||||
function emit(kind, name) {
|
||||
if (name != "" && name !~ /^__.*__$/) print kind "\t" name
|
||||
@@ -597,10 +597,10 @@ scribe_exposed() {
|
||||
# always its own first mention and listing it says nothing.
|
||||
#
|
||||
# `|| true` INSIDE the substitution, for the reason given at
|
||||
# scribe_local_dups: under `pipefail` a `head` that exits early kills the
|
||||
# grimoire_local_dups: under `pipefail` a `head` that exits early kills the
|
||||
# still-writing git grep, and an outer fallback then wipes the hits head had
|
||||
# already printed.
|
||||
scribe_contract_readers() {
|
||||
grimoire_contract_readers() {
|
||||
local root="$1" rel="$2" name="$3"
|
||||
[ -n "$root" ] && [ -n "$name" ] || return 0
|
||||
git -C "$root" grep -I -l -w -e "$name" -- . ":(exclude)${rel}" 2>/dev/null \
|
||||
@@ -621,7 +621,7 @@ scribe_contract_readers() {
|
||||
# result ends it silently. On a repo of any size most edits touch something
|
||||
# local, so most edits say nothing here — and a hint that fires on everything
|
||||
# is one that gets skipped.
|
||||
scribe_contract_block() {
|
||||
grimoire_contract_block() {
|
||||
local root="$1" rel="$2" subject="$3" old_text="$4" new_text="$5" ledger="$6"
|
||||
local changed gained lost readers count
|
||||
[ -n "$root" ] && [ -n "$subject" ] || return 0
|
||||
@@ -638,8 +638,8 @@ scribe_contract_block() {
|
||||
# also keeps the two sides' extraction visibly identical.
|
||||
changed=$(
|
||||
{
|
||||
printf '%s' "$old_text" | scribe_exposed | sed 's/^/O\t/'
|
||||
printf '%s' "$new_text" | scribe_exposed | sed 's/^/N\t/'
|
||||
printf '%s' "$old_text" | grimoire_exposed | sed 's/^/O\t/'
|
||||
printf '%s' "$new_text" | grimoire_exposed | sed 's/^/N\t/'
|
||||
} | awk -F'\t' '
|
||||
NF >= 3 { k = $2 "\t" $3; side[k] = side[k] $1 }
|
||||
END {
|
||||
@@ -651,7 +651,7 @@ scribe_contract_block() {
|
||||
)
|
||||
[ -n "$changed" ] || return 0
|
||||
|
||||
readers=$(scribe_contract_readers "$root" "$rel" "$subject")
|
||||
readers=$(grimoire_contract_readers "$root" "$rel" "$subject")
|
||||
[ -n "$readers" ] || return 0
|
||||
count=$(printf '%s\n' "$readers" | grep -c . 2>/dev/null || printf '0')
|
||||
|
||||
@@ -669,7 +669,7 @@ scribe_contract_block() {
|
||||
}
|
||||
|
||||
|
||||
scribe_slippage_lines() {
|
||||
grimoire_slippage_lines() {
|
||||
# $1 state dir, $2 sanitised session id.
|
||||
#
|
||||
# SILENT ONLY WHEN NO RULE TOUCHED THE SESSION AT ALL. Traffic that did
|
||||
@@ -690,15 +690,15 @@ scribe_slippage_lines() {
|
||||
# their TTL has usually already eaten the early part of a long session. A
|
||||
# readout built on them would report only the last stretch of the session
|
||||
# and read as though it had reported all of it.
|
||||
named=$(scribe_ledger_kept "$dir/${sid}.rules.keep.ids")
|
||||
opened=$(scribe_ledger_kept "$dir/${sid}.opened.keep.ids")
|
||||
acted=$(scribe_ledger_kept "$dir/${sid}.acted.keep.ids")
|
||||
held=$(scribe_ledger_kept "$dir/${sid}.checkpoint.keep.ids")
|
||||
named=$(grimoire_ledger_kept "$dir/${sid}.rules.keep.ids")
|
||||
opened=$(grimoire_ledger_kept "$dir/${sid}.opened.keep.ids")
|
||||
acted=$(grimoire_ledger_kept "$dir/${sid}.acted.keep.ids")
|
||||
held=$(grimoire_ledger_kept "$dir/${sid}.checkpoint.keep.ids")
|
||||
[ -n "$named$opened" ] || return 0
|
||||
|
||||
local unread unresolved
|
||||
unread=$(scribe_ids_minus "$named" "$opened")
|
||||
unresolved=$(scribe_ids_minus "$opened" "$acted")
|
||||
unread=$(grimoire_ids_minus "$named" "$opened")
|
||||
unresolved=$(grimoire_ids_minus "$opened" "$acted")
|
||||
|
||||
printf -- '- This session'"'"'s rule traffic, from what actually happened rather than from recollection:\n'
|
||||
[ -n "$opened" ] && printf -- ' read: %s\n' "$opened"
|
||||
@@ -710,7 +710,7 @@ scribe_slippage_lines() {
|
||||
return 0
|
||||
}
|
||||
|
||||
scribe_ids_minus() {
|
||||
grimoire_ids_minus() {
|
||||
# Set difference over two space-separated id lists, order preserved. Written
|
||||
# as one awk pass rather than a nested shell loop because the ledgers can
|
||||
# hold a few dozen ids by the end of a long session and this runs inside the
|
||||
@@ -757,15 +757,15 @@ scribe_ids_minus() {
|
||||
#
|
||||
# ONCE PER DISTINCT FILE, not once per (name, file) pair: the same file is
|
||||
# usually a candidate for several names at once, and the extractor reads the
|
||||
# whole file either way. `scribe_defs` runs at roughly 33ms per 250KB, so
|
||||
# whole file either way. `grimoire_defs` runs at roughly 33ms per 250KB, so
|
||||
# even a pathological payload of ordinary-word names costs a few hundred ms to
|
||||
# confirm; the per-(name, file) shape would pay that several times over for
|
||||
# the same answer.
|
||||
_SCRIBE_DUP_CANDIDATES=12
|
||||
_SCRIBE_DUP_SHOWN=4
|
||||
_SCRIBE_DUP_BUDGET=48
|
||||
_GRIMOIRE_DUP_CANDIDATES=12
|
||||
_GRIMOIRE_DUP_SHOWN=4
|
||||
_GRIMOIRE_DUP_BUDGET=48
|
||||
|
||||
scribe_local_dups() {
|
||||
grimoire_local_dups() {
|
||||
local root="$1" rel="$2" kind name pat hits
|
||||
local records="" cands="" idx=$'\n' seen=0
|
||||
local f defs line record rest files shown
|
||||
@@ -774,7 +774,7 @@ scribe_local_dups() {
|
||||
#
|
||||
# THE CANDIDATE CAP IS WIDER THAN THE DISPLAY CAP. Confirmation REMOVES
|
||||
# hits, so capping before it runs would let phantom matches crowd out a real
|
||||
# definition. The display cap (_SCRIBE_DUP_SHOWN) applies to CONFIRMED hits,
|
||||
# definition. The display cap (_GRIMOIRE_DUP_SHOWN) applies to CONFIRMED hits,
|
||||
# which is where a cap belongs.
|
||||
while IFS=$'\t' read -r kind name; do
|
||||
[ -n "${name:-}" ] || continue
|
||||
@@ -789,7 +789,7 @@ scribe_local_dups() {
|
||||
# wipe the hits head had already printed — dropping exactly the most
|
||||
# duplicated name.
|
||||
hits=$(git -C "$root" grep -I -l -E -e "$pat" -- . ":(exclude)${rel}" 2>/dev/null \
|
||||
| head -"$_SCRIBE_DUP_CANDIDATES" || true)
|
||||
| head -"$_GRIMOIRE_DUP_CANDIDATES" || true)
|
||||
[ -n "$hits" ] || continue
|
||||
records+="${kind}"$'\t'"${name}"$'\t'"${hits//$'\n'/$'\t'}"$'\n'
|
||||
cands+="${hits}"$'\n'
|
||||
@@ -806,9 +806,9 @@ scribe_local_dups() {
|
||||
# worth less here than no claim.
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
[ "$seen" -ge "$_SCRIBE_DUP_BUDGET" ] && break
|
||||
[ "$seen" -ge "$_GRIMOIRE_DUP_BUDGET" ] && break
|
||||
seen=$((seen + 1))
|
||||
defs=$(scribe_defs < "$root/$f" 2>/dev/null | sort -u) || defs=""
|
||||
defs=$(grimoire_defs < "$root/$f" 2>/dev/null | sort -u) || defs=""
|
||||
[ -n "$defs" ] || continue
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
@@ -834,7 +834,7 @@ scribe_local_dups() {
|
||||
esac
|
||||
files+="${f} "
|
||||
shown=$((shown + 1))
|
||||
[ "$shown" -ge "$_SCRIBE_DUP_SHOWN" ] && break
|
||||
[ "$shown" -ge "$_GRIMOIRE_DUP_SHOWN" ] && break
|
||||
done <<< "$(printf '%s' "${rest#*$'\t'}" | tr '\t' '\n')"
|
||||
[ -n "$files" ] || continue
|
||||
if [ "$kind" = css ]; then label=".$name"; else label="$name"; fi
|
||||
@@ -851,9 +851,9 @@ scribe_local_dups() {
|
||||
# Sets `url` and `token` as globals rather than echoing them: a token must not
|
||||
# pass through a subshell's output, where it could land in a log or an `xtrace`
|
||||
# line. Returns 0 only when both are usable, so a caller can either bail
|
||||
# (`scribe_config || exit 0`) or carry on degraded — the session-context hook
|
||||
# (`grimoire_config || exit 0`) or carry on degraded — the session-context hook
|
||||
# still owes its static floor when Grimoire is unconfigured.
|
||||
# Declared here, not just assigned inside the function: `scribe_defs.sh` owns
|
||||
# Declared here, not just assigned inside the function: `grimoire_defs.sh` owns
|
||||
# these two names, and a sourcing hook should have them defined the moment it
|
||||
# sources — before any code path that might reference them. It also lets
|
||||
# the linter see the assignment, which it cannot follow into a function in
|
||||
@@ -861,9 +861,9 @@ scribe_local_dups() {
|
||||
url=""
|
||||
token=""
|
||||
|
||||
scribe_config() {
|
||||
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_API_ENDPOINT:-}}
|
||||
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_API_TOKEN:-}}
|
||||
grimoire_config() {
|
||||
url=${GRIMOIRE_URL:-${CLAUDE_PLUGIN_OPTION_API_ENDPOINT:-}}
|
||||
token=${GRIMOIRE_TOKEN:-${CLAUDE_PLUGIN_OPTION_API_TOKEN:-}}
|
||||
# An unexpanded `${...}` placeholder arriving as a literal would be sent as a
|
||||
# garbage Bearer token and 401. Treat it as unset.
|
||||
case "$url" in *'${'*) url="" ;; esac
|
||||
@@ -879,21 +879,21 @@ scribe_config() {
|
||||
# spoke; within ten minutes of that it stays quiet, and a successful call
|
||||
# clears it so the next outage announces itself afresh. Unconfigured installs
|
||||
# never reach this: no URL/token means no call was owed.
|
||||
_SCRIBE_UNREACHED_QUIET=600
|
||||
_GRIMOIRE_UNREACHED_QUIET=600
|
||||
|
||||
scribe_unreached() {
|
||||
grimoire_unreached() {
|
||||
local marker="$1/$2.unreached" now last
|
||||
now=$(date +%s 2>/dev/null) || now=0
|
||||
if [ -f "$marker" ]; then
|
||||
last=$(cat "$marker" 2>/dev/null) || last=0
|
||||
case "$last" in ''|*[!0-9]*) last=0 ;; esac
|
||||
[ $((now - last)) -lt "$_SCRIBE_UNREACHED_QUIET" ] && return 0
|
||||
[ $((now - last)) -lt "$_GRIMOIRE_UNREACHED_QUIET" ] && return 0
|
||||
fi
|
||||
printf '%s' "$now" > "$marker" 2>/dev/null || true
|
||||
printf '> Grimoire did not answer the prior-art check for `%s` within %ss — this write went UNCHECKED against the record and the shape ledger (the local by-name arm, if it spoke above, needed no server). If the name matters, check it yourself: `search` for the concept, `list_shapes(project_id, path=…)` for the ledger. Said once per outage; if it keeps happening the instance is slow or down.' "$4" "$3"
|
||||
}
|
||||
|
||||
scribe_reached() {
|
||||
grimoire_reached() {
|
||||
rm -f "$1/$2.unreached" 2>/dev/null || true
|
||||
}
|
||||
|
||||
@@ -901,7 +901,7 @@ scribe_reached() {
|
||||
# THE RULE EXCLUSION LEDGER, and how entries in it AGE.
|
||||
#
|
||||
# Two hooks write this file and two read it, which is the whole reason the
|
||||
# parsing lives here: `scribe_prior_art.sh` and `scribe_tool_rules.sh` share
|
||||
# parsing lives here: `grimoire_prior_art.sh` and `grimoire_tool_rules.sh` share
|
||||
# one ledger so a rule named by one arm is not re-offered by the other, and a
|
||||
# format only one of them understood would break that on the first read.
|
||||
#
|
||||
@@ -923,7 +923,7 @@ scribe_reached() {
|
||||
# wrong. Wall time is available from `date` and costs nothing. The failure mode
|
||||
# it accepts is a session left idle over lunch treating its rules as forgotten,
|
||||
# which produces one extra full line per rule and no other harm.
|
||||
_SCRIBE_RULE_TTL=2700
|
||||
_GRIMOIRE_RULE_TTL=2700
|
||||
|
||||
# 45 MINUTES, and the reasoning rather than the number.
|
||||
#
|
||||
@@ -957,11 +957,11 @@ _SCRIBE_RULE_TTL=2700
|
||||
# never "old": treating it as expired would re-announce every such rule at
|
||||
# once, which is precisely the noise this exists to prevent. Those entries
|
||||
# simply never age, which is bounded: the session ends.
|
||||
scribe_rules_live() {
|
||||
grimoire_rules_live() {
|
||||
local f="$1" now
|
||||
[ -n "$f" ] && [ -f "$f" ] || return 0
|
||||
now=$(date +%s 2>/dev/null) || now=0
|
||||
awk -F'\t' -v now="$now" -v ttl="$_SCRIBE_RULE_TTL" '
|
||||
awk -F'\t' -v now="$now" -v ttl="$_GRIMOIRE_RULE_TTL" '
|
||||
{
|
||||
id = $1
|
||||
gsub(/[^0-9]/, "", id)
|
||||
@@ -982,8 +982,8 @@ scribe_rules_live() {
|
||||
}
|
||||
|
||||
# Append surfaced ids, stamped. Reads ids on stdin, one per line — the shape
|
||||
# `scribe_json_list "$flat" '.rule_ids'` already produces at both call sites.
|
||||
scribe_rules_append() {
|
||||
# `grimoire_json_list "$flat" '.rule_ids'` already produces at both call sites.
|
||||
grimoire_rules_append() {
|
||||
# Writes TWO files, and the second one is the point.
|
||||
#
|
||||
# `$f` is an EXCLUSION ledger: it answers "does this context already hold
|
||||
@@ -998,7 +998,7 @@ scribe_rules_append() {
|
||||
# would clear the ledger that is also the record.
|
||||
#
|
||||
# DERIVED HERE rather than listed at the call sites, because a list goes
|
||||
# stale (see `scribe_clear_session_ledgers`). Every ledger
|
||||
# stale (see `grimoire_clear_session_ledgers`). Every ledger
|
||||
# written through this function gets its twin, including the next one
|
||||
# somebody adds. The cost is a second small file per ledger per session.
|
||||
local f="$1" now
|
||||
@@ -1009,7 +1009,7 @@ scribe_rules_append() {
|
||||
' >> "$f" 2>/dev/null || true
|
||||
}
|
||||
|
||||
scribe_ledger_kept() {
|
||||
grimoire_ledger_kept() {
|
||||
# Ids from an EVIDENCE twin: deduped, order preserved, NOT aged. A rule
|
||||
# opened two hours ago was still opened, so the TTL that keeps an exclusion
|
||||
# ledger honest would here delete the finding.
|
||||
@@ -1030,7 +1030,7 @@ scribe_ledger_kept() {
|
||||
# remedy has been offered; repeating it on the next call would
|
||||
# turn a reader who decided the rule does not apply into a
|
||||
# reader who cannot proceed.
|
||||
# per session — at most `_SCRIBE_CHECKPOINT_CAP` stops in total, whatever the
|
||||
# per session — at most `_GRIMOIRE_CHECKPOINT_CAP` stops in total, whatever the
|
||||
# corpus scores. A mis-set floor or a corpus that suddenly
|
||||
# resembles everything must degrade to a noisy session, never
|
||||
# to one that cannot make progress. This is the guard on the
|
||||
@@ -1042,9 +1042,9 @@ scribe_ledger_kept() {
|
||||
# and that does not become untrue an hour later. Ageing it would let a long
|
||||
# session be stopped by the same rule repeatedly, which is the one outcome
|
||||
# both guards exist to prevent.
|
||||
_SCRIBE_CHECKPOINT_CAP=5
|
||||
_GRIMOIRE_CHECKPOINT_CAP=5
|
||||
|
||||
scribe_checkpoint_allowed() {
|
||||
grimoire_checkpoint_allowed() {
|
||||
# $1 ledger file, $2 rule id. Returns 0 (and RECORDS the stop) when this act
|
||||
# may be held; non-zero otherwise. Records on the way out rather than asking
|
||||
# the caller to, because a caller that forgets is a session that can be
|
||||
@@ -1062,7 +1062,7 @@ scribe_checkpoint_allowed() {
|
||||
# others, which is how a cap comes to cap nothing.
|
||||
n=$(printf '%s' "$n" | tr -cd '0-9')
|
||||
[ -n "$n" ] || n=0
|
||||
[ "$n" -ge "$_SCRIBE_CHECKPOINT_CAP" ] && return 1
|
||||
[ "$n" -ge "$_GRIMOIRE_CHECKPOINT_CAP" ] && return 1
|
||||
fi
|
||||
printf '%s\n' "$id" >> "$f" 2>/dev/null || return 1
|
||||
# The evidence twin, beside the cap rather than instead of it. `$f` is swept
|
||||
@@ -1073,7 +1073,7 @@ scribe_checkpoint_allowed() {
|
||||
return 0
|
||||
}
|
||||
|
||||
scribe_json_deny() {
|
||||
grimoire_json_deny() {
|
||||
# $1 hook event name, $2 the reason the agent reads INSTEAD of the result.
|
||||
#
|
||||
# The one place this plugin emits a decision on a tool call. `deny` returns
|
||||
@@ -1083,7 +1083,7 @@ scribe_json_deny() {
|
||||
# would hand a judgement that is the agent's to make to the person who asked
|
||||
# for the work.
|
||||
local esc
|
||||
esc=$(printf '%s' "$2" | scribe_json_escape) || return 0
|
||||
esc=$(printf '%s' "$2" | grimoire_json_escape) || return 0
|
||||
printf '{"hookSpecificOutput":{"hookEventName":"%s","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' \
|
||||
"$1" "$esc"
|
||||
}
|
||||
@@ -1092,7 +1092,7 @@ scribe_json_deny() {
|
||||
#
|
||||
# TWO LEDGERS, BECAUSE THEY RECORD TWO DIFFERENT FACTS. `.rules.ids` holds
|
||||
# every id an arm has NAMED; `.opened.ids` holds the ids the session actually
|
||||
# read, written by scribe_record_opened.sh from the `get_rule` call itself.
|
||||
# read, written by grimoire_record_opened.sh from the `get_rule` call itself.
|
||||
# Named is not read: the injected line is a teaser, and one skimmed past
|
||||
# leaves nothing behind — least of all across a compaction. Sending both lets
|
||||
# the server tell a reader who opened a rule from one who was only shown it,
|
||||
@@ -1100,9 +1100,9 @@ scribe_json_deny() {
|
||||
#
|
||||
# Same reader as the naming ledger on purpose, so ageing, the last-entry-wins
|
||||
# rule and the bare-id format are defined once and cannot drift apart.
|
||||
scribe_held_query() {
|
||||
grimoire_held_query() {
|
||||
local ids
|
||||
ids=$(scribe_rules_live "$1")
|
||||
ids=$(grimoire_rules_live "$1")
|
||||
[ -n "$ids" ] && printf '&held_rule_ids=%s' "$ids"
|
||||
return 0
|
||||
}
|
||||
@@ -1113,7 +1113,7 @@ scribe_held_query() {
|
||||
# the server already answers a repeat with one short reference line. The file
|
||||
# follows the ledger name convention, so a compaction clears it and the
|
||||
# rulings are shown in full again to the context that lost them.
|
||||
scribe_rulings_query() {
|
||||
grimoire_rulings_query() {
|
||||
local ids
|
||||
[ -f "$1" ] || return 0
|
||||
ids=$(grep -E '^[0-9]+$' "$1" 2>/dev/null | sort -un | tr '\n' ',' | sed 's/,$//')
|
||||
@@ -1139,18 +1139,18 @@ scribe_rulings_query() {
|
||||
# what the session holds, and the once-per-outage line needs it to outlive a
|
||||
# compaction.
|
||||
#
|
||||
# TWO DIRECTORIES: `scribe_autoinject.sh` keeps its note ledger in
|
||||
# `scribe-autoinject`, apart from the prior-art hooks'. The roster lives here
|
||||
# TWO DIRECTORIES: `grimoire_autoinject.sh` keeps its note ledger in
|
||||
# `grimoire-autoinject`, apart from the prior-art hooks'. The roster lives here
|
||||
# rather than as a path at each call site: one place to add to, and the tests
|
||||
# read THIS string rather than a copy of it.
|
||||
SCRIBE_LEDGER_DIRS="scribe-priorart scribe-autoinject"
|
||||
GRIMOIRE_LEDGER_DIRS="grimoire-priorart grimoire-autoinject"
|
||||
|
||||
# THE WRITTEN-SHAPES LEDGER. Every definition a write names,
|
||||
# appended as `path<TAB>kind<TAB>name`, for the Stop hook to ask about at the
|
||||
# end of the turn (scribe_shape_check.sh). Its own file — one file, one
|
||||
# end of the turn (grimoire_shape_check.sh). Its own file — one file, one
|
||||
# question — under the prior-art directory, so the session
|
||||
# clear above already sweeps it. Read `kind<TAB>name` lines on stdin (the
|
||||
# scribe_defs format).
|
||||
# grimoire_defs format).
|
||||
#
|
||||
# A NEW file that is a unit in its own right adds one `file` line named by its
|
||||
# stem — the same test the server's sync applies (coverage.is_file_unit): it
|
||||
@@ -1160,10 +1160,10 @@ SCRIBE_LEDGER_DIRS="scribe-priorart scribe-autoinject"
|
||||
# $1 session id (already made filename-safe) $2 repo-relative path
|
||||
# $3 "new" when the file did not exist before this write
|
||||
# $4 the code written (to test whether the file renders)
|
||||
scribe_written_append() {
|
||||
grimoire_written_append() {
|
||||
local sid="$1" rel="$2" fresh="${3:-}" code="${4:-}" dir stem names
|
||||
if [ -z "$sid" ] || [ -z "$rel" ]; then cat >/dev/null; return 0; fi
|
||||
dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$dir" 2>/dev/null || true
|
||||
names=$(cat)
|
||||
{
|
||||
@@ -1190,34 +1190,34 @@ scribe_written_append() {
|
||||
# point rather than a side effect: after a compaction the session no longer
|
||||
# holds the shape, so the next turn gets it in full again. Not aged — the
|
||||
# per-turn reminder is what keeps it in view between compactions.
|
||||
# scribe_shapes_file <sid> the ledger's path
|
||||
# scribe_shapes_seen <file> its keys, comma-joined, for `shapes_seen=`
|
||||
# scribe_shapes_append <file> keys on stdin, appended
|
||||
scribe_shapes_file() {
|
||||
printf '%s/scribe-priorart/%s.shapes.ids' "${TMPDIR:-/tmp}" "$1"
|
||||
# grimoire_shapes_file <sid> the ledger's path
|
||||
# grimoire_shapes_seen <file> its keys, comma-joined, for `shapes_seen=`
|
||||
# grimoire_shapes_append <file> keys on stdin, appended
|
||||
grimoire_shapes_file() {
|
||||
printf '%s/grimoire-priorart/%s.shapes.ids' "${TMPDIR:-/tmp}" "$1"
|
||||
}
|
||||
|
||||
scribe_shapes_seen() {
|
||||
grimoire_shapes_seen() {
|
||||
[ -n "$1" ] && [ -f "$1" ] || return 0
|
||||
awk '/^[a-z]+$/ && !seen[$0]++ { out = out (out == "" ? "" : ",") $0 } END { print out }' \
|
||||
"$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
scribe_shapes_append() {
|
||||
grimoire_shapes_append() {
|
||||
[ -n "$1" ] || { cat >/dev/null; return 0; }
|
||||
mkdir -p "$(dirname "$1")" 2>/dev/null || true
|
||||
awk '/^[a-z]+$/' >> "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
scribe_clear_session_ledgers() {
|
||||
grimoire_clear_session_ledgers() {
|
||||
# SPARES `*.keep.ids`, which are evidence rather than exclusions — see
|
||||
# `scribe_rules_append`. Everything else still goes: the convention is
|
||||
# `grimoire_rules_append`. Everything else still goes: the convention is
|
||||
# unchanged and still covers a ledger added tomorrow, which is what the
|
||||
# block above insists on. The twin opts OUT by its name, so a new ledger is
|
||||
# born on the swept side unless somebody says otherwise.
|
||||
local sid="$1" dir f
|
||||
[ -n "$sid" ] || return 0
|
||||
for dir in $SCRIBE_LEDGER_DIRS; do
|
||||
for dir in $GRIMOIRE_LEDGER_DIRS; do
|
||||
for f in "${TMPDIR:-/tmp}/$dir/$sid"*.ids; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in *.keep.ids) continue ;; esac
|
||||
@@ -1228,13 +1228,13 @@ scribe_clear_session_ledgers() {
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHAT HAPPENED IN THE LAST TURN of a transcript: scribe_turn.awk's
|
||||
# WHAT HAPPENED IN THE LAST TURN of a transcript: grimoire_turn.awk's
|
||||
# facts — bounded, closed, task_ids, reply — one `KEY<TAB>VALUE` per line.
|
||||
# Shared by the two Stop hooks that read a turn: the report check and the
|
||||
# reply check. Empty output when the turn cannot be bounded.
|
||||
#
|
||||
# The turn is parsed once. A window of recent lines, flattened record by
|
||||
# record and then read by scribe_turn.awk, which carries the turn-bounding
|
||||
# record and then read by grimoire_turn.awk, which carries the turn-bounding
|
||||
# rules. A line that does not parse is dropped and the rest are still read —
|
||||
# the first line of a `tail -n 3000` window is routinely half a record. If the
|
||||
# window holds no prompt, the turn cannot be bounded, and the caller says
|
||||
@@ -1269,28 +1269,28 @@ scribe_clear_session_ledgers() {
|
||||
# does, grep matches nothing, `start` stays 1, and the whole window is read the
|
||||
# slow way — correct, and slow, which is the right way round for a check that
|
||||
# can block a stop.
|
||||
_scribe_turn_window() { tail -n 3000 "$1" 2>/dev/null; }
|
||||
_scribe_turn_parse() {
|
||||
_scribe_turn_window "$1" | tail -n +"${2:-1}" | scribe_json_flat_lines \
|
||||
| awk -f "$SCRIBE_HOOK_DIR/scribe_turn.awk" 2>/dev/null
|
||||
_grimoire_turn_window() { tail -n 3000 "$1" 2>/dev/null; }
|
||||
_grimoire_turn_parse() {
|
||||
_grimoire_turn_window "$1" | tail -n +"${2:-1}" | grimoire_json_flat_lines \
|
||||
| awk -f "$GRIMOIRE_HOOK_DIR/grimoire_turn.awk" 2>/dev/null
|
||||
}
|
||||
|
||||
scribe_turn_facts() {
|
||||
grimoire_turn_facts() {
|
||||
local transcript="$1" start facts
|
||||
[ -n "$transcript" ] && [ -f "$transcript" ] || return 0
|
||||
start=$(_scribe_turn_window "$transcript" \
|
||||
start=$(_grimoire_turn_window "$transcript" \
|
||||
| grep -n -F '"message":{"role":"user","content":"' 2>/dev/null \
|
||||
| cut -d: -f1 | awk '{ last = $0 } END { if (NR) print last }')
|
||||
case "$start" in ''|*[!0-9]*) start=1 ;; esac
|
||||
facts=$(_scribe_turn_parse "$transcript" "$start")
|
||||
if [ "$(scribe_turn_fact "$facts" bounded)" != "1" ] && [ "$start" != "1" ]; then
|
||||
facts=$(_scribe_turn_parse "$transcript" 1)
|
||||
facts=$(_grimoire_turn_parse "$transcript" "$start")
|
||||
if [ "$(grimoire_turn_fact "$facts" bounded)" != "1" ] && [ "$start" != "1" ]; then
|
||||
facts=$(_grimoire_turn_parse "$transcript" 1)
|
||||
fi
|
||||
printf '%s\n' "$facts"
|
||||
}
|
||||
|
||||
# $1 facts from scribe_turn_facts, $2 key → that fact's value, or "".
|
||||
scribe_turn_fact() {
|
||||
# $1 facts from grimoire_turn_facts, $2 key → that fact's value, or "".
|
||||
grimoire_turn_fact() {
|
||||
printf '%s\n' "$1" | awk -F'\t' -v k="$2" '$1 == k { print substr($0, index($0, "\t") + 1); exit }'
|
||||
}
|
||||
|
||||
@@ -1302,12 +1302,12 @@ scribe_turn_fact() {
|
||||
# works inside a bound repo and not at all outside one, where a session would
|
||||
# get no project scope from any hook.
|
||||
#
|
||||
# The second key is a `.scribe` file in the directory (or above it, the way
|
||||
# The second key is a `.grimoire` file in the directory (or above it, the way
|
||||
# git finds its root), naming the project the work belongs to. It is a
|
||||
# POINTER, not a copy: an id and enough to check the id means what it says.
|
||||
# Nothing Grimoire should be holding goes in it.
|
||||
#
|
||||
# {"instance": "https://scribe.example.com", "project_id": 2,
|
||||
# {"instance": "https://grimoire.example.com", "project_id": 2,
|
||||
# "project": "FabledScribe"}
|
||||
#
|
||||
# instance WHICH Grimoire the id belongs to, and the reason this file is
|
||||
@@ -1323,7 +1323,7 @@ scribe_turn_fact() {
|
||||
# answers "what is this?" when opened, and so a stale one is
|
||||
# visible rather than inert.
|
||||
#
|
||||
# A bare integer is also accepted (`echo 2 > .scribe`), because it is what a
|
||||
# A bare integer is also accepted (`echo 2 > .grimoire`), because it is what a
|
||||
# person writes by hand and it parses as JSON already. It skips the instance
|
||||
# check by having nothing to check — deliberate, and the reason the written
|
||||
# form carries `instance`.
|
||||
@@ -1332,13 +1332,13 @@ scribe_turn_fact() {
|
||||
# a remote is just where the code happens to be pushed. That also makes the
|
||||
# marker the way to override a binding for one directory.
|
||||
|
||||
# Nearest `.scribe` at or above DIR. Walks up to the filesystem root, capped so
|
||||
# Nearest `.grimoire` at or above DIR. Walks up to the filesystem root, capped so
|
||||
# a pathological path cannot spin.
|
||||
scribe_marker_file() {
|
||||
grimoire_marker_file() {
|
||||
local dir="$1" depth=0
|
||||
[ -n "$dir" ] || return 0
|
||||
while [ "$depth" -lt 40 ]; do
|
||||
[ -f "$dir/.scribe" ] && { printf '%s' "$dir/.scribe"; return 0; }
|
||||
[ -f "$dir/.grimoire" ] && { printf '%s' "$dir/.grimoire"; return 0; }
|
||||
case "$dir" in ""|"/") return 0 ;; esac
|
||||
dir=$(dirname -- "$dir" 2>/dev/null) || return 0
|
||||
depth=$((depth + 1))
|
||||
@@ -1347,7 +1347,7 @@ scribe_marker_file() {
|
||||
}
|
||||
|
||||
# Host of a URL, lowercased, port kept. "" for empty input.
|
||||
scribe_url_host() {
|
||||
grimoire_url_host() {
|
||||
printf '%s' "${1:-}" \
|
||||
| sed -e 's#^[A-Za-z][A-Za-z0-9+.-]*://##' -e 's#^[^/@]*@##' -e 's#[/?].*$##' \
|
||||
| tr 'A-Z' 'a-z'
|
||||
@@ -1364,24 +1364,24 @@ scribe_url_host() {
|
||||
# subshell. The caller would then read an unset variable, which under the
|
||||
# `set -u` these hooks all run with aborts the hook and costs the whole
|
||||
# session's context — a failure far larger than the message it was fetching.
|
||||
scribe_marker_read() {
|
||||
grimoire_marker_read() {
|
||||
local f="$1" flat id inst want
|
||||
[ -n "$f" ] && [ -f "$f" ] || { printf '\t'; return 0; }
|
||||
flat=$(scribe_json_flat < "$f") || flat=""
|
||||
flat=$(grimoire_json_flat < "$f") || flat=""
|
||||
# Both accepted forms, read off one parse: the object's `project_id`, or —
|
||||
# for the bare integer someone writes by hand — the root scalar, whose path
|
||||
# is the empty string because it sits under no key.
|
||||
id=$(scribe_json_pick "$flat" '.project_id')
|
||||
[ -n "$id" ] || id=$(scribe_json_pick "$flat" '')
|
||||
id=$(grimoire_json_pick "$flat" '.project_id')
|
||||
[ -n "$id" ] || id=$(grimoire_json_pick "$flat" '')
|
||||
case "$id" in ''|*[!0-9]*) id="" ;; esac
|
||||
if [ -z "$id" ] || [ "$id" = "0" ]; then
|
||||
printf '\tnames no project_id'
|
||||
return 0
|
||||
fi
|
||||
inst=$(scribe_json_pick "$flat" '.instance')
|
||||
inst=$(grimoire_json_pick "$flat" '.instance')
|
||||
if [ -n "$inst" ]; then
|
||||
want=$(scribe_url_host "${url:-}")
|
||||
inst=$(scribe_url_host "$inst")
|
||||
want=$(grimoire_url_host "${url:-}")
|
||||
inst=$(grimoire_url_host "$inst")
|
||||
if [ -n "$want" ] && [ "$inst" != "$want" ]; then
|
||||
printf '\tpoints at %s, but this session is configured for %s' "$inst" "$want"
|
||||
return 0
|
||||
@@ -1390,25 +1390,25 @@ scribe_marker_read() {
|
||||
printf '%s\t' "$id"
|
||||
}
|
||||
|
||||
# Just the id a marker names, or "" — the half scribe_scope_query needs.
|
||||
scribe_marker_project() {
|
||||
scribe_marker_read "$1" | cut -f1
|
||||
# Just the id a marker names, or "" — the half grimoire_scope_query needs.
|
||||
grimoire_marker_project() {
|
||||
grimoire_marker_read "$1" | cut -f1
|
||||
}
|
||||
|
||||
# The query args identifying DIR's project — `project_id=N` or `repo=<enc>` —
|
||||
# with NO leading `?` or `&`, so each caller keeps its own separator. Empty
|
||||
# when neither key is available. Requires `url` to be set (scribe_config) for
|
||||
# when neither key is available. Requires `url` to be set (grimoire_config) for
|
||||
# the marker's instance check; without it a marker is still honoured, since an
|
||||
# unconfigured hook is not going to send the request anyway.
|
||||
scribe_scope_query() {
|
||||
grimoire_scope_query() {
|
||||
local dir="$1" id repo enc
|
||||
id=$(scribe_marker_project "$(scribe_marker_file "$dir")")
|
||||
id=$(grimoire_marker_project "$(grimoire_marker_file "$dir")")
|
||||
if [ -n "$id" ]; then
|
||||
printf 'project_id=%s' "$id"
|
||||
return 0
|
||||
fi
|
||||
repo=$(git -C "$dir" remote get-url origin 2>/dev/null || true)
|
||||
[ -n "$repo" ] || return 0
|
||||
enc=$(printf '%s' "$repo" | scribe_urlenc) || enc=""
|
||||
enc=$(printf '%s' "$repo" | grimoire_urlenc) || enc=""
|
||||
[ -n "$enc" ] && printf 'repo=%s' "$enc"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
# VALUE for a string, the RAW JSON-escaped body with the quotes stripped — not
|
||||
# the decoded text. Decoding here would let a newline or a tab inside a
|
||||
# value break the line format that makes this readable from shell. The
|
||||
# caller decodes what it actually uses, with `scribe_json_unescape`.
|
||||
# caller decodes what it actually uses, with `grimoire_json_unescape`.
|
||||
# Numbers, `true`, `false` and `null` are emitted literally.
|
||||
#
|
||||
# EVERY ARRAY ALSO EMITS `PATH[#]` with its LENGTH, including an empty one —
|
||||
@@ -16,53 +16,53 @@
|
||||
# other tool costs one file read. An install that has mounted nothing gets an
|
||||
# empty list and never sends a moment request at all.
|
||||
#
|
||||
# SCRIBE'S OWN TOOLS ARE SKIPPED for delivery. Closing a task or recording a
|
||||
# GRIMOIRE'S OWN TOOLS ARE SKIPPED for delivery. Closing a task or recording a
|
||||
# lesson reaches its moment through the tool's own response (`moment_rules`),
|
||||
# which works for a client without this plugin too; delivering it here as well
|
||||
# would say it twice. They are still written to the acts ledger below.
|
||||
#
|
||||
# SILENT ON OUTAGE, like scribe_tool_rules.sh and for its reason: this fires
|
||||
# SILENT ON OUTAGE, like grimoire_tool_rules.sh and for its reason: this fires
|
||||
# before many calls, and an outage line before each is noise. A failed list
|
||||
# fetch is cached as an empty list, so a down instance costs one timeout per
|
||||
# window rather than one per call.
|
||||
#
|
||||
# Env:
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# GRIMOIRE_URL / GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
tool_name=$(scribe_json_pick "$event_flat" '.tool_name')
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
tool_name=$(grimoire_json_pick "$event_flat" '.tool_name')
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
|
||||
[ -n "$tool_name" ] && [ -n "$session_id" ] || exit 0
|
||||
|
||||
scribe_config || exit 0
|
||||
grimoire_config || exit 0
|
||||
|
||||
# The tool's KEY, as the server writes mappings: no MCP server prefix, lowercase.
|
||||
tool_key=${tool_name##*__}
|
||||
tool_key=$(printf '%s' "$tool_key" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
|
||||
# Its own directory, outside SCRIBE_LEDGER_DIRS, so a compaction does not
|
||||
# Its own directory, outside GRIMOIRE_LEDGER_DIRS, so a compaction does not
|
||||
# sweep it: what is kept here describes the install and what the session
|
||||
# DID, not what its context holds. scribe_session_end.sh removes it.
|
||||
cache_dir="${TMPDIR:-/tmp}/scribe-moment"
|
||||
# DID, not what its context holds. grimoire_session_end.sh removes it.
|
||||
cache_dir="${TMPDIR:-/tmp}/grimoire-moment"
|
||||
mkdir -p "$cache_dir" 2>/dev/null || true
|
||||
now=$(date +%s 2>/dev/null) || now=0
|
||||
|
||||
# ── The acts ledger ──────────────────────────────────────────────────────
|
||||
#
|
||||
# Every call, before any filter: `epoch<TAB>event`, one line each. When the
|
||||
# session then opens a rule, scribe_record_opened.sh sends the last few
|
||||
# session then opens a rule, grimoire_record_opened.sh sends the last few
|
||||
# minutes of these, and the server counts the open as evidence the rule
|
||||
# belongs on the moments they reached — which is how a rule nobody mounted
|
||||
# gets proposed for one. Recorded whether or not anything is mounted, since
|
||||
@@ -73,7 +73,7 @@ if [ "$now" -gt 0 ] && [ "$tool_key" != "get_rule" ]; then
|
||||
acts_file="$cache_dir/${safe_sid}.acts"
|
||||
act=$(printf '%s' "$event" | tr -d '\n\r')
|
||||
if [ "${#act}" -gt 16384 ]; then
|
||||
esc=$(printf '%s' "$tool_name" | scribe_json_escape) || esc=""
|
||||
esc=$(printf '%s' "$tool_name" | grimoire_json_escape) || esc=""
|
||||
act="{\"tool_name\":\"${esc}\",\"tool_input\":{}}"
|
||||
fi
|
||||
printf '%s\t%s\n' "$now" "$act" >> "$acts_file" 2>/dev/null || true
|
||||
@@ -86,7 +86,7 @@ if [ "$now" -gt 0 ] && [ "$tool_key" != "get_rule" ]; then
|
||||
fi
|
||||
|
||||
case "$tool_name" in
|
||||
mcp__*scribe*__*) exit 0 ;;
|
||||
mcp__*grimoire*__*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# ── Which tools can reach a mounted rule: cached, first line a timestamp ──
|
||||
@@ -103,7 +103,7 @@ if [ "$now" -eq 0 ] || [ $((now - stamp)) -gt 300 ]; then
|
||||
"${url%/}/api/plugin/moment-tools" 2>/dev/null) || listed=""
|
||||
{
|
||||
printf '%s\n' "$now"
|
||||
scribe_json_list "$(printf '%s' "$listed" | scribe_json_flat)" '.tools'
|
||||
grimoire_json_list "$(printf '%s' "$listed" | grimoire_json_flat)" '.tools'
|
||||
} > "$tools_file" 2>/dev/null || true
|
||||
fi
|
||||
tail -n +2 "$tools_file" 2>/dev/null | grep -Fqx -- "$tool_key" || exit 0
|
||||
@@ -112,27 +112,27 @@ tail -n +2 "$tools_file" 2>/dev/null | grep -Fqx -- "$tool_key" || exit 0
|
||||
|
||||
repo_q=""
|
||||
lookup_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
|
||||
scope=$(scribe_scope_query "$lookup_dir")
|
||||
scope=$(grimoire_scope_query "$lookup_dir")
|
||||
[ -n "$scope" ] && repo_q="&${scope}"
|
||||
|
||||
# The shared session ledger, as scribe_tool_rules.sh reads and writes it: a
|
||||
# The shared session ledger, as grimoire_tool_rules.sh reads and writes it: a
|
||||
# rule this session was already shown is cited, not quoted again.
|
||||
rulefile="$state_dir/${safe_sid}.rules.ids"
|
||||
ledger_q=""
|
||||
rule_seen=$(scribe_rules_live "$rulefile")
|
||||
rule_seen=$(grimoire_rules_live "$rulefile")
|
||||
[ -n "$rule_seen" ] && ledger_q="&exclude_rule_ids=${rule_seen}"
|
||||
ledger_q="${ledger_q}$(scribe_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
ledger_q="${ledger_q}$(grimoire_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
# The reply-shape ledger: a shape this session already holds in
|
||||
# full comes back as its reminder.
|
||||
shapefile=$(scribe_shapes_file "$safe_sid")
|
||||
shapes_seen=$(scribe_shapes_seen "$shapefile")
|
||||
shapefile=$(grimoire_shapes_file "$safe_sid")
|
||||
shapes_seen=$(grimoire_shapes_seen "$shapefile")
|
||||
[ -n "$shapes_seen" ] && ledger_q="${ledger_q}&shapes_seen=${shapes_seen}"
|
||||
|
||||
# The event whole, since which field a mapping matches is the mapping's
|
||||
# business. A write of a very large file is reduced to its name: the moments a
|
||||
# write reaches never depend on its content.
|
||||
if [ "${#event}" -gt 65536 ]; then
|
||||
esc=$(printf '%s' "$tool_name" | scribe_json_escape) || exit 0
|
||||
esc=$(printf '%s' "$tool_name" | grimoire_json_escape) || exit 0
|
||||
event="{\"tool_name\":\"${esc}\",\"tool_input\":{}}"
|
||||
fi
|
||||
|
||||
@@ -144,11 +144,11 @@ body=$(printf '%s' "$event" | curl -fsS --max-time 3 \
|
||||
--data-binary @- \
|
||||
"${url%/}/api/plugin/moment${query:+?$query}" 2>/dev/null) || exit 0
|
||||
|
||||
body_flat=$(printf '%s' "$body" | scribe_json_flat)
|
||||
scribe_json_list "$body_flat" '.rule_ids' | scribe_rules_append "$rulefile"
|
||||
scribe_json_list "$body_flat" '.shape_keys' | scribe_shapes_append "$shapefile"
|
||||
body_flat=$(printf '%s' "$body" | grimoire_json_flat)
|
||||
grimoire_json_list "$body_flat" '.rule_ids' | grimoire_rules_append "$rulefile"
|
||||
grimoire_json_list "$body_flat" '.shape_keys' | grimoire_shapes_append "$shapefile"
|
||||
|
||||
context=$(scribe_json_pick "$body_flat" '.context')
|
||||
context=$(grimoire_json_pick "$body_flat" '.context')
|
||||
[ -n "$context" ] || exit 0
|
||||
scribe_json_out PreToolUse "$context"
|
||||
grimoire_json_out PreToolUse "$context"
|
||||
exit 0
|
||||
+5
-5
@@ -40,8 +40,8 @@
|
||||
# text is the receipt. (Auto-compaction suppresses that notification.)
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
# THE EVENT IS READ for `session_id`: the slippage readout below is specific
|
||||
# to THIS session, and the id is the key to the ledgers that hold it. The
|
||||
@@ -51,8 +51,8 @@ set -uo pipefail
|
||||
event=$(cat 2>/dev/null || true)
|
||||
session_id=""
|
||||
if [ -n "$event" ]; then
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat 2>/dev/null || true)
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id' 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat 2>/dev/null || true)
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
@@ -94,7 +94,7 @@ EOF
|
||||
# readout teaches its reader to skip it.
|
||||
if [ -n "$session_id" ]; then
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
slippage=$(scribe_slippage_lines "${TMPDIR:-/tmp}/scribe-priorart" "$safe_sid" 2>/dev/null || true)
|
||||
slippage=$(grimoire_slippage_lines "${TMPDIR:-/tmp}/grimoire-priorart" "$safe_sid" 2>/dev/null || true)
|
||||
if [ -n "$slippage" ]; then
|
||||
printf '\n%s\n' "$slippage"
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grimoire plugin — PreToolUse write-path trigger (prior-art recall).
|
||||
#
|
||||
# Auto-inject (scribe_autoinject.sh) fires on the operator's prompt. The moment
|
||||
# Auto-inject (grimoire_autoinject.sh) fires on the operator's prompt. The moment
|
||||
# reuse is actually lost is later: when the AGENT decides mid-task to write a
|
||||
# helper. This hook fires there — on Write/Edit — and asks the operator's Grimoire
|
||||
# instance what prior art is already recorded for the target file: a snippet at
|
||||
@@ -31,38 +31,38 @@
|
||||
# hook then exits 0 in silence, looking exactly like "no prior art"):
|
||||
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
|
||||
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# GRIMOIRE_URL / GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
set -uo pipefail
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Shared with the after-write hook: the prose/data skip list, the definition
|
||||
# extractor and the local by-name duplicate arm live in scribe_defs.sh so the
|
||||
# extractor and the local by-name duplicate arm live in grimoire_defs.sh so the
|
||||
# two hooks cannot drift apart. Sourced FIRST because the JSON reader is there
|
||||
# too.
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
# PreToolUse delivers { session_id, cwd, tool_name, tool_input: {...}, ... }
|
||||
# One parse, five fields. `.tool_input.content` on a Write is the entire file
|
||||
# being written, so parsing per field would mean reading it five times.
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
file_path=$(scribe_json_pick "$event_flat" '.tool_input.file_path')
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
file_path=$(grimoire_json_pick "$event_flat" '.tool_input.file_path')
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
|
||||
[ -n "$file_path" ] || exit 0
|
||||
|
||||
# The code about to be written. Write and Edit name this field differently, and
|
||||
# the names have changed across Claude Code versions — take whichever is present
|
||||
# rather than betting on one shape.
|
||||
code=$(scribe_json_pick "$event_flat" '.tool_input.content')
|
||||
[ -n "$code" ] || code=$(scribe_json_pick "$event_flat" '.tool_input.file_content')
|
||||
[ -n "$code" ] || code=$(scribe_json_pick "$event_flat" '.tool_input.new_string')
|
||||
[ -n "$code" ] || code=$(scribe_json_pick "$event_flat" '.tool_input.new_str')
|
||||
code=$(grimoire_json_pick "$event_flat" '.tool_input.content')
|
||||
[ -n "$code" ] || code=$(grimoire_json_pick "$event_flat" '.tool_input.file_content')
|
||||
[ -n "$code" ] || code=$(grimoire_json_pick "$event_flat" '.tool_input.new_string')
|
||||
[ -n "$code" ] || code=$(grimoire_json_pick "$event_flat" '.tool_input.new_str')
|
||||
|
||||
scribe_skip_path "$file_path" && exit 0
|
||||
grimoire_skip_path "$file_path" && exit 0
|
||||
|
||||
# Snippet locations are recorded repo-relative, so send a repo-relative path —
|
||||
# an absolute one would simply match nothing. Resolved BEFORE the config gate
|
||||
@@ -78,16 +78,16 @@ if [ -n "$repo_root" ]; then
|
||||
fi
|
||||
|
||||
# ARM 1 — BY NAME, LOCALLY: does a definition of this already exist
|
||||
# in the repo? (scribe_local_dups in scribe_defs.sh carries the why.)
|
||||
# in the repo? (grimoire_local_dups in grimoire_defs.sh carries the why.)
|
||||
names=""
|
||||
if [ -n "$code" ]; then
|
||||
# `|| true` inside: an early-exiting `head` must not void its own output.
|
||||
names=$(printf '%s' "$code" | scribe_defs | sort -u | head -12 || true)
|
||||
names=$(printf '%s' "$code" | grimoire_defs | sort -u | head -12 || true)
|
||||
fi
|
||||
|
||||
local_lines=""
|
||||
if [ -n "$repo_root" ] && [ -n "$names" ]; then
|
||||
local_lines=$(scribe_local_dups "$repo_root" "$rel_path" <<< "$names") || local_lines=""
|
||||
local_lines=$(grimoire_local_dups "$repo_root" "$rel_path" <<< "$names") || local_lines=""
|
||||
[ -n "$local_lines" ] && local_lines="${local_lines}"$'\n'
|
||||
fi
|
||||
|
||||
@@ -114,13 +114,13 @@ fi
|
||||
# and no `head -1`, whose early exit is a SIGPIPE trap under pipefail.
|
||||
shapes="$names"
|
||||
if [ -z "$shapes" ] && [ -f "$file_path" ]; then
|
||||
old_first=$(scribe_json_pick "$event_flat" '.tool_input.old_string')
|
||||
[ -n "$old_first" ] || old_first=$(scribe_json_pick "$event_flat" '.tool_input.old_str')
|
||||
old_first=$(grimoire_json_pick "$event_flat" '.tool_input.old_string')
|
||||
[ -n "$old_first" ] || old_first=$(grimoire_json_pick "$event_flat" '.tool_input.old_str')
|
||||
old_first=$(printf '%s' "$old_first" | grep -m1 -v '^[[:space:]]*$' || true)
|
||||
if [ -n "$old_first" ]; then
|
||||
ln=$(grep -nF -m1 -- "$old_first" "$file_path" 2>/dev/null | cut -d: -f1) || ln=""
|
||||
if [ -n "$ln" ]; then
|
||||
shapes=$(head -n "$ln" "$file_path" | scribe_defs | tail -1 || true)
|
||||
shapes=$(head -n "$ln" "$file_path" | grimoire_defs | tail -1 || true)
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -128,16 +128,16 @@ shapes_q=""
|
||||
if [ -n "$shapes" ]; then
|
||||
enc=$(printf '%s\n' "$shapes" \
|
||||
| awk -F'\t' 'NF>=2 {printf "%s%s:%s", (n++?",":""), $1, $2}' \
|
||||
| scribe_urlenc) || enc=""
|
||||
| grimoire_urlenc) || enc=""
|
||||
[ -n "$enc" ] && shapes_q="&shapes=${enc}"
|
||||
fi
|
||||
|
||||
scribe_config || : # sets url/token; unconfigured is handled just below
|
||||
grimoire_config || : # sets url/token; unconfigured is handled just below
|
||||
# Unconfigured install → the recorded-prior-art arms are skipped, but the local
|
||||
# arm above already ran and may have something to say.
|
||||
if [ -z "$url" ] || [ -z "$token" ]; then
|
||||
if [ -n "$local_context" ]; then
|
||||
scribe_json_out PreToolUse "$local_context"
|
||||
grimoire_json_out PreToolUse "$local_context"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
@@ -153,13 +153,13 @@ q=$(printf '%s' "$code" | head -c 1200)
|
||||
|
||||
# Encoded whole, never line by line: a payload encoded per line comes back as
|
||||
# separately-encoded lines joined by raw newlines, an invalid URL, and code is
|
||||
# full of newlines. scribe_urlenc reads bytes and has no notion of a line.
|
||||
path_enc=$(printf '%s' "$rel_path" | scribe_urlenc)
|
||||
# full of newlines. grimoire_urlenc reads bytes and has no notion of a line.
|
||||
path_enc=$(printf '%s' "$rel_path" | grimoire_urlenc)
|
||||
[ -n "$path_enc" ] || exit 0
|
||||
code_enc=$(printf '%s' "$q" | scribe_urlenc)
|
||||
code_enc=$(printf '%s' "$q" | grimoire_urlenc)
|
||||
|
||||
# Scope to this directory's project — a `.scribe` marker, else the git remote.
|
||||
scope=$(scribe_scope_query "$lookup_dir")
|
||||
# Scope to this directory's project — a `.grimoire` marker, else the git remote.
|
||||
scope=$(grimoire_scope_query "$lookup_dir")
|
||||
repo_q=""
|
||||
[ -n "$scope" ] && repo_q="&${scope}"
|
||||
|
||||
@@ -173,7 +173,7 @@ repo_q=""
|
||||
# exact file being edited — updating the record is part of the edit"). They
|
||||
# dedup separately: a reuse hint shown early in the session must not suppress
|
||||
# the sync nudge when the recorded file itself is edited later.
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
#
|
||||
# A THIRD channel: the ledger's derive arm names a duplicate family
|
||||
@@ -186,7 +186,7 @@ mkdir -p "$state_dir" 2>/dev/null || true
|
||||
# re-offered on every subsequent write in the session.
|
||||
#
|
||||
# A FIFTH: the Systems whose RULINGS were shown in full. SHARED
|
||||
# with scribe_tool_rules.sh, like the rule file: an area's rulings shown before
|
||||
# with grimoire_tool_rules.sh, like the rule file: an area's rulings shown before
|
||||
# a command must not be repeated in full before the next edit there.
|
||||
idfile=""
|
||||
syncfile=""
|
||||
@@ -202,18 +202,18 @@ if [ -n "$session_id" ]; then
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
# What this write defined, for the end-of-turn question. A
|
||||
# Write onto a path that does not exist yet creates a file, which may be a
|
||||
# unit in its own right (see scribe_written_append). Written before any
|
||||
# unit in its own right (see grimoire_written_append). Written before any
|
||||
# server call: the ask at the end of the turn must not depend on this
|
||||
# write's hint having been answered.
|
||||
fresh=""
|
||||
[ -e "$file_path" ] || fresh="new"
|
||||
printf '%s\n' "$shapes" | scribe_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
||||
printf '%s\n' "$shapes" | grimoire_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
||||
idfile="$state_dir/${safe_sid}.ids"
|
||||
syncfile="$state_dir/${safe_sid}.sync.ids"
|
||||
derivefile="$state_dir/${safe_sid}.derive.ids"
|
||||
rulefile="$state_dir/${safe_sid}.rules.ids"
|
||||
rulingsfile="$state_dir/${safe_sid}.rulings.ids"
|
||||
rulings_q=$(scribe_rulings_query "$rulingsfile")
|
||||
rulings_q=$(grimoire_rulings_query "$rulingsfile")
|
||||
if [ -f "$idfile" ]; then
|
||||
seen=$(tr '\n' ',' < "$idfile" 2>/dev/null | sed 's/,$//')
|
||||
[ -n "$seen" ] && exclude_q="&exclude_ids=${seen}"
|
||||
@@ -223,16 +223,16 @@ if [ -n "$session_id" ]; then
|
||||
[ -n "$sync_seen" ] && sync_exclude_q="&exclude_sync_ids=${sync_seen}"
|
||||
fi
|
||||
if [ -f "$derivefile" ]; then
|
||||
derive_seen=$(tr '\n' ',' < "$derivefile" 2>/dev/null | sed 's/,$//' | scribe_urlenc) || derive_seen=""
|
||||
derive_seen=$(tr '\n' ',' < "$derivefile" 2>/dev/null | sed 's/,$//' | grimoire_urlenc) || derive_seen=""
|
||||
[ -n "$derive_seen" ] && derive_exclude_q="&exclude_derive=${derive_seen}"
|
||||
fi
|
||||
# Ageing, not a flat read, and the ONLY ledger here that ages: the
|
||||
# note channels above are a different question with a different answer, and
|
||||
# this arm's sibling hook reads the same rule file through the same helper.
|
||||
rule_seen=$(scribe_rules_live "$rulefile")
|
||||
rule_seen=$(grimoire_rules_live "$rulefile")
|
||||
[ -n "$rule_seen" ] && rule_exclude_q="&exclude_rule_ids=${rule_seen}"
|
||||
# What the session actually OPENED, as against what it was shown.
|
||||
rule_exclude_q="${rule_exclude_q}$(scribe_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
rule_exclude_q="${rule_exclude_q}$(grimoire_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
fi
|
||||
|
||||
# Not `|| exit 0`: an unreachable instance must not discard a local finding
|
||||
@@ -245,34 +245,34 @@ body=$(curl -fsS --max-time 5 \
|
||||
"${url%/}/api/plugin/prior-art?path=${path_enc}&code=${code_enc}${repo_q}${exclude_q}${sync_exclude_q}${derive_exclude_q}${rule_exclude_q}${rulings_q}${shapes_q}" 2>/dev/null) || { body=""; reached=0; }
|
||||
unreached_context=""
|
||||
if [ "$reached" = 1 ]; then
|
||||
scribe_reached "$state_dir" "${safe_sid:-nosession}"
|
||||
grimoire_reached "$state_dir" "${safe_sid:-nosession}"
|
||||
else
|
||||
unreached_context=$(scribe_unreached "$state_dir" "${safe_sid:-nosession}" 5 "$rel_path")
|
||||
unreached_context=$(grimoire_unreached "$state_dir" "${safe_sid:-nosession}" 5 "$rel_path")
|
||||
fi
|
||||
|
||||
context=""
|
||||
body_flat=""
|
||||
if [ -n "$body" ]; then
|
||||
body_flat=$(printf '%s' "$body" | scribe_json_flat)
|
||||
context=$(scribe_json_pick "$body_flat" '.context')
|
||||
body_flat=$(printf '%s' "$body" | grimoire_json_flat)
|
||||
context=$(grimoire_json_pick "$body_flat" '.context')
|
||||
# Remember what was surfaced so it isn't shown again this session — each
|
||||
# class into its own channel: sync ids (snippets recording the edited file)
|
||||
# to the sync file, everything else to the reuse file.
|
||||
if [ -n "$context" ]; then
|
||||
if [ -n "$idfile" ]; then
|
||||
scribe_json_list_minus "$body_flat" '.note_ids' '.sync_note_ids' >> "$idfile" || true
|
||||
grimoire_json_list_minus "$body_flat" '.note_ids' '.sync_note_ids' >> "$idfile" || true
|
||||
fi
|
||||
if [ -n "$syncfile" ]; then
|
||||
scribe_json_list "$body_flat" '.sync_note_ids' >> "$syncfile" || true
|
||||
grimoire_json_list "$body_flat" '.sync_note_ids' >> "$syncfile" || true
|
||||
fi
|
||||
if [ -n "$rulefile" ]; then
|
||||
scribe_json_list "$body_flat" '.rule_ids' | scribe_rules_append "$rulefile"
|
||||
grimoire_json_list "$body_flat" '.rule_ids' | grimoire_rules_append "$rulefile"
|
||||
fi
|
||||
if [ -n "$derivefile" ]; then
|
||||
scribe_json_list "$body_flat" '.derive_keys' >> "$derivefile" || true
|
||||
grimoire_json_list "$body_flat" '.derive_keys' >> "$derivefile" || true
|
||||
fi
|
||||
if [ -n "$rulingsfile" ]; then
|
||||
scribe_json_list "$body_flat" '.ruling_system_ids' >> "$rulingsfile" || true
|
||||
grimoire_json_list "$body_flat" '.ruling_system_ids' >> "$rulingsfile" || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -287,7 +287,7 @@ fi
|
||||
# of those copies is recorded" is a claim only an answer can back — the
|
||||
# unreached line says what actually happened instead.
|
||||
if [ -n "$local_lines" ] && [ "$reached" = 1 ]; then
|
||||
n_recorded=$(scribe_json_len "$body_flat" '.note_ids')
|
||||
n_recorded=$(grimoire_json_len "$body_flat" '.note_ids')
|
||||
if [ "${n_recorded:-0}" = "0" ] || [ "$n_recorded" = "" ]; then
|
||||
local_context="${local_context}"$'\n'"> None of those existing copies is recorded in Grimoire. If the version being written is the canonical one — or this edit is consolidating the copies — record it now with create_snippet (name, code, when-to-reach-for-it, location) so the next session is offered it instead of writing another copy."
|
||||
fi
|
||||
@@ -309,19 +309,19 @@ fi
|
||||
# no "before" and nothing to break.
|
||||
contract_context=""
|
||||
if [ -n "$repo_root" ] && [ -n "$code" ]; then
|
||||
old_code=$(scribe_json_pick "$event_flat" '.tool_input.old_string')
|
||||
[ -n "$old_code" ] || old_code=$(scribe_json_pick "$event_flat" '.tool_input.old_str')
|
||||
old_code=$(grimoire_json_pick "$event_flat" '.tool_input.old_string')
|
||||
[ -n "$old_code" ] || old_code=$(grimoire_json_pick "$event_flat" '.tool_input.old_str')
|
||||
# The SUBJECT is the definition whose contract may have moved. `shapes`
|
||||
# already holds either the definitions in the payload or — for an edit
|
||||
# inside a function body — the one enclosing the edit, which is exactly the
|
||||
# thing whose callers matter. CSS rows are skipped: a class has readers, but
|
||||
# they are markup files and `scribe_local_dups` already speaks for those.
|
||||
# they are markup files and `grimoire_local_dups` already speaks for those.
|
||||
subject=$(printf '%s\n' "$shapes" \
|
||||
| awk -F'\t' 'NF>=2 && $1!="css" {print $2; exit}')
|
||||
if [ -n "$old_code" ] && [ -n "$subject" ]; then
|
||||
contract_file=""
|
||||
[ -n "${safe_sid:-}" ] && contract_file="$state_dir/${safe_sid}.contract.ids"
|
||||
contract_context=$(scribe_contract_block \
|
||||
contract_context=$(grimoire_contract_block \
|
||||
"$repo_root" "$rel_path" "$subject" "$old_code" "$code" "$contract_file" \
|
||||
2>/dev/null) || contract_context=""
|
||||
fi
|
||||
@@ -347,5 +347,5 @@ fi
|
||||
[ -n "$combined" ] || exit 0
|
||||
|
||||
# No permissionDecision: this is a nudge, not a gate. The write goes ahead.
|
||||
scribe_json_out PreToolUse "$combined"
|
||||
grimoire_json_out PreToolUse "$combined"
|
||||
exit 0
|
||||
@@ -40,46 +40,46 @@
|
||||
# here is one extra line, in the direction that shows more rather than less.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
event=$(cat 2>/dev/null || true)
|
||||
[ -n "$event" ] || exit 0
|
||||
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
[ -n "$session_id" ] || exit 0
|
||||
|
||||
# The matcher in hooks.json already narrows to the get_rule tools, but the
|
||||
# server segment of an MCP tool name varies with how the plugin was installed,
|
||||
# so the id is read from whichever field is actually present rather than from
|
||||
# an assumed tool name. An event that carries none simply records nothing.
|
||||
rule_id=$(scribe_json_pick "$event_flat" '.tool_input.rule_id')
|
||||
rule_id=$(grimoire_json_pick "$event_flat" '.tool_input.rule_id')
|
||||
rule_id=$(printf '%s' "$rule_id" | tr -cd '0-9')
|
||||
[ -n "$rule_id" ] || exit 0
|
||||
|
||||
# The same directory the naming ledger uses. One session keeps its state in one
|
||||
# place, and the prior-art name is kept for the reason scribe_tool_rules.sh
|
||||
# place, and the prior-art name is kept for the reason grimoire_tool_rules.sh
|
||||
# gives: renaming it would orphan every live session's state for a cosmetic
|
||||
# gain.
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
|
||||
# Stamped and append-only, exactly like the naming ledger — so the same reader
|
||||
# (`scribe_rules_live`) ages both, and the last entry for an id wins.
|
||||
printf '%s\n' "$rule_id" | scribe_rules_append "$state_dir/${safe_sid}.opened.ids"
|
||||
# (`grimoire_rules_live`) ages both, and the last entry for an id wins.
|
||||
printf '%s\n' "$rule_id" | grimoire_rules_append "$state_dir/${safe_sid}.opened.ids"
|
||||
|
||||
# ── What came just before the open ───────────────────────────────────────
|
||||
#
|
||||
# scribe_moment.sh writes every call to `scribe-moment/<sid>.acts`. The ones
|
||||
# grimoire_moment.sh writes every call to `grimoire-moment/<sid>.acts`. The ones
|
||||
# from the last three minutes go to the server with this rule's id: a rule
|
||||
# opened just after a moment fired is evidence it belongs on that moment, and
|
||||
# once that repeats across sessions the server answers with one line asking
|
||||
# the reader to offer the operator the mount. Nothing is mounted by this.
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
scribe_config || exit 0
|
||||
acts_file="${TMPDIR:-/tmp}/scribe-moment/${safe_sid}.acts"
|
||||
grimoire_config || exit 0
|
||||
acts_file="${TMPDIR:-/tmp}/grimoire-moment/${safe_sid}.acts"
|
||||
[ -f "$acts_file" ] || exit 0
|
||||
now=$(date +%s 2>/dev/null) || exit 0
|
||||
acts=$(awk -F '\t' -v since=$((now - 180)) \
|
||||
@@ -87,9 +87,9 @@ acts=$(awk -F '\t' -v since=$((now - 180)) \
|
||||
| tail -n 30 | paste -sd ',' - 2>/dev/null)
|
||||
[ -n "$acts" ] || exit 0
|
||||
|
||||
sid_esc=$(printf '%s' "$session_id" | scribe_json_escape) || exit 0
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
scope=$(scribe_scope_query "${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}")
|
||||
sid_esc=$(printf '%s' "$session_id" | grimoire_json_escape) || exit 0
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
scope=$(grimoire_scope_query "${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}")
|
||||
body=$(printf '{"rule_id":%s,"session_id":"%s","acts":[%s]}' "$rule_id" "$sid_esc" "$acts" \
|
||||
| curl -fsS --max-time 3 \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
@@ -97,7 +97,7 @@ body=$(printf '{"rule_id":%s,"session_id":"%s","acts":[%s]}' "$rule_id" "$sid_es
|
||||
--data-binary @- \
|
||||
"${url%/}/api/plugin/rule-opened${scope:+?$scope}" 2>/dev/null) || exit 0
|
||||
|
||||
context=$(scribe_json_pick "$(printf '%s' "$body" | scribe_json_flat)" '.context')
|
||||
context=$(grimoire_json_pick "$(printf '%s' "$body" | grimoire_json_flat)" '.context')
|
||||
[ -n "$context" ] || exit 0
|
||||
scribe_json_out PostToolUse "$context"
|
||||
grimoire_json_out PostToolUse "$context"
|
||||
exit 0
|
||||
@@ -28,28 +28,28 @@
|
||||
# turn a successful tool call into a hook error.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
event=$(cat 2>/dev/null || true)
|
||||
[ -n "$event" ] || exit 0
|
||||
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
[ -n "$session_id" ] || exit 0
|
||||
|
||||
# The matcher in hooks.json narrows to the rule_outcome tools, but the server
|
||||
# segment of an MCP tool name varies with how the plugin was installed, so the
|
||||
# id is read from the field rather than from an assumed tool name — the same
|
||||
# reasoning scribe_record_opened.sh gives.
|
||||
rule_id=$(scribe_json_pick "$event_flat" '.tool_input.rule_id')
|
||||
# reasoning grimoire_record_opened.sh gives.
|
||||
rule_id=$(grimoire_json_pick "$event_flat" '.tool_input.rule_id')
|
||||
rule_id=$(printf '%s' "$rule_id" | tr -cd '0-9')
|
||||
[ -n "$rule_id" ] || exit 0
|
||||
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
|
||||
# Stamped and append-only like its two siblings, so one reader ages them all.
|
||||
printf '%s\n' "$rule_id" | scribe_rules_append "$state_dir/${safe_sid}.acted.ids"
|
||||
printf '%s\n' "$rule_id" | grimoire_rules_append "$state_dir/${safe_sid}.acted.ids"
|
||||
exit 0
|
||||
@@ -26,23 +26,23 @@
|
||||
# not stop the reply about it, and the per-session cap counts both doors.
|
||||
#
|
||||
# Env:
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# GRIMOIRE_URL / GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
transcript=$(scribe_json_pick "$event_flat" '.transcript_path')
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
active=$(scribe_json_pick "$event_flat" '.stop_hook_active')
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
transcript=$(grimoire_json_pick "$event_flat" '.transcript_path')
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
active=$(grimoire_json_pick "$event_flat" '.stop_hook_active')
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
|
||||
[ -n "$transcript" ] && [ -f "$transcript" ] && [ -n "$session_id" ] || exit 0
|
||||
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
rulefile="$state_dir/${safe_sid}.rules.ids"
|
||||
@@ -62,21 +62,21 @@ else
|
||||
rm -f "$reportfile" 2>/dev/null || true
|
||||
rewrite=false
|
||||
fi
|
||||
scribe_config || exit 0
|
||||
grimoire_config || exit 0
|
||||
|
||||
facts=$(scribe_turn_facts "$transcript")
|
||||
[ "$(scribe_turn_fact "$facts" bounded)" = "1" ] || exit 0
|
||||
reply=$(scribe_turn_fact "$facts" reply | scribe_json_unescape)
|
||||
facts=$(grimoire_turn_facts "$transcript")
|
||||
[ "$(grimoire_turn_fact "$facts" bounded)" = "1" ] || exit 0
|
||||
reply=$(grimoire_turn_fact "$facts" reply | grimoire_json_unescape)
|
||||
# The reply may not be in the transcript yet when the hook fires: an empty
|
||||
# reply is "cannot tell", never "missing everything".
|
||||
[ -n "$(printf '%s' "$reply" | tr -d '[:space:]')" ] || exit 0
|
||||
# How many tasks this turn closed, and which — successful closes only, this
|
||||
# session only (scribe_turn.awk). A task created already done closes with no
|
||||
# session only (grimoire_turn.awk). A task created already done closes with no
|
||||
# id, so the count is what says a check is due. Digits and commas only, so
|
||||
# both drop straight into the JSON.
|
||||
closed=$(scribe_turn_fact "$facts" closed | tr -cd '0-9')
|
||||
closed=$(grimoire_turn_fact "$facts" closed | tr -cd '0-9')
|
||||
closed=${closed:-0}
|
||||
task_ids=$(scribe_turn_fact "$facts" task_ids | tr -cd '0-9,' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||||
task_ids=$(grimoire_turn_fact "$facts" task_ids | tr -cd '0-9,' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||||
[ "$rewrite" = "true" ] && [ "$closed" = "0" ] && exit 0
|
||||
|
||||
# Bounded before encoding. The server reads the head and the tail — the part
|
||||
@@ -85,18 +85,18 @@ task_ids=$(scribe_turn_fact "$facts" task_ids | tr -cd '0-9,' | sed 's/,,*/,/g;
|
||||
if [ "${#reply}" -gt 12000 ]; then
|
||||
reply="${reply:0:4000} … ${reply: -8000}"
|
||||
fi
|
||||
reply_esc=$(printf '%s' "$reply" | scribe_json_escape) || exit 0
|
||||
reply_esc=$(printf '%s' "$reply" | grimoire_json_escape) || exit 0
|
||||
closing=""
|
||||
if [ "$closed" != "0" ]; then
|
||||
closing=$(printf ',"closed":%d,"closed_task_ids":[%s],"rewrite":%s' "$closed" "$task_ids" "$rewrite")
|
||||
fi
|
||||
|
||||
query=""
|
||||
scope=$(scribe_scope_query "${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}")
|
||||
scope=$(grimoire_scope_query "${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}")
|
||||
[ -n "$scope" ] && query="&${scope}"
|
||||
rule_seen=$(scribe_rules_live "$rulefile")
|
||||
rule_seen=$(grimoire_rules_live "$rulefile")
|
||||
[ -n "$rule_seen" ] && query="${query}&exclude_rule_ids=${rule_seen}"
|
||||
query="${query}$(scribe_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
query="${query}$(grimoire_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
if [ -f "$stopfile" ]; then
|
||||
stopped=$(grep -E '^[0-9]+$' "$stopfile" 2>/dev/null | paste -sd, -)
|
||||
[ -n "$stopped" ] && query="${query}&stopped_rule_ids=${stopped}"
|
||||
@@ -110,17 +110,17 @@ answer=$(printf '{"reply":"%s"%s}' "$reply_esc" "$closing" | curl -fsS --max-tim
|
||||
"${url%/}/api/plugin/reply-rules${query:+?$query}" 2>/dev/null) || exit 0
|
||||
|
||||
[ "$rewrite" = "true" ] && exit 0
|
||||
answer_flat=$(printf '%s' "$answer" | scribe_json_flat)
|
||||
reason=$(scribe_json_pick "$answer_flat" '.reason')
|
||||
answer_flat=$(printf '%s' "$answer" | grimoire_json_flat)
|
||||
reason=$(grimoire_json_pick "$answer_flat" '.reason')
|
||||
[ -n "$reason" ] || exit 0
|
||||
[ "$(scribe_json_pick "$answer_flat" '.report_check')" = "blocked" ] && : > "$reportfile" 2>/dev/null
|
||||
[ "$(grimoire_json_pick "$answer_flat" '.report_check')" = "blocked" ] && : > "$reportfile" 2>/dev/null
|
||||
|
||||
# Recorded BEFORE the block is emitted, for the act checkpoint's reason: a
|
||||
# hold that is shown and not recorded is one that can be shown again.
|
||||
for id in $(scribe_json_list "$answer_flat" '.rule_ids'); do
|
||||
scribe_checkpoint_allowed "$stopfile" "$id" || true
|
||||
for id in $(grimoire_json_list "$answer_flat" '.rule_ids'); do
|
||||
grimoire_checkpoint_allowed "$stopfile" "$id" || true
|
||||
done
|
||||
scribe_json_list "$answer_flat" '.rule_ids' | scribe_rules_append "$rulefile"
|
||||
grimoire_json_list "$answer_flat" '.rule_ids' | grimoire_rules_append "$rulefile"
|
||||
|
||||
printf '{"decision":"block","reason":"%s"}\n' "$(printf '%s' "$reason" | scribe_json_escape)"
|
||||
printf '{"decision":"block","reason":"%s"}\n' "$(printf '%s' "$reason" | grimoire_json_escape)"
|
||||
exit 0
|
||||
@@ -2,9 +2,9 @@
|
||||
# Grimoire plugin — SessionStart push channel (two tiers + compaction re-grounding).
|
||||
#
|
||||
# Tier 1 (STATIC, always fires, no auth, no network): injects the Claude Code
|
||||
# adapter's own guidance (scribe_static_context.md) — where Grimoire's reflexes
|
||||
# are stated (the using-scribe skill), Claude Code's memory files, /compact,
|
||||
# /scribe:sync, and what to do when Grimoire is unavailable. It carries only what
|
||||
# adapter's own guidance (grimoire_static_context.md) — where Grimoire's reflexes
|
||||
# are stated (the using-grimoire skill), Claude Code's memory files, /compact,
|
||||
# /grimoire:sync, and what to do when Grimoire is unavailable. It carries only what
|
||||
# this client needs said; the reflexes themselves are owned by the shared
|
||||
# skills and the server's index.
|
||||
#
|
||||
@@ -28,7 +28,7 @@
|
||||
# plus a post-compaction reload from the instance.
|
||||
#
|
||||
# The other half: a host hook cannot make the model flush before a compaction,
|
||||
# but scribe_precompact_preserve.sh steers the
|
||||
# but grimoire_precompact_preserve.sh steers the
|
||||
# SUMMARY, because a PreCompact hook's stdout becomes the compaction's custom
|
||||
# instructions. The two are complementary, and neither replaces the other: that
|
||||
# hook decides what survives into the summary, this one reloads from the record
|
||||
@@ -37,8 +37,8 @@
|
||||
# IMPORTANT: do NOT pass config via `${user_config.*}` substitution in a
|
||||
# shell-form hooks.json command — Claude Code rejects that outright (splicing a
|
||||
# configured value into a shell command line would let the shell run whatever it
|
||||
# contains). The env vars above are the supported channel; SCRIBE_URL /
|
||||
# SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# contains). The env vars above are the supported channel; GRIMOIRE_URL /
|
||||
# GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
#
|
||||
# FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session, and every
|
||||
# way it can come up empty produces a short status line — failed fetch, missing
|
||||
@@ -46,8 +46,8 @@
|
||||
# allowed to fail quietly; see the comment at the status block below.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
# `CDPATH= cd` is deliberate, not a typo'd assignment: it runs this one `cd`
|
||||
# with CDPATH empty, so an operator whose CDPATH happens to contain a matching
|
||||
@@ -58,12 +58,12 @@ here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0
|
||||
|
||||
# SessionStart delivers a JSON event on stdin; `source` is startup|resume|compact|clear.
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
source=$(scribe_json_pick "$event_flat" '.source')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
source=$(grimoire_json_pick "$event_flat" '.source')
|
||||
|
||||
# --- The rule ledger outlives the context it describes ---
|
||||
#
|
||||
# scribe_prior_art.sh and scribe_tool_rules.sh record every rule id they have
|
||||
# grimoire_prior_art.sh and grimoire_tool_rules.sh record every rule id they have
|
||||
# named in <state>/<sid>.rules.ids and hand it back as exclude_rule_ids, so a
|
||||
# rule is named once per session and then goes quiet. That is right while the
|
||||
# session still HOLDS what it was told, and wrong the moment it does not.
|
||||
@@ -105,21 +105,21 @@ source=$(scribe_json_pick "$event_flat" '.source')
|
||||
# first minute of a session unreachable for the rest of it, through any number
|
||||
# of compactions.
|
||||
#
|
||||
# `scribe_clear_session_ledgers` matches on the naming convention — a
|
||||
# `grimoire_clear_session_ledgers` matches on the naming convention — a
|
||||
# per-session ledger is `<sid>[.<kind>].ids` — so it covers every ledger,
|
||||
# including one added later. Best-effort, like every other filesystem touch in these hooks:
|
||||
# a ledger that cannot be removed costs a repeated exclusion, never a session.
|
||||
#
|
||||
# NOT swept: `<sid>.unreached`, which records that the instance was unreachable
|
||||
# rather than what the session holds, and survives on purpose. The directories
|
||||
# swept are `scribe_defs.sh`'s `SCRIBE_LEDGER_DIRS` — plural, because the
|
||||
# swept are `grimoire_defs.sh`'s `GRIMOIRE_LEDGER_DIRS` — plural, because the
|
||||
# auto-inject arm keeps its ledger somewhere else.
|
||||
case "$source" in
|
||||
compact|clear)
|
||||
sid=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
sid=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
if [ -n "$sid" ]; then
|
||||
safe_sid=$(printf '%s' "$sid" | tr -c 'A-Za-z0-9._-' '_')
|
||||
scribe_clear_session_ledgers "$safe_sid"
|
||||
grimoire_clear_session_ledgers "$safe_sid"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -131,14 +131,14 @@ append() { if [ -n "$out" ]; then out="${out}"$'\n\n---\n\n'"$1"; else out="$1";
|
||||
prepend() { if [ -n "$out" ]; then out="$1"$'\n\n---\n\n'"${out}"; else out="$1"; fi; }
|
||||
|
||||
# --- Tier 1: static behavioral mandate (always, keyless, networkless) ---
|
||||
[ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")
|
||||
[ -f "$here/grimoire_static_context.md" ] && out=$(cat "$here/grimoire_static_context.md")
|
||||
|
||||
# --- Which version is actually RUNNING (keyless, networkless) ---
|
||||
#
|
||||
# An install has two halves and only one self-updates:
|
||||
#
|
||||
# marketplaces/…/scribe-plugin/ git clone — pulls on its own
|
||||
# cache/…/scribe/<version>/ what EXECUTES — refreshed only when the
|
||||
# marketplaces/…/grimoire-plugin/ git clone — pulls on its own
|
||||
# cache/…/grimoire/<version>/ what EXECUTES — refreshed only when the
|
||||
# manifest version changes
|
||||
#
|
||||
# So inspecting the clone shows a fix present while the broken copy keeps
|
||||
@@ -151,7 +151,7 @@ prepend() { if [ -n "$out" ]; then out="$1"$'\n\n---\n\n'"${out}"; else out="$1"
|
||||
# appears there.
|
||||
manifest="$here/../.claude-plugin/plugin.json"
|
||||
if [ -f "$manifest" ]; then
|
||||
plugin_version=$(scribe_json_pick "$(scribe_json_flat < "$manifest")" '.version')
|
||||
plugin_version=$(grimoire_json_pick "$(grimoire_json_flat < "$manifest")" '.version')
|
||||
if [ -n "$plugin_version" ]; then
|
||||
append "> Grimoire plugin **v${plugin_version}** is executing in this session. A fix merged after this version has not reached it — the marketplace clone updates on its own, but the cache that runs only refreshes when the manifest version changes."
|
||||
fi
|
||||
@@ -160,28 +160,28 @@ fi
|
||||
# --- Tier 2: dynamic rules + active-project context (best-effort) ---
|
||||
# Unconfigured is NOT a failure here: tier 1's static floor is still owed,
|
||||
# so this records the answer rather than acting on it.
|
||||
scribe_config || :
|
||||
grimoire_config || :
|
||||
|
||||
dyn=""
|
||||
status=""
|
||||
if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
# Which project is this directory's? A `.scribe` marker first, then the git
|
||||
# remote. scribe_scope_query answers that, and is shared with the
|
||||
# Which project is this directory's? A `.grimoire` marker first, then the git
|
||||
# remote. grimoire_scope_query answers that, and is shared with the
|
||||
# other five hooks so a directory scopes the same way everywhere; the pieces
|
||||
# are re-read here only to explain what happened when nothing resolved.
|
||||
repo_dir=${CLAUDE_PROJECT_DIR:-$PWD}
|
||||
marker=$(scribe_marker_file "$repo_dir")
|
||||
marker_read=$(scribe_marker_read "$marker")
|
||||
marker=$(grimoire_marker_file "$repo_dir")
|
||||
marker_read=$(grimoire_marker_read "$marker")
|
||||
marker_id=${marker_read%%$'\t'*}
|
||||
marker_why=${marker_read#*$'\t'}
|
||||
repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
|
||||
scope=$(scribe_scope_query "$repo_dir")
|
||||
scope=$(grimoire_scope_query "$repo_dir")
|
||||
# The source and the session id decide what the claim section says: a
|
||||
# compaction gets back the work this session had claimed,
|
||||
# with its latest logs; a new session hears about other sessions' claims.
|
||||
sid_now=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
q="source=$(printf '%s' "$source" | scribe_urlenc)"
|
||||
[ -n "$sid_now" ] && q="${q}&session_id=$(printf '%s' "$sid_now" | scribe_urlenc)"
|
||||
sid_now=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
q="source=$(printf '%s' "$source" | grimoire_urlenc)"
|
||||
[ -n "$sid_now" ] && q="${q}&session_id=$(printf '%s' "$sid_now" | grimoire_urlenc)"
|
||||
[ -n "$scope" ] && q="${q}&${scope}"
|
||||
q="?${q}"
|
||||
# ONE FETCH, NAMED WHEN IT FAILS. A timeout, an HTTP error and a refused key
|
||||
@@ -212,8 +212,8 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
esac
|
||||
body_flat=""
|
||||
if [ "$ctx_rc" = 0 ] && [ "${ctx_code#2}" != "$ctx_code" ] && [ -n "$body" ]; then
|
||||
body_flat=$(printf '%s' "$body" | scribe_json_flat)
|
||||
dyn=$(scribe_json_pick "$body_flat" '.context')
|
||||
body_flat=$(printf '%s' "$body" | grimoire_json_flat)
|
||||
dyn=$(grimoire_json_pick "$body_flat" '.context')
|
||||
fi
|
||||
if [ -z "$dyn" ]; then
|
||||
if [ "$ctx_rc" = 28 ]; then
|
||||
@@ -235,16 +235,16 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
else
|
||||
first="Start by finding this repo's project with \`list_projects()\` and calling \`enter_project(<id>)\`"
|
||||
fi
|
||||
status="> ⚠️ Grimoire: live project context was not loaded this session — ${why}. The tools may still answer. ${first} before any other work: it loads the recent milestones and open tasks this session would otherwise begin without, and prior work you cannot see is work you will redo. The using-scribe skill still applies."
|
||||
status="> ⚠️ Grimoire: live project context was not loaded this session — ${why}. The tools may still answer. ${first} before any other work: it loads the recent milestones and open tasks this session would otherwise begin without, and prior work you cannot see is work you will redo. The using-grimoire skill still applies."
|
||||
fi
|
||||
elif [ -n "$url" ] && [ -z "$token" ]; then
|
||||
status="> ⚠️ Grimoire: live context disabled this session — the API key is not configured (Grimoire base URL is). Set it with \`/plugin\` → Grimoire → configure, or export SCRIBE_TOKEN. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
|
||||
status="> ⚠️ Grimoire: live context disabled this session — the API key is not configured (Grimoire base URL is). Set it with \`/plugin\` → Grimoire → configure, or export GRIMOIRE_TOKEN. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
|
||||
elif [ -z "$url" ] && [ -z "$token" ]; then
|
||||
# NEITHER value arrived. Not a benign "unconfigured install": the plugin
|
||||
# prompts for both values at enable time, so if neither reached the hook,
|
||||
# something is wrong (an env-var casing mismatch looks exactly like this).
|
||||
# Say so.
|
||||
status="> ⚠️ Grimoire: live context disabled this session — neither the Grimoire base URL nor the API key reached this hook. Configure the plugin (\`/plugin\` → Grimoire), or export SCRIBE_URL + SCRIBE_TOKEN. Note this also disables prompt auto-inject and the write-path prior-art trigger. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
|
||||
status="> ⚠️ Grimoire: live context disabled this session — neither the Grimoire base URL nor the API key reached this hook. Configure the plugin (\`/plugin\` → Grimoire), or export GRIMOIRE_URL + GRIMOIRE_TOKEN. Note this also disables prompt auto-inject and the write-path prior-art trigger. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
|
||||
fi
|
||||
|
||||
[ -n "$dyn" ] && append "$dyn"
|
||||
@@ -262,14 +262,14 @@ fi
|
||||
# server reports whether a project resolved, and this hook — which knows what
|
||||
# it sent and why — turns that into the sentence the operator can act on. The
|
||||
# repo case is left to the server's existing "bind this repo" hint.
|
||||
if [ -n "$dyn" ] && [ -z "$(scribe_json_pick "$body_flat" '.project.id')" ]; then
|
||||
host=$(scribe_url_host "$url")
|
||||
if [ -n "$dyn" ] && [ -z "$(grimoire_json_pick "$body_flat" '.project.id')" ]; then
|
||||
host=$(grimoire_url_host "$url")
|
||||
if [ -n "$marker_why" ]; then
|
||||
append "> ⚠️ Grimoire: the marker file \`${marker}\` ${marker_why}, so no project context was loaded. Fix the file, or ignore it and bind this directory another way."
|
||||
elif [ -n "$marker_id" ]; then
|
||||
append "> ⚠️ Grimoire: \`${marker}\` names project ${marker_id}, which this account cannot read on ${host} — it may belong to a different Grimoire instance, or the project may have been deleted. Check with \`list_projects()\` and correct the file."
|
||||
elif [ -z "$repo" ]; then
|
||||
append "> ℹ️ Grimoire: this directory is not a git repository and has no \`.scribe\` marker, so no project context was loaded — every hook this session is unscoped. If this work belongs to a Grimoire project, call \`list_projects()\` and write the marker: \`{\"instance\": \"${url%/}\", \"project_id\": <id>, \"project\": \"<title>\"}\` in \`${repo_dir}/.scribe\`. Future sessions here load that project on their own."
|
||||
append "> ℹ️ Grimoire: this directory is not a git repository and has no \`.grimoire\` marker, so no project context was loaded — every hook this session is unscoped. If this work belongs to a Grimoire project, call \`list_projects()\` and write the marker: \`{\"instance\": \"${url%/}\", \"project_id\": <id>, \"project\": \"<title>\"}\` in \`${repo_dir}/.grimoire\`. Future sessions here load that project on their own."
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -281,5 +281,5 @@ fi
|
||||
# Nothing at all to inject → stay silent.
|
||||
[ -n "$out" ] || exit 0
|
||||
|
||||
scribe_json_out SessionStart "$out"
|
||||
grimoire_json_out SessionStart "$out"
|
||||
exit 0
|
||||
@@ -21,29 +21,29 @@
|
||||
# session is ending whether this succeeds or not.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
event=$(cat 2>/dev/null || true)
|
||||
[ -n "$event" ] || exit 0
|
||||
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
[ -n "$session_id" ] || exit 0
|
||||
|
||||
# The moment arm's per-session files (its tool cache and the acts ledger)
|
||||
# describe a session that is over, on any exit — a clear
|
||||
# included, since the next conversation is a new session id.
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
rm -f "${TMPDIR:-/tmp}/scribe-moment/${safe_sid}".* 2>/dev/null || true
|
||||
rm -f "${TMPDIR:-/tmp}/grimoire-moment/${safe_sid}".* 2>/dev/null || true
|
||||
|
||||
reason=$(scribe_json_pick "$event_flat" '.reason')
|
||||
reason=$(grimoire_json_pick "$event_flat" '.reason')
|
||||
[ "$reason" = "clear" ] && exit 0
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
scribe_config || exit 0
|
||||
grimoire_config || exit 0
|
||||
|
||||
sid_enc=$(printf '%s' "$session_id" | scribe_urlenc) || exit 0
|
||||
sid_enc=$(printf '%s' "$session_id" | grimoire_urlenc) || exit 0
|
||||
curl -fsS --max-time 4 \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
"${url%/}/api/plugin/release-session?session_id=${sid_enc}" \
|
||||
@@ -2,15 +2,15 @@
|
||||
# Grimoire plugin — Stop hook: the turn's new shapes are judged by the agent
|
||||
# that wrote them.
|
||||
#
|
||||
# The write hooks (scribe_prior_art.sh, scribe_after_write.sh) append every
|
||||
# The write hooks (grimoire_prior_art.sh, grimoire_after_write.sh) append every
|
||||
# definition a write names to a session ledger, `<sid>.written.ids`
|
||||
# (`path<TAB>kind<TAB>name`, see scribe_written_append). At the end of the
|
||||
# (`path<TAB>kind<TAB>name`, see grimoire_written_append). At the end of the
|
||||
# turn this hook sends that ledger to the instance, which answers with what
|
||||
# nobody has judged yet. If anything is, the hook blocks ONCE, in the server's
|
||||
# words: the agent that built the code is the one participant who knows what
|
||||
# it is, and the end of the turn is the last moment that is still true.
|
||||
#
|
||||
# THE SAME DISCIPLINE AS the completion-section check in scribe_reply_check.sh:
|
||||
# THE SAME DISCIPLINE AS the completion-section check in grimoire_reply_check.sh:
|
||||
# - it blocks only on a `reason` the instance returned, which it returns
|
||||
# only for a block it RECORDED — an unconfigured or unreachable instance
|
||||
# never stops a session, and every intervention is one the numbers see;
|
||||
@@ -28,25 +28,25 @@
|
||||
# Config (same as the other hooks):
|
||||
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
|
||||
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# GRIMOIRE_URL / GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
set -uo pipefail
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
# Stop delivers { session_id, transcript_path, cwd, hook_event_name, stop_hook_active }.
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
active=$(scribe_json_pick "$event_flat" '.stop_hook_active')
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
active=$(grimoire_json_pick "$event_flat" '.stop_hook_active')
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
[ -n "$session_id" ] || exit 0
|
||||
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
ledger="${TMPDIR:-/tmp}/scribe-priorart/${safe_sid}.written.ids"
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-shapecheck"
|
||||
ledger="${TMPDIR:-/tmp}/grimoire-priorart/${safe_sid}.written.ids"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-shapecheck"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
marker="$state_dir/${safe_sid}.blocked"
|
||||
|
||||
@@ -63,15 +63,15 @@ if [ ! -s "$ledger" ]; then
|
||||
rm -f "$marker" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
scribe_config || exit 0
|
||||
grimoire_config || exit 0
|
||||
|
||||
# Unique lines, oldest first, capped: the request stays a GET (a read-scoped
|
||||
# key runs the plugin), and a turn that wrote more than this generated code.
|
||||
written=$(awk '!seen[$0]++' "$ledger" 2>/dev/null | head -n 80)
|
||||
written_enc=$(printf '%s' "$written" | scribe_urlenc) || exit 0
|
||||
written_enc=$(printf '%s' "$written" | grimoire_urlenc) || exit 0
|
||||
dir="${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}"
|
||||
q="phase=${phase}&written=${written_enc}"
|
||||
scope=$(scribe_scope_query "$dir")
|
||||
scope=$(grimoire_scope_query "$dir")
|
||||
[ -n "$scope" ] && q="${q}&${scope}"
|
||||
# The repo rides along even when a marker names the project: the instance
|
||||
# needs it to tell the agent which repo a just-written shape belongs to.
|
||||
@@ -80,7 +80,7 @@ case "$scope" in
|
||||
*)
|
||||
remote=$(git -C "$dir" remote get-url origin 2>/dev/null || true)
|
||||
if [ -n "$remote" ]; then
|
||||
remote_enc=$(printf '%s' "$remote" | scribe_urlenc) || remote_enc=""
|
||||
remote_enc=$(printf '%s' "$remote" | grimoire_urlenc) || remote_enc=""
|
||||
[ -n "$remote_enc" ] && q="${q}&repo=${remote_enc}"
|
||||
fi
|
||||
;;
|
||||
@@ -95,11 +95,11 @@ if [ "$phase" = "after" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
reason=$(scribe_json_pick "$(printf '%s' "$answer" | scribe_json_flat)" '.reason')
|
||||
reason=$(grimoire_json_pick "$(printf '%s' "$answer" | grimoire_json_flat)" '.reason')
|
||||
if [ -z "$reason" ]; then
|
||||
rm -f "$ledger" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
: > "$marker" 2>/dev/null || true
|
||||
printf '{"decision":"block","reason":"%s"}\n' "$(printf '%s' "$reason" | scribe_json_escape)"
|
||||
printf '{"decision":"block","reason":"%s"}\n' "$(printf '%s' "$reason" | grimoire_json_escape)"
|
||||
exit 0
|
||||
@@ -1,8 +1,8 @@
|
||||
# Grimoire — Claude Code adapter
|
||||
|
||||
This session is connected to **Grimoire**, the operator's system of record for
|
||||
their work, through the `scribe` MCP tools. How to work with Grimoire is stated
|
||||
once, in the **`using-scribe`** skill: reach for it at the start of the session
|
||||
their work, through the `grimoire` MCP tools. How to work with Grimoire is stated
|
||||
once, in the **`using-grimoire`** skill: reach for it at the start of the session
|
||||
and whenever you are unsure what Grimoire expects. Each tool's contract is in its
|
||||
description, and the process skills (writing-plans, reporting-back,
|
||||
reusing-code, systematic-debugging, verification, brainstorming,
|
||||
@@ -12,13 +12,13 @@ What only Claude Code needs said:
|
||||
|
||||
- **Keep one copy — in Grimoire, not Claude Code's local memory.** Claude
|
||||
Code's local memory is `CLAUDE.md` and auto-memory: the operator's rules,
|
||||
plans and project notes go to Grimoire instead, and using-scribe says how
|
||||
plans and project notes go to Grimoire instead, and using-grimoire says how
|
||||
Grimoire works alongside them.
|
||||
- **Lines injected beside your work are retrieval.** When the operator sends a
|
||||
message, and before a write or a command, Grimoire may add rules, preferences,
|
||||
notes and prior art that resemble what you are doing — and, when a tool call
|
||||
or the reply ending a turn reaches a moment of work, the rules mounted on
|
||||
it. Open the ones that apply; using-scribe says what a quiet turn means and
|
||||
it. Open the ones that apply; using-grimoire says what a quiet turn means and
|
||||
how to correct a moment that fired wrongly.
|
||||
- **Compact at clean seams.** Because work is recorded as you go, a compaction
|
||||
is safe once in-flight state is logged. After finishing a block of work in a
|
||||
@@ -27,9 +27,9 @@ What only Claude Code needs said:
|
||||
seams, not every turn.
|
||||
- **When the operator wraps up, hand off first.** Log where each task you
|
||||
worked stands before the session ends; the plugin releases your claims at
|
||||
session end, but only you can say what happened (using-scribe, "Hand off").
|
||||
- **Stored Processes arrive as skills** (`scribe-proc-*`), refreshed at session
|
||||
start. After a Process is added or edited, `/scribe:sync` makes it available
|
||||
session end, but only you can say what happened (using-grimoire, "Hand off").
|
||||
- **Stored Processes arrive as skills** (`grimoire-proc-*`), refreshed at session
|
||||
start. After a Process is added or edited, `/grimoire:sync` makes it available
|
||||
straight away.
|
||||
- **If the Grimoire tools are unavailable, say so** rather than silently falling
|
||||
back to local notes.
|
||||
@@ -2,7 +2,7 @@
|
||||
# Grimoire plugin — sync stored Processes into auto-surfacing local skills.
|
||||
#
|
||||
# Fetches `GET /api/plugin/processes` and writes one
|
||||
# ~/.claude/skills/scribe-proc-<slug>/SKILL.md
|
||||
# ~/.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).
|
||||
@@ -13,69 +13,69 @@
|
||||
# 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 `/scribe:sync` command.
|
||||
# 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 SCRIBE_URL / SCRIBE_TOKEN as the override.
|
||||
# with GRIMOIRE_URL / GRIMOIRE_TOKEN as the override.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
scribe_config || 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" | scribe_json_flat)
|
||||
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=$(scribe_json_len "$body_flat" '.processes')
|
||||
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 scribe-proc-* is pruned below.
|
||||
# Slugs written this run — anything else under grimoire-proc-* is pruned below.
|
||||
managed=" "
|
||||
|
||||
i=0
|
||||
while [ "$i" -lt "$count" ]; do
|
||||
name=$(scribe_json_pick "$body_flat" ".processes[$i].name")
|
||||
slug=$(scribe_json_pick "$body_flat" ".processes[$i].slug")
|
||||
desc=$(scribe_json_pick "$body_flat" ".processes[$i].description")
|
||||
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}/scribe-proc-${slug}"
|
||||
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: scribe-proc-%s\n' "$slug"
|
||||
printf 'name: grimoire-proc-%s\n' "$slug"
|
||||
printf 'description: %s\n' "$desc"
|
||||
printf -- '---\n\n'
|
||||
printf '<!-- GENERATED by the Grimoire plugin (scribe_sync_processes.sh). Do not edit here; edit the Process in Grimoire and re-sync with /scribe:sync. -->\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 scribe-proc-* prefix).
|
||||
for d in "${skills_dir}"/scribe-proc-*; do
|
||||
# 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#scribe-proc-}
|
||||
slug=$(basename "$d"); slug=${slug#grimoire-proc-}
|
||||
case "$managed" in
|
||||
*" $slug "*) : ;;
|
||||
*) rm -rf "$d" 2>/dev/null ;;
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grimoire — PreToolUse rule arm for ACTIONS.
|
||||
#
|
||||
# The sibling of scribe_prior_art.sh. That hook is registered on Write|Edit and
|
||||
# The sibling of grimoire_prior_art.sh. That hook is registered on Write|Edit and
|
||||
# asks "what is recorded about the file being written". This one asks "does a
|
||||
# standing rule speak to the command about to be run" — which is how a rule
|
||||
# about which tool to reach for arrives at all.
|
||||
@@ -13,41 +13,41 @@
|
||||
#
|
||||
# SILENT ON OUTAGE, deliberately, unlike the prior-art hook. A write is
|
||||
# occasional; a Bash call is not, and an "instance did not answer" line before
|
||||
# every command is the noise that gets a channel muted. scribe_prior_art.sh
|
||||
# every command is the noise that gets a channel muted. grimoire_prior_art.sh
|
||||
# still speaks for both when the instance is down.
|
||||
#
|
||||
# Env:
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
# GRIMOIRE_URL / GRIMOIRE_TOKEN override for the settings.json dogfooding path.
|
||||
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Sourced FIRST, because the JSON reader below lives there. It defines
|
||||
# functions and clears `url`/`token`; nothing here runs before it is needed.
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
||||
# shellcheck source=plugin/hooks/grimoire_defs.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/grimoire_defs.sh"
|
||||
|
||||
# PreToolUse delivers { session_id, cwd, tool_name, tool_input: {...}, ... }
|
||||
event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
tool_name=$(scribe_json_pick "$event_flat" '.tool_name')
|
||||
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
||||
event_flat=$(printf '%s' "$event" | grimoire_json_flat)
|
||||
tool_name=$(grimoire_json_pick "$event_flat" '.tool_name')
|
||||
session_id=$(grimoire_json_pick "$event_flat" '.session_id')
|
||||
event_cwd=$(grimoire_json_pick "$event_flat" '.cwd')
|
||||
|
||||
[ -n "$tool_name" ] || exit 0
|
||||
|
||||
# The action, as text. `.command` is Bash's field; the fallbacks let the matcher
|
||||
# in hooks.json widen to other tools without this script changing — which is the
|
||||
# whole reason the server side takes a name and a string rather than a schema.
|
||||
command_text=$(scribe_json_pick "$event_flat" '.tool_input.command')
|
||||
[ -n "$command_text" ] || command_text=$(scribe_json_pick "$event_flat" '.tool_input.url')
|
||||
[ -n "$command_text" ] || command_text=$(scribe_json_pick "$event_flat" '.tool_input.prompt')
|
||||
command_text=$(grimoire_json_pick "$event_flat" '.tool_input.command')
|
||||
[ -n "$command_text" ] || command_text=$(grimoire_json_pick "$event_flat" '.tool_input.url')
|
||||
[ -n "$command_text" ] || command_text=$(grimoire_json_pick "$event_flat" '.tool_input.prompt')
|
||||
|
||||
[ -n "$command_text" ] || exit 0
|
||||
|
||||
# scribe_config, not a hand-rolled pair of parameter expansions: it also treats
|
||||
# grimoire_config, not a hand-rolled pair of parameter expansions: it also treats
|
||||
# an UNEXPANDED `${...}` placeholder as unset, which would otherwise be sent as
|
||||
# a garbage Bearer token and 401 on every call.
|
||||
scribe_config || exit 0
|
||||
grimoire_config || exit 0
|
||||
|
||||
# Bounded before encoding: a heredoc or a pasted script can be enormous, and
|
||||
# the verb and its target — the part a rule is about — sit at the front. The
|
||||
@@ -57,14 +57,14 @@ command_text=$(printf '%s' "$command_text" | head -c 2000)
|
||||
|
||||
# Whole, never line by line: per-line encoding joins a multi-line command with
|
||||
# raw newlines — an invalid URL.
|
||||
# scribe_urlenc reads bytes and has no notion of a line.
|
||||
cmd_enc=$(printf '%s' "$command_text" | scribe_urlenc)
|
||||
tool_enc=$(printf '%s' "$tool_name" | scribe_urlenc)
|
||||
# grimoire_urlenc reads bytes and has no notion of a line.
|
||||
cmd_enc=$(printf '%s' "$command_text" | grimoire_urlenc)
|
||||
tool_enc=$(printf '%s' "$tool_name" | grimoire_urlenc)
|
||||
[ -n "$cmd_enc" ] && [ -n "$tool_enc" ] || exit 0
|
||||
|
||||
repo_q=""
|
||||
lookup_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
|
||||
scope=$(scribe_scope_query "$lookup_dir")
|
||||
scope=$(grimoire_scope_query "$lookup_dir")
|
||||
[ -n "$scope" ] && repo_q="&${scope}"
|
||||
|
||||
# Where the command runs, against the repo's root: the paths a
|
||||
@@ -73,12 +73,12 @@ scope=$(scribe_scope_query "$lookup_dir")
|
||||
where_q=""
|
||||
repo_root=$(git -C "$lookup_dir" rev-parse --show-toplevel 2>/dev/null || true)
|
||||
if [ -n "$repo_root" ]; then
|
||||
where_q="&root=$(printf '%s' "$repo_root" | scribe_urlenc)&cwd=$(printf '%s' "$lookup_dir" | scribe_urlenc)"
|
||||
where_q="&root=$(printf '%s' "$repo_root" | grimoire_urlenc)&cwd=$(printf '%s' "$lookup_dir" | grimoire_urlenc)"
|
||||
fi
|
||||
|
||||
# THE SHARED SESSION LEDGER, and the thing most worth getting right here.
|
||||
#
|
||||
# scribe_prior_art.sh keeps the rules it has already named in
|
||||
# grimoire_prior_art.sh keeps the rules it has already named in
|
||||
# <state>/<sid>.rules.ids and passes them as exclude_rule_ids. This hook reads
|
||||
# and appends to that SAME file rather than keeping its own: two ledgers would
|
||||
# mean a rule named by one arm gets re-offered by the other, and the hint that
|
||||
@@ -86,7 +86,7 @@ fi
|
||||
#
|
||||
# The directory keeps the prior-art name on purpose — renaming it would orphan
|
||||
# every live session's state for a cosmetic gain.
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
||||
state_dir="${TMPDIR:-/tmp}/grimoire-priorart"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
rulefile=""
|
||||
rule_exclude_q=""
|
||||
@@ -96,15 +96,15 @@ if [ -n "$session_id" ]; then
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
rulefile="$state_dir/${safe_sid}.rules.ids"
|
||||
# Ageing, not a flat read: an id named two hours ago is not one the
|
||||
# session is still holding. scribe_rules_live carries the reasoning.
|
||||
rule_seen=$(scribe_rules_live "$rulefile")
|
||||
# session is still holding. grimoire_rules_live carries the reasoning.
|
||||
rule_seen=$(grimoire_rules_live "$rulefile")
|
||||
[ -n "$rule_seen" ] && rule_exclude_q="&exclude_rule_ids=${rule_seen}"
|
||||
# What the session actually OPENED, as against what it was shown.
|
||||
rule_exclude_q="${rule_exclude_q}$(scribe_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
rule_exclude_q="${rule_exclude_q}$(grimoire_held_query "$state_dir/${safe_sid}.opened.ids")"
|
||||
# The Systems whose rulings were shown in full — shared with the write-path
|
||||
# hook, for the reason the rule file is.
|
||||
rulingsfile="$state_dir/${safe_sid}.rulings.ids"
|
||||
rulings_q=$(scribe_rulings_query "$rulingsfile")
|
||||
rulings_q=$(grimoire_rulings_query "$rulingsfile")
|
||||
fi
|
||||
|
||||
# `|| exit 0` here, unlike the prior-art hook: there is no local arm whose
|
||||
@@ -114,8 +114,8 @@ body=$(curl -fsS --max-time 5 \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
"${url%/}/api/plugin/tool-rules?tool=${tool_enc}&command=${cmd_enc}${repo_q}${where_q}${rule_exclude_q}${rulings_q}" 2>/dev/null) || exit 0
|
||||
|
||||
body_flat=$(printf '%s' "$body" | scribe_json_flat)
|
||||
context=$(scribe_json_pick "$body_flat" '.context')
|
||||
body_flat=$(printf '%s' "$body" | grimoire_json_flat)
|
||||
context=$(grimoire_json_pick "$body_flat" '.context')
|
||||
|
||||
# Remember what was named so it is not repeated this session. BEFORE the
|
||||
# checkpoint branch and before the empty-context exit: the ledger records what
|
||||
@@ -123,15 +123,15 @@ context=$(scribe_json_pick "$body_flat" '.context')
|
||||
# renders it. Doing it inside one branch is how the two arms' ledgers came to
|
||||
# disagree once already.
|
||||
if [ -n "$rulefile" ]; then
|
||||
scribe_json_list "$body_flat" '.rule_ids' | scribe_rules_append "$rulefile"
|
||||
grimoire_json_list "$body_flat" '.rule_ids' | grimoire_rules_append "$rulefile"
|
||||
fi
|
||||
if [ -n "$rulingsfile" ]; then
|
||||
scribe_json_list "$body_flat" '.ruling_system_ids' >> "$rulingsfile" || true
|
||||
grimoire_json_list "$body_flat" '.ruling_system_ids' >> "$rulingsfile" || true
|
||||
fi
|
||||
|
||||
# ── The pre-act checkpoint ───────────────────────────────────────────────
|
||||
#
|
||||
# WHY THIS ARM AND NOT THE WRITE PATH. scribe_prior_art.sh carries an explicit,
|
||||
# WHY THIS ARM AND NOT THE WRITE PATH. grimoire_prior_art.sh carries an explicit,
|
||||
# tested property that it never returns a permissionDecision — a recall aid may
|
||||
# not stand in the way of a write, which is the operator's decision and is
|
||||
# guarded by test_hook_never_returns_a_permission_decision. No equivalent
|
||||
@@ -147,18 +147,18 @@ fi
|
||||
# exists. It costs the operator nothing: no prompt reaches them, and the model
|
||||
# clears it by reading one record and re-submitting.
|
||||
#
|
||||
# It cannot recur. `scribe_checkpoint_allowed` holds at most one act per rule
|
||||
# It cannot recur. `grimoire_checkpoint_allowed` holds at most one act per rule
|
||||
# and at most five per session, so the worst case of a mis-set floor is a noisy
|
||||
# session rather than one that cannot move.
|
||||
cp_rule=$(scribe_json_pick "$body_flat" '.checkpoint.rule_id')
|
||||
cp_reason=$(scribe_json_pick "$body_flat" '.checkpoint.reason')
|
||||
cp_rule=$(grimoire_json_pick "$body_flat" '.checkpoint.rule_id')
|
||||
cp_reason=$(grimoire_json_pick "$body_flat" '.checkpoint.reason')
|
||||
if [ -n "$cp_rule" ] && [ -n "$cp_reason" ] && [ -n "$session_id" ]; then
|
||||
if scribe_checkpoint_allowed "$state_dir/${safe_sid}.checkpoint.ids" "$cp_rule"; then
|
||||
scribe_json_deny PreToolUse "$cp_reason"
|
||||
if grimoire_checkpoint_allowed "$state_dir/${safe_sid}.checkpoint.ids" "$cp_rule"; then
|
||||
grimoire_json_deny PreToolUse "$cp_reason"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
[ -n "$context" ] || exit 0
|
||||
scribe_json_out PreToolUse "$context"
|
||||
grimoire_json_out PreToolUse "$context"
|
||||
exit 0
|
||||
@@ -1,8 +1,8 @@
|
||||
# Grimoire plugin — what happened in the LAST TURN of a transcript.
|
||||
#
|
||||
# Reads the flat `IDX<TAB>PATH<TAB>VALUE` stream that scribe_json.awk produces
|
||||
# Reads the flat `IDX<TAB>PATH<TAB>VALUE` stream that grimoire_json.awk produces
|
||||
# in `mode=lines` from a Claude Code transcript, and answers the four questions
|
||||
# the Stop hooks ask (scribe_reply_check.sh). It replaces a thirty-line jq program; the shape
|
||||
# the Stop hooks ask (grimoire_reply_check.sh). It replaces a thirty-line jq program; the shape
|
||||
# of the answer is unchanged, so the hook around it reads the same.
|
||||
#
|
||||
# bounded 1 if a user prompt was found in the window, else empty. A window
|
||||
@@ -12,7 +12,7 @@
|
||||
# closed how many task-closing tool calls SUCCEEDED in that turn.
|
||||
# task_ids their task ids, comma-joined, for the server to record.
|
||||
# reply the assistant text after the last action, STILL JSON-ESCAPED and
|
||||
# on one line. The caller decodes it with scribe_json_unescape —
|
||||
# on one line. The caller decodes it with grimoire_json_unescape —
|
||||
# decoding here would put newlines into a line-oriented format.
|
||||
#
|
||||
# A SIDECHAIN IS NOT THIS SESSION. Subagent records interleave into the same
|
||||
+14
-14
@@ -5,11 +5,11 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_session_context.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_session_context.sh\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_sync_processes.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_sync_processes.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_autoinject.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_autoinject.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_prior_art.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_prior_art.sh\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -39,7 +39,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_tool_rules.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_tool_rules.sh\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -48,7 +48,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_moment.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_moment.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -59,7 +59,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_after_write.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_after_write.sh\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -68,7 +68,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_record_opened.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_record_opened.sh\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -77,7 +77,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_record_outcome.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_record_outcome.sh\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -86,7 +86,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_claim_session.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_claim_session.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -96,7 +96,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_precompact_preserve.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_precompact_preserve.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -106,11 +106,11 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_reply_check.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_reply_check.sh\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_shape_check.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_shape_check.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -120,7 +120,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_session_end.sh\""
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/grimoire_session_end.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: using-scribe
|
||||
name: using-grimoire
|
||||
description: Use at the START of every session, and before answering anything about the operator's work or starting any task — establishes the Grimoire-first reflex. You hold none of the operator's rules: they arrive by retrieval when your work matches one, and what_might_apply is how you ask before a consequential act — it returns the wide net of candidates with no bar. Call enter_project when a repo/project is in scope. Then recall before acting, update over duplicate, plan in Grimoire not in files.
|
||||
metadata:
|
||||
moments: session.start
|
||||
@@ -9,7 +9,7 @@ metadata:
|
||||
|
||||
Grimoire is the operator's self-hosted system of record (notes, tasks, issues,
|
||||
projects, milestones, systems) and rulebook, reachable
|
||||
through the bundled `scribe` MCP server. Its value is mostly in what it
|
||||
through the bundled `grimoire` MCP server. Its value is mostly in what it
|
||||
**already holds** — so make reading it a reflex, not something you wait to be
|
||||
asked for.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# When a record doesn't reach the moment it should
|
||||
|
||||
Part of the using-scribe skill. Read it when a rule should have governed a
|
||||
Part of the using-grimoire skill. Read it when a rule should have governed a
|
||||
moment and never arrived, when one arrives on every turn and never applies,
|
||||
or before touching a retrieval floor.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Moments — rules that arrive when the work reaches a point
|
||||
|
||||
Part of the using-scribe skill. Read it when a line says a rule arrived *at* a
|
||||
Part of the using-grimoire skill. Read it when a line says a rule arrived *at* a
|
||||
moment, when a reply is held for one read, when an action reached the wrong
|
||||
moment or none, when a mounted rule arrived and did not apply, and when a line
|
||||
proposes mounting or unmounting a rule.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Projects: binding a directory, and starting one
|
||||
|
||||
Part of the using-scribe skill. Read it when a working directory maps to no
|
||||
Part of the using-grimoire skill. Read it when a working directory maps to no
|
||||
project, or when a new project is about to be created.
|
||||
|
||||
## Binding a directory to a project
|
||||
@@ -8,9 +8,9 @@ project, or when a new project is about to be created.
|
||||
**A directory does not have to be a git repo to have a project.** A repo is
|
||||
bound by its remote (`list_repo_bindings` shows the bindings). Anything else —
|
||||
a notes folder, a server's config directory, a scratch directory — is bound by
|
||||
a `.scribe` file naming the project:
|
||||
a `.grimoire` file naming the project:
|
||||
|
||||
{"instance": "https://scribe.example.com", "project_id": 2, "project": "Homelab"}
|
||||
{"instance": "https://grimoire.example.com", "project_id": 2, "project": "Homelab"}
|
||||
|
||||
`instance` is what makes the id trustworthy. An id means nothing on its own —
|
||||
it is a different project on every Grimoire — so a marker that has travelled to
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Writing a rule, a lesson, or a note that asserts a fact
|
||||
|
||||
Part of the using-scribe skill. Read it before `create_rule`,
|
||||
Part of the using-grimoire skill. Read it before `create_rule`,
|
||||
`create_project_rule`, `create_preference`, `create_process` or
|
||||
`create_lesson`; when a lesson
|
||||
arrives that names the situation you are actually in; when the operator
|
||||
Reference in New Issue
Block a user