rename(547): what people read says Grimoire
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
Step 1 of the Scribe -> Grimoire rename: every capitalised "Scribe" in prose
becomes "Grimoire" ("Fabled Scribe" -> "Fabled Grimoire") across the UI,
the PWA manifest and page title, the MCP instructions and tool docstrings,
hook and skill text, docs and README, with the tests that assert on that
prose moving with it. Old migrations keep their wording.
Identifiers are untouched here: the package, plugin name, MCP server key,
hook file names, env vars, the .scribe marker and FabledScribe repo/image
names move in steps 2 and 3, along with prose that quotes them.
The sweep matched "Scribe" only as a whole word, so describe/subscribe and
friends are untouched. The longer name took the MCP instructions one
character over their 1,600 budget; two phrases are shortened to fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "scribe-plugin",
|
||||
"owner": { "name": "Bryan Van Deusen" },
|
||||
"description": "Scribe ships its own Claude Code plugin from this repo, versioned in lockstep with the app + the /api/plugin/context contract.",
|
||||
"description": "Grimoire ships its own Claude Code plugin from this repo, versioned in lockstep with the app + the /api/plugin/context contract.",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "scribe",
|
||||
"source": "./plugin",
|
||||
"description": "Scribe second brain: MCP tools + session-start push channel + universal process-skills."
|
||||
"description": "Grimoire second brain: MCP tools + session-start push channel + universal process-skills."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
# version check diffs two trees, so a depth-1 fetch of main is enough.
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Per-job rather than in the image: Scribe is shellcheck's only consumer.
|
||||
# Per-job rather than in the image: Grimoire is shellcheck's only consumer.
|
||||
# Install nothing else here; the hooks use only POSIX tools, and the
|
||||
# smoke test runs on a bare image to prove it.
|
||||
- name: Install shell tooling
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Fabled Scribe
|
||||
# Fabled Grimoire
|
||||
|
||||
A self-hosted work system-of-record for software projects, built to be driven by Claude Code. Notes, tasks, issues, projects, milestones, rules, and stored processes — reachable from Claude via a built-in MCP endpoint and a bundled Claude Code plugin, with a clean web UI for humans. No in-app LLM; Claude is the sole assistant.
|
||||
|
||||
@@ -34,7 +34,7 @@ Open `http://localhost:5000`. The first user to register becomes admin. To conne
|
||||
| [API Keys & MCP](docs/api-keys-and-mcp.md) | API keys and installing the Claude Code plugin |
|
||||
| [SSO / OAuth](docs/sso-oauth.md) | OIDC setup for Authentik, Keycloak, and other providers |
|
||||
| [Backups](docs/backups.md) | Scheduled backups to a folder or S3, and restoring from one |
|
||||
| [Design system](docs/design-system.md) | The FabledSword house style and how Scribe applies it |
|
||||
| [Design system](docs/design-system.md) | The FabledSword house style and how Grimoire applies it |
|
||||
| [API Reference](docs/api-reference.md) | All REST API endpoints |
|
||||
|
||||
## License
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ Anything CI installs at job time that isn't in the image. Promotion
|
||||
candidates if more than one project needs them.
|
||||
|
||||
- `shellcheck` — apt-installed in the **plugin** job, which lints the
|
||||
Claude Code hook scripts and runs their fail-open smoke test. Scribe is
|
||||
Claude Code hook scripts and runs their fail-open smoke test. Grimoire is
|
||||
its only consumer, so per `docs/process.md` it stays per-job. It is small
|
||||
(~20 MB) and a promotion candidate once another project lints shell.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- scribe_backend
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Scribe needs.
|
||||
# Postgres 17 with the vector extension Grimoire needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
volumes:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Fabled Scribe from the pre-built image:
|
||||
# Fabled Grimoire from the pre-built image:
|
||||
# docker compose -f docker-compose.quickstart.yml up -d
|
||||
# Then open http://localhost:5000 (the first account registered becomes admin)
|
||||
# and connect Claude from Settings → MCP Access.
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Scribe needs.
|
||||
# Postgres 17 with the vector extension Grimoire needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
volumes:
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ services:
|
||||
# SEARXNG_URL: "http://searxng:8080"
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Scribe needs.
|
||||
# Postgres 17 with the vector extension Grimoire needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# API Keys and Scribe MCP
|
||||
# API Keys and Grimoire MCP
|
||||
|
||||
## API Keys
|
||||
|
||||
API keys let external tools access your Scribe data without a browser session. Each key is scoped to a single user — it can only access data that user owns or has been shared with them.
|
||||
API keys let external tools access your Grimoire data without a browser session. Each key is scoped to a single user — it can only access data that user owns or has been shared with them.
|
||||
|
||||
### Scopes
|
||||
|
||||
@@ -22,7 +22,7 @@ Admin-level operations (log access, user management) require a `write`-scoped ke
|
||||
5. Copy the key immediately — it is shown only once (the token is `fmcp_`-prefixed)
|
||||
|
||||
Paste the key into the `Authorization: Bearer <key>` header of your MCP client
|
||||
config (see **Scribe MCP Server** below).
|
||||
config (see **Grimoire MCP Server** below).
|
||||
|
||||
### Revoking a Key
|
||||
|
||||
@@ -30,12 +30,12 @@ Click **Revoke** next to the key in the keys table and confirm. Revoked keys are
|
||||
|
||||
---
|
||||
|
||||
## Scribe MCP Server
|
||||
## Grimoire MCP Server
|
||||
|
||||
Scribe exposes itself as a set of MCP tools that Claude (and other MCP clients)
|
||||
Grimoire exposes itself as a set of MCP tools that Claude (and other MCP clients)
|
||||
can use to read and write your notes, tasks, projects, rulebooks, and more. The
|
||||
server is **built into the app** — it is mounted as a streamable-HTTP endpoint
|
||||
at **`/mcp`** on the running Scribe instance (`src/scribe/mcp/server.py`). There
|
||||
at **`/mcp`** on the running Grimoire instance (`src/scribe/mcp/server.py`). There
|
||||
is nothing to install: no wheel, no separate package, no CLI. You connect a
|
||||
client straight to the URL with a Bearer token.
|
||||
|
||||
@@ -50,14 +50,14 @@ without a read-shaped name are spelled out here. A `write`-scoped key may call e
|
||||
|
||||
### Claude Code: the plugin (recommended)
|
||||
|
||||
The plugin connects the MCP server and adds the hooks and skills that put Scribe's records in front of Claude as it works. Install it from this repo:
|
||||
The plugin connects the MCP server and adds the hooks and skills that put Grimoire's records in front of Claude as it works. Install it from this repo:
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledScribe.git
|
||||
/plugin install scribe@scribe-plugin
|
||||
```
|
||||
|
||||
It asks for your Scribe base URL, an API key (kept in your OS keychain) and, optionally, a project id. Settings → MCP Access shows the exact command for your instance. [`plugin/README.md`](../plugin/README.md) describes each hook.
|
||||
It asks for your Grimoire base URL, an API key (kept in your OS keychain) and, optionally, a project id. Settings → MCP Access shows the exact command for your instance. [`plugin/README.md`](../plugin/README.md) describes each hook.
|
||||
|
||||
### Claude Code without the plugin
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ These serve the plugin's hooks.
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/webhooks/forge` | Push events from a git forge, verified with `FORGE_WEBHOOK_SECRET` **(public)** |
|
||||
|
||||
## Scribe MCP
|
||||
## Grimoire MCP
|
||||
|
||||
Claude clients connect to the built-in MCP server at `POST /mcp` (streamable HTTP,
|
||||
Bearer auth with an `fmcp_` key), served by `src/scribe/mcp/`. It is not a REST
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Architecture
|
||||
|
||||
Scribe is one app container and one PostgreSQL container. There is no language model in the stack: Claude works with Scribe from outside, through the MCP endpoint and the Claude Code plugin, and people use the web UI.
|
||||
Grimoire is one app container and one PostgreSQL container. There is no language model in the stack: Claude works with Grimoire from outside, through the MCP endpoint and the Claude Code plugin, and people use the web UI.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -74,7 +74,7 @@ Every database wait on startup has a deadline. Migrations wait for the database
|
||||
- **OIDC** single sign-on with PKCE, optionally replacing local logins ([SSO / OAuth](sso-oauth.md)).
|
||||
- **Rate limits** on register, login and forgot-password, per client IP (`TRUST_PROXY_HEADERS` reads the IP from the proxy's headers).
|
||||
|
||||
Credentials Scribe stores for outside services (forge tokens, the backup destination's S3 secret) are sealed under a key derived from `SECRET_KEY`.
|
||||
Credentials Grimoire stores for outside services (forge tokens, the backup destination's S3 secret) are sealed under a key derived from `SECRET_KEY`.
|
||||
|
||||
## Repository layout
|
||||
|
||||
|
||||
+17
-17
@@ -1,7 +1,7 @@
|
||||
# Backups
|
||||
|
||||
Scribe can back itself up every day to a **folder** or an **S3-compatible
|
||||
bucket**. Each run adds one file; Scribe never deletes an old one. Keeping and
|
||||
Grimoire can back itself up every day to a **folder** or an **S3-compatible
|
||||
bucket**. Each run adds one file; Grimoire never deletes an old one. Keeping and
|
||||
thinning the history is the destination's job.
|
||||
|
||||
Set it up under **Settings → Data → Scheduled backups** (admins only).
|
||||
@@ -16,14 +16,14 @@ scribe-20261015T020000Z.tar
|
||||
|
||||
It is a plain, **uncompressed** tar:
|
||||
|
||||
- `manifest.json` comes first. It holds the backup format version, the Scribe
|
||||
- `manifest.json` comes first. It holds the backup format version, the Grimoire
|
||||
version, when the backup was taken, and how many rows each table has.
|
||||
- Then there is one `<table>.jsonl` per table, one row per line.
|
||||
|
||||
It holds everything the admin **Full Backup** download holds, laid out
|
||||
differently. It does **not** hold:
|
||||
|
||||
- **Search indexes.** They are rebuilt from the records when Scribe starts.
|
||||
- **Search indexes.** They are rebuilt from the records when Grimoire starts.
|
||||
- **Sharing:** groups, project shares and note shares.
|
||||
- **Credentials:** API keys and forge connections. After a restore, issue new
|
||||
API keys and add forge connections again. (Forge tokens are also sealed under
|
||||
@@ -61,7 +61,7 @@ file again.
|
||||
|
||||
### A folder
|
||||
|
||||
A path **inside the Scribe container**, normally a share mounted there:
|
||||
A path **inside the Grimoire container**, normally a share mounted there:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
- /nfs/backup/scribe:/backups
|
||||
```
|
||||
|
||||
Scribe writes each file under a hidden name (`.scribe-….tar.part`) and renames
|
||||
Grimoire writes each file under a hidden name (`.scribe-….tar.part`) and renames
|
||||
it once it is complete. A program watching the folder never sees half a
|
||||
backup.
|
||||
|
||||
@@ -79,12 +79,12 @@ backup.
|
||||
You need four things:
|
||||
|
||||
- **Address:** the service itself, like `https://backup.example.com`. Don't
|
||||
include the bucket; Scribe addresses buckets by path.
|
||||
include the bucket; Grimoire addresses buckets by path.
|
||||
- **Bucket.**
|
||||
- **Access key.**
|
||||
- **Secret key.**
|
||||
|
||||
Scribe uploads and lists; it never deletes. A key that may **only add
|
||||
Grimoire uploads and lists; it never deletes. A key that may **only add
|
||||
objects** is therefore enough, and it is the safer key to hand out: a stolen
|
||||
one cannot wipe the history.
|
||||
|
||||
@@ -115,21 +115,21 @@ page asks for the secret again.
|
||||
[Fabled Archives](https://git.fabledsword.com/bvandeusen/fabledarchives)
|
||||
keeps service backups as "backup targets". Either destination works with it.
|
||||
|
||||
1. On Archives' **Backups** page, make a target for Scribe, named `scribe`
|
||||
1. On Archives' **Backups** page, make a target for Grimoire, named `scribe`
|
||||
for example.
|
||||
2. Choose how Scribe's files arrive:
|
||||
2. Choose how Grimoire's files arrive:
|
||||
- **S3**: turn on the S3 way in for the target and make an **append-only**
|
||||
S3 key. In Scribe, choose **An S3 bucket**:
|
||||
S3 key. In Grimoire, choose **An S3 bucket**:
|
||||
- address: Archives' backup address (`http://<archives>:8081`);
|
||||
- bucket: the target's name;
|
||||
- access key and secret key: the key you just made.
|
||||
- **A folder**: mount the share Archives watches into Scribe as
|
||||
`/backups/scribe`, and point the target at that folder. In Scribe, choose
|
||||
- **A folder**: mount the share Archives watches into Grimoire as
|
||||
`/backups/scribe`, and point the target at that folder. In Grimoire, choose
|
||||
**A folder** with `/backups/scribe`.
|
||||
3. Set the target's **Retention** to **Archives**. Archives then keeps the
|
||||
newest backups and thins the rest by day, week and month. Scribe only adds.
|
||||
newest backups and thins the rest by day, week and month. Grimoire only adds.
|
||||
4. Give the target an expected cadence of **daily**, so a missed backup turns
|
||||
it late in Archives as well as overdue in Scribe.
|
||||
it late in Archives as well as overdue in Grimoire.
|
||||
5. Add an off-site **restic** replication job for the target. The uncompressed
|
||||
layout above is what lets that job send only what changed each day.
|
||||
|
||||
@@ -137,7 +137,7 @@ keeps service backups as "backup targets". Either destination works with it.
|
||||
|
||||
### Into a fresh instance
|
||||
|
||||
Start Scribe on an empty database, copy the backup file into the container,
|
||||
Start Grimoire on an empty database, copy the backup file into the container,
|
||||
and run:
|
||||
|
||||
```sh
|
||||
@@ -150,7 +150,7 @@ docker exec -it scribe python -m scribe.restore /backups/scribe-20261015T020000Z
|
||||
- Nothing limits the file's size.
|
||||
- It also reads the JSON from the admin **Full Backup** download.
|
||||
|
||||
Afterwards, **restart Scribe** so the search indexes are rebuilt. Then issue
|
||||
Afterwards, **restart Grimoire** so the search indexes are rebuilt. Then issue
|
||||
new API keys, add forge connections again, and set up any sharing.
|
||||
|
||||
### From the web
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Configuration
|
||||
|
||||
Scribe reads its configuration from environment variables. Most settings people change day to day (registration, SMTP, retrieval thresholds, backups, forge connections) live in **Settings** in the web UI instead.
|
||||
Grimoire reads its configuration from environment variables. Most settings people change day to day (registration, SMTP, retrieval thresholds, backups, forge connections) live in **Settings** in the web UI instead.
|
||||
|
||||
Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to the path, for example a Docker secret at `/run/secrets/scribe_key`.
|
||||
|
||||
@@ -13,7 +13,7 @@ Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to
|
||||
| `DATABASE_URL` *(secret)* | `postgresql+asyncpg://scribe:scribe@localhost:5432/scribe` | PostgreSQL connection string, using the `asyncpg` driver |
|
||||
| `SECRET_KEY` *(secret)* | `dev-secret-change-me` | Signs sessions and seals stored credentials. Set it before first use and keep it (see below) |
|
||||
| `BASE_URL` | `http://localhost:5000` | Public URL, used in email links and OIDC redirects |
|
||||
| `SECURE_COOKIES` | `false` | `true` when Scribe is served over HTTPS |
|
||||
| `SECURE_COOKIES` | `false` | `true` when Grimoire is served over HTTPS |
|
||||
| `TRUST_PROXY_HEADERS` | `false` | `true` behind a reverse proxy, to read the client IP from `X-Forwarded-For` / `X-Real-IP` |
|
||||
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING` or `ERROR` |
|
||||
| `LOG_RETENTION_DAYS` | `90` | Days of app logs to keep |
|
||||
@@ -42,7 +42,7 @@ Admins can set SMTP in **Settings**; these are the defaults until they do.
|
||||
| `SMTP_USERNAME` | — |
|
||||
| `SMTP_PASSWORD` *(secret)* | — |
|
||||
| `SMTP_FROM_ADDRESS` | — |
|
||||
| `SMTP_FROM_NAME` | `Fabled Scribe` |
|
||||
| `SMTP_FROM_NAME` | `Fabled Grimoire` |
|
||||
| `SMTP_USE_TLS` | `true` |
|
||||
|
||||
### Integrations
|
||||
@@ -86,14 +86,14 @@ The shipped files leave out healthchecks and restart policies on purpose. The ap
|
||||
|
||||
### Volumes
|
||||
|
||||
- **Database** (`pgdata`): everything Scribe stores.
|
||||
- **Database** (`pgdata`): everything Grimoire stores.
|
||||
- **App data** (`/data`): the embedding model cache and diagnostics. Losing it costs a re-download, nothing else.
|
||||
|
||||
Back up the database with **Settings → Data → Scheduled backups** ([Backups](backups.md)).
|
||||
|
||||
## Behind a reverse proxy
|
||||
|
||||
Scribe doesn't terminate TLS. Put it behind Nginx, Traefik or Caddy, forward to port 5000, and keep port 5000 off the internet. Set `SECURE_COOKIES=true`, `TRUST_PROXY_HEADERS=true` and `BASE_URL`.
|
||||
Grimoire doesn't terminate TLS. Put it behind Nginx, Traefik or Caddy, forward to port 5000, and keep port 5000 off the internet. Set `SECURE_COOKIES=true`, `TRUST_PROXY_HEADERS=true` and `BASE_URL`.
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
@@ -114,7 +114,7 @@ With `TRUST_PROXY_HEADERS=true`, make sure the proxy replaces any `X-Forwarded-F
|
||||
## Security checklist
|
||||
|
||||
- **Set a strong `SECRET_KEY`, and keep it.** Forge tokens and the backup destination's S3 secret are sealed under a key derived from it, so a copy of the database alone can't read them. If the key changes (say, a restore onto an instance with a different key), Settings marks those credentials and asks for them again; nothing else breaks. The app logs a warning at startup while the default key is in use, and refuses to start with it when `SECURE_COOKIES=true`.
|
||||
- **`SECURE_COOKIES=true`** whenever Scribe is served over HTTPS.
|
||||
- **`SECURE_COOKIES=true`** whenever Grimoire is served over HTTPS.
|
||||
- **Registration** is open only for the first account. Invite people, or open it briefly from Settings.
|
||||
- **Signing everyone else out**: changing a password ends that user's other sessions; Settings → Account has a button for it too.
|
||||
- **API keys** are per user, shown once, and can be read-only. Revoke any you no longer use under Settings → MCP Access.
|
||||
|
||||
+10
-10
@@ -53,7 +53,7 @@ Parchment is intentionally not pure white — it's slightly warm to feel like ag
|
||||
| Bronze | `#8B7355` | Secondary action buttons (Cancel-but-not-destructive, alternate paths) |
|
||||
| Pewter | `#3F4651` | Tertiary/ghost buttons |
|
||||
|
||||
**Critical rule:** Action button colors are universal across all apps. A Save button in Scribe and a Save button in Minstrel look identical. Per-app accents do not appear on buttons.
|
||||
**Critical rule:** Action button colors are universal across all apps. A Save button in Grimoire and a Save button in Minstrel look identical. Per-app accents do not appear on buttons.
|
||||
|
||||
### Semantic colors
|
||||
|
||||
@@ -71,7 +71,7 @@ Parchment is intentionally not pure white — it's slightly warm to feel like ag
|
||||
|
||||
| App | Hex | Mood |
|
||||
|---|---|---|
|
||||
| Fabled Scribe | `#5B4A8A` | Dusty violet — ink, manuscripts |
|
||||
| Fabled Grimoire | `#5B4A8A` | Dusty violet — ink, manuscripts |
|
||||
| Minstrel | `#4A6B5C` | Forest teal — music, performance |
|
||||
| Fabled Forge | `#8B5A2B` | Forge bronze — creation, craft |
|
||||
| Roundtable | `#4A5D7E` | Slate blue — stewardship, infrastructure |
|
||||
@@ -80,7 +80,7 @@ Parchment is intentionally not pure white — it's slightly warm to feel like ag
|
||||
**Accent usage rules:**
|
||||
- The accent appears on the app's wordmark and icon.
|
||||
- The accent indicates active/current state in nav (the selected page, the active tab).
|
||||
- The accent is the cursor color and text-selection color in long-form surfaces (Scribe notes, Forge story drafts).
|
||||
- The accent is the cursor color and text-selection color in long-form surfaces (Grimoire notes, Forge story drafts).
|
||||
- The accent does NOT appear on primary or secondary action buttons.
|
||||
- The accent does NOT appear in body text or chrome.
|
||||
- One accent per app. Don't mix accents within a single app.
|
||||
@@ -124,7 +124,7 @@ Fraunces is a contemporary serif with personality — it has the warmth and auth
|
||||
- **Sentence case everywhere.** Never Title Case for headings, never ALL CAPS except for the Tiny micro-label style.
|
||||
- **Two weights only:** 400 regular and 500 medium. Never 600 or 700 — they read heavy in dark mode.
|
||||
- **Fraunces only at 18px and above.** Below that it loses too much detail and feels fragile. For h3 and below, use Inter.
|
||||
- **Line height** 1.5 for body, 1.3 for headings, 1.7 for long-form reading surfaces (Scribe notes, Forge drafts).
|
||||
- **Line height** 1.5 for body, 1.3 for headings, 1.7 for long-form reading surfaces (Grimoire notes, Forge drafts).
|
||||
- **Letter-spacing** at default for everything except the Tiny micro-label, which gets `0.08em` letter-spacing and uppercase styling.
|
||||
|
||||
---
|
||||
@@ -173,7 +173,7 @@ Padding: `8px 16px` for default, `6px 12px` for compact, `10px 20px` for promine
|
||||
|
||||
Used for tags, hashtags, code spans, status badges. Background is the accent color at ~15% opacity, text is the accent at full strength. Border-radius 4px, padding `2px 8px`, Inter 11px/500.
|
||||
|
||||
In Scribe specifically, hashtags and tags use the dusty violet accent. In Minstrel, they'd use forest teal. The pattern is shared; the color follows the app.
|
||||
In Grimoire specifically, hashtags and tags use the dusty violet accent. In Minstrel, they'd use forest teal. The pattern is shared; the color follows the app.
|
||||
|
||||
### Cards
|
||||
|
||||
@@ -272,9 +272,9 @@ Use **Lucide** (https://lucide.dev) as the base icon set — it matches this sty
|
||||
|
||||
## Per-app application
|
||||
|
||||
### Fabled Scribe (#5B4A8A — dusty violet)
|
||||
### Fabled Grimoire (#5B4A8A — dusty violet)
|
||||
|
||||
A second-brain notes and task management tool. The accent appears in: the wordmark, hashtags and tag pills, the active nav item, text selection color, and the cursor in the editor. Notes are presented on Iron-surfaced cards with generous reading line-height. The hashtag system uses Scribe's accent for visual continuity.
|
||||
A second-brain notes and task management tool. The accent appears in: the wordmark, hashtags and tag pills, the active nav item, text selection color, and the cursor in the editor. Notes are presented on Iron-surfaced cards with generous reading line-height. The hashtag system uses Grimoire's accent for visual continuity.
|
||||
|
||||
### Minstrel (#4A6B5C — forest teal)
|
||||
|
||||
@@ -354,11 +354,11 @@ If using Tailwind, extend the theme with these tokens rather than relying on def
|
||||
|
||||
---
|
||||
|
||||
# Scribe
|
||||
# Grimoire
|
||||
|
||||
The design system Scribe renders with is stored in Scribe itself: **Scribe**, which inherits **FabledSword**, edited at `/design-systems`. `frontend/src/assets/theme.css` is generated from it, and CI rejects a `var()` the sheet doesn't define. Where this page and the stored system differ, the stored system is right. Scribe applies the baseline above with these choices:
|
||||
The design system Grimoire renders with is stored in Grimoire itself: **Grimoire**, which inherits **FabledSword**, edited at `/design-systems`. `frontend/src/assets/theme.css` is generated from it, and CI rejects a `var()` the sheet doesn't define. Where this page and the stored system differ, the stored system is right. Grimoire applies the baseline above with these choices:
|
||||
|
||||
- **Accent footprint.** Dusty violet marks Scribe's own moments: the wordmark, active navigation, the editor's cursor and selection, tags and wikilinks, in-progress status, focus rings, and the call to action that starts something new (a first note, say). Operating the software uses the action colours: Moss to save or confirm, Bronze for alternatives, Oxblood with an icon to delete, Pewter ghost for tertiary actions.
|
||||
- **Accent footprint.** Dusty violet marks Grimoire's own moments: the wordmark, active navigation, the editor's cursor and selection, tags and wikilinks, in-progress status, focus rings, and the call to action that starts something new (a first note, say). Operating the software uses the action colours: Moss to save or confirm, Bronze for alternatives, Oxblood with an icon to delete, Pewter ghost for tertiary actions.
|
||||
- **Light and dark.** Both are supported. Light mode is warm parchment (`#F5F1E8` page, `#FBF8F0` cards, `#14171A` text) rather than white, with a cool wash on code blocks so syntax colours stay readable. The accent, action and semantic colours are the same in both.
|
||||
- **Status and priority.** Status: todo Pewter, in progress the accent, done Moss, cancelled Ash, paused Warning. Priority runs cool to warm: low Info, medium Warning, high Error. Overdue uses Error. Each has a low-alpha `-bg` companion.
|
||||
- **Type.** Inter for UI and body, Fraunces at 18px and up (display, H1, H2), JetBrains Mono for code. Two weights only, 400 and 500. Line height 1.5 for body, 1.3 for headings, 1.7 on reading surfaces such as a note.
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ docker compose exec app alembic revision -m "short_description"
|
||||
|
||||
`plugin/` ships straight from git: installs fetch it through `.claude-plugin/marketplace.json`, and there is no build step. After changing anything under `plugin/` or `.claude-plugin/`, run `make mint-plugin` before committing. Claude Code only refreshes an installed plugin when its version changes, and CI fails a plugin change that didn't bump it.
|
||||
|
||||
The hooks must run on a bare machine: POSIX shell, awk, sed and the usual coreutils, plus `git` and `curl`. They fail open, so a slow or unreachable Scribe never blocks Claude Code.
|
||||
The hooks must run on a bare machine: POSIX shell, awk, sed and the usual coreutils, plus `git` and `curl`. They fail open, so a slow or unreachable Grimoire never blocks Claude Code.
|
||||
|
||||
## CI and releases
|
||||
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
# Features
|
||||
|
||||
Scribe is a self-hosted system of record for the work you do with Claude Code. Claude records what it learned, decided and built, and that record reaches the next session, so a solution is recalled rather than reinvented. There is no language model inside Scribe: Claude reaches it through the MCP endpoint and the Claude Code plugin, and the web UI is where people read and edit the same records.
|
||||
Grimoire is a self-hosted system of record for the work you do with Claude Code. Claude records what it learned, decided and built, and that record reaches the next session, so a solution is recalled rather than reinvented. There is no language model inside Grimoire: Claude reaches it through the MCP endpoint and the Claude Code plugin, and the web UI is where people read and edit the same records.
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -40,8 +40,8 @@ A task is a note with a status (`todo`, `in_progress`, `done`, `cancelled`), plu
|
||||
## Snippets and the shape ledger
|
||||
|
||||
- **Snippets** are recorded prior art: a reusable piece of code with when to reach for it, its signature and its canonical location in a repo. Claude searches them before building and is shown matching ones before it writes a file.
|
||||
- **Forge integration**: with a git forge connected, Scribe checks a snippet's cached body against its source and marks it current or diverged. Push webhooks keep that fresh.
|
||||
- **The shape ledger**: Scribe extracts the code shapes from a bound repo and tracks how each is accounted for: an instance of a snippet, a deliberate variant with its reason, or a one-off. Coverage shows what is still unclassified.
|
||||
- **Forge integration**: with a git forge connected, Grimoire checks a snippet's cached body against its source and marks it current or diverged. Push webhooks keep that fresh.
|
||||
- **The shape ledger**: Grimoire extracts the code shapes from a bound repo and tracks how each is accounted for: an instance of a snippet, a deliberate variant with its reason, or a one-off. Coverage shows what is still unclassified.
|
||||
|
||||
## Family canon
|
||||
|
||||
@@ -49,7 +49,7 @@ Projects on the same platform (Android apps, Go services, ...) share good ideas.
|
||||
|
||||
## Design systems
|
||||
|
||||
Tokens (colour, type, space, motion, ...) and written guidance, with inheritance: a project's design system can extend a house style and record where it departs. Scribe generates a stylesheet from it, checks code snippets against it, and gives Claude the values to use before it writes UI. `/design-systems` edits them.
|
||||
Tokens (colour, type, space, motion, ...) and written guidance, with inheritance: a project's design system can extend a house style and record where it departs. Grimoire generates a stylesheet from it, checks code snippets against it, and gives Claude the values to use before it writes UI. `/design-systems` edits them.
|
||||
|
||||
## Stored processes
|
||||
|
||||
@@ -65,8 +65,8 @@ Saved procedures, such as a drift audit or a DRY pass. The plugin syncs each one
|
||||
|
||||
The whole store is reachable through the MCP endpoint at `/mcp`, authenticated with an API key. The Claude Code plugin in this repo adds:
|
||||
|
||||
- hooks for session start, each prompt, before and after tool calls, the end of a turn, compaction and session end, all failing open if Scribe is unreachable;
|
||||
- skills for the working practices: using Scribe, writing plans, systematic debugging, verification, brainstorming, reporting back, reusing code, shape accounting and family canon;
|
||||
- hooks for session start, each prompt, before and after tool calls, the end of a turn, compaction and session end, all failing open if Grimoire is unreachable;
|
||||
- skills for the working practices: using Grimoire, writing plans, systematic debugging, verification, brainstorming, reporting back, reusing code, shape accounting and family canon;
|
||||
- your stored processes as skills.
|
||||
|
||||
See [API Keys & MCP](api-keys-and-mcp.md).
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
# OAuth / OIDC SSO Setup
|
||||
|
||||
Scribe supports single sign-on via any OpenID Connect provider.
|
||||
Grimoire supports single sign-on via any OpenID Connect provider.
|
||||
This guide covers Authentik, but the same pattern works with Keycloak, Authelia, Zitadel, etc.
|
||||
|
||||
## 1. Create the provider in Authentik
|
||||
@@ -8,7 +8,7 @@ This guide covers Authentik, but the same pattern works with Keycloak, Authelia,
|
||||
1. Log in to the Authentik admin UI.
|
||||
2. Go to **Applications → Providers → Create → OAuth2/OpenID Provider**.
|
||||
3. Fill in:
|
||||
- **Name:** `Scribe` (or whatever you like)
|
||||
- **Name:** `Grimoire` (or whatever you like)
|
||||
- **Authorization flow:** your default authorization flow
|
||||
- **Client type:** `Confidential`
|
||||
- **Redirect URIs:** `https://your-fabled-domain/api/auth/oauth/callback`
|
||||
@@ -20,7 +20,7 @@ This guide covers Authentik, but the same pattern works with Keycloak, Authelia,
|
||||
https://auth.example.com/application/o/fabled-assistant/
|
||||
```
|
||||
|
||||
## 2. Configure Scribe
|
||||
## 2. Configure Grimoire
|
||||
|
||||
Add the following environment variables to the `app` service in `docker-compose.yml`:
|
||||
|
||||
@@ -64,7 +64,7 @@ docker compose up --build -d
|
||||
|
||||
## 4. Account Linking
|
||||
|
||||
When a user logs in via OAuth for the first time, Scribe checks in this order:
|
||||
When a user logs in via OAuth for the first time, Grimoire checks in this order:
|
||||
|
||||
1. **Existing OAuth sub** — returns that user immediately.
|
||||
2. **Matching email** — if a local account already exists with the same email, the OAuth identity is linked to it automatically. The user retains all their notes and tasks.
|
||||
@@ -92,4 +92,4 @@ The backend will reject any `POST /api/auth/login` or `POST /api/auth/register`
|
||||
| Zitadel | `https://your-instance.zitadel.cloud` |
|
||||
| Google | `https://accounts.google.com` |
|
||||
|
||||
The OIDC discovery endpoint (`<issuer>/.well-known/openid-configuration`) must be publicly reachable from the Scribe container (server-to-server call at login time).
|
||||
The OIDC discovery endpoint (`<issuer>/.well-known/openid-configuration`) must be publicly reachable from the Grimoire container (server-to-server call at login time).
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Scribe">
|
||||
<title>Fabled Scribe</title>
|
||||
<meta name="apple-mobile-web-app-title" content="Grimoire">
|
||||
<title>Fabled Grimoire</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Fabled Scribe",
|
||||
"short_name": "Scribe",
|
||||
"name": "Fabled Grimoire",
|
||||
"short_name": "Grimoire",
|
||||
"description": "Your self-hosted second brain with AI assistance",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
|
||||
@@ -68,7 +68,7 @@ export function apiErrorMessage(e: unknown, fallback: string): string {
|
||||
*/
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
|
||||
/** HTTP 408. Not a status any Scribe route returns, so it unambiguously means
|
||||
/** HTTP 408. Not a status any Grimoire route returns, so it unambiguously means
|
||||
* "the client gave up" rather than anything the server said. */
|
||||
const CLIENT_TIMEOUT_STATUS = 408;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiGet } from "@/api/client";
|
||||
|
||||
/**
|
||||
* The reply shapes Scribe ships — `GET /api/retrieval/reply-shapes`.
|
||||
* The reply shapes Grimoire ships — `GET /api/retrieval/reply-shapes`.
|
||||
* The same catalog the `list_reply_shapes` MCP tool returns, plus what only
|
||||
* the operator's view needs: the preferences mounted on each shape's moment,
|
||||
* and how often each shape was delivered.
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface Snippet {
|
||||
export type SnippetUsage = RecordUsage;
|
||||
|
||||
/** Result of the last drift check — does the recorded location and code still
|
||||
* match source? The check runs agent-side (Scribe has no checkout); this is the
|
||||
* match source? The check runs agent-side (Grimoire has no checkout); this is the
|
||||
* remembered verdict. `current` is false once the snippet has been edited since
|
||||
* the check, at which point the verdict describes code that's no longer there. */
|
||||
export interface SnippetVerification {
|
||||
@@ -188,7 +188,7 @@ export async function findDuplicateSnippets(
|
||||
}
|
||||
|
||||
/** Record a drift-check verdict. The check itself runs where the code is — an
|
||||
* agent with the working tree — since Scribe has no checkout. This stores what
|
||||
* agent with the working tree — since Grimoire has no checkout. This stores what
|
||||
* was found, and is how the UI clears a stale marker after a manual fix. */
|
||||
export async function verifySnippet(
|
||||
id: number,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Inter:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:ital,wght@0,400;1,400&display=swap');
|
||||
|
||||
/* ==========================================================================
|
||||
GENERATED FROM THE DESIGN SYSTEM — Scribe (design system 2), which inherits
|
||||
GENERATED FROM THE DESIGN SYSTEM — Grimoire (design system 2), which inherits
|
||||
the FabledSword house style (design system 1).
|
||||
|
||||
Do not hand-edit the --fs-* block below. Edit the design system in the app
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
:root {
|
||||
/* accent */
|
||||
--fs-accent: #5B4A8A; /* Scribe's signature colour */
|
||||
--fs-accent: #5B4A8A; /* Grimoire's signature colour */
|
||||
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); /* Pill, tag and badge backgrounds */
|
||||
--fs-accent-faint: color-mix(in srgb, var(--fs-accent) 8%, transparent); /* The faintest accent wash */
|
||||
--fs-accent-deep: color-mix(in srgb, var(--fs-accent) 70%, black); /* The accent, darkened */
|
||||
|
||||
@@ -39,7 +39,7 @@ router.afterEach(() => {
|
||||
<!-- Left: brand -->
|
||||
<router-link to="/" class="nav-brand">
|
||||
<AppLogo :size="34" />
|
||||
<span class="brand-text">Scribe</span>
|
||||
<span class="brand-text">Grimoire</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Center: primary navigation (desktop) -->
|
||||
|
||||
@@ -189,7 +189,7 @@ async function backUpNow() {
|
||||
<p class="intro">
|
||||
Each run adds one dated file, <code>scribe-<date>.tar</code>, holding every table
|
||||
as plain lines. It is not compressed, so a deduplicating tool such as restic stores only what
|
||||
changed since the day before. Scribe never deletes old backups — the destination keeps and
|
||||
changed since the day before. Grimoire never deletes old backups — the destination keeps and
|
||||
thins the history. Sharing, API keys and forge connections are not included.
|
||||
</p>
|
||||
|
||||
@@ -223,7 +223,7 @@ async function backUpNow() {
|
||||
<span>Folder</span>
|
||||
<input v-model.trim="form.folder_path" class="fs-input" placeholder="/backups" />
|
||||
<span class="field-hint">
|
||||
A path inside the Scribe container — usually a share mounted there that a backup
|
||||
A path inside the Grimoire container — usually a share mounted there that a backup
|
||||
service watches.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -153,7 +153,7 @@ onMounted(() => store.load(true));
|
||||
v-for="a in actionsFor(m.name)"
|
||||
:key="`${a.via}:${a.tool}:${a.match}`"
|
||||
:class="['action-chip', { 'is-install': a.via === 'install' }]"
|
||||
:title="a.via === 'install' ? 'Added on this install' : 'Ships with Scribe'"
|
||||
:title="a.via === 'install' ? 'Added on this install' : 'Ships with Grimoire'"
|
||||
>
|
||||
<code>{{ actionLabel(a) }}</code>
|
||||
<button
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* someone changes the shared value and half the components don't move.
|
||||
*
|
||||
* SCOPE, and it is a limit rather than an omission: this reads RECORDED code —
|
||||
* snippets — because that is the code Scribe holds. A repository's own sources
|
||||
* snippets — because that is the code Grimoire holds. A repository's own sources
|
||||
* are checked where they live, by that project's CI.
|
||||
*/
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
|
||||
@@ -49,7 +49,7 @@ const label = (s: string) => LABELS[s] ?? s;
|
||||
Measured worst-case over raised and hover in both modes: active 4.82:1,
|
||||
paused 4.63:1, completed 4.78:1, archived 4.84:1.
|
||||
No new design tokens: four values used by one component are the kind of
|
||||
growth Scribe's own design-system note warns about ("if this system grows
|
||||
growth Grimoire's own design-system note warns about ("if this system grows
|
||||
past a handful of tokens, that is worth noticing rather than
|
||||
accommodating"). The derivation is stated once, here. */
|
||||
.project-status--active {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { RulePreset } from "@/api/rulebooks";
|
||||
import RuleEditorSlideOver from "@/components/rules/RuleEditorSlideOver.vue";
|
||||
|
||||
/**
|
||||
* The reply shapes Scribe ships, and the operator's adjustments to each
|
||||
* The reply shapes Grimoire ships, and the operator's adjustments to each
|
||||
* A shape is product, so its text is read-only here;
|
||||
* what the operator owns is the preferences mounted on the shape's moment,
|
||||
* which arrive beside it and win where they differ. "Adjust this" opens the
|
||||
@@ -39,7 +39,7 @@ const delivered = computed(() =>
|
||||
const verdict = computed(() => {
|
||||
if (!data.value) return "";
|
||||
const n = shapes.value.length;
|
||||
const parts = [`${n} ${n === 1 ? "shape ships" : "shapes ship"} with Scribe`];
|
||||
const parts = [`${n} ${n === 1 ? "shape ships" : "shapes ship"} with Grimoire`];
|
||||
parts.push(adjusted.value
|
||||
? `${adjusted.value} adjusted by your preferences`
|
||||
: "none adjusted — the defaults stand");
|
||||
@@ -126,7 +126,7 @@ onMounted(load);
|
||||
</button>
|
||||
|
||||
<div v-if="open === s.key" :id="`shape-${s.key}`" class="shape-body">
|
||||
<h4 class="body-label">Ships with Scribe · at <code>{{ s.moment }}</code></h4>
|
||||
<h4 class="body-label">Ships with Grimoire · at <code>{{ s.moment }}</code></h4>
|
||||
<div class="shape-text">{{ s.text }}</div>
|
||||
|
||||
<h4 class="body-label">Your adjustments</h4>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* What Scribe has changed about how it works with you.
|
||||
* What Grimoire has changed about how it works with you.
|
||||
*
|
||||
* THE RISK THIS EXISTS FOR. A preference is the one record
|
||||
* kind the agent rewrites on its own, mid-work, without asking — which is
|
||||
@@ -78,7 +78,7 @@ onMounted(() => store.fetchDrift());
|
||||
<header>
|
||||
<h2>Recent changes</h2>
|
||||
<p class="sweep-lede">
|
||||
Preferences Scribe rewrote while working, most recently changed first. A
|
||||
Preferences Grimoire rewrote while working, most recently changed first. A
|
||||
preference is how you want work done, so sessions keep it current
|
||||
without asking — this is where you see what they decided. Rules are not
|
||||
here: those change when you change them.
|
||||
|
||||
@@ -261,7 +261,7 @@ watch(() => props.ruleId, load);
|
||||
<input v-model="kind" type="radio" value="preference" />
|
||||
<span>
|
||||
<strong>Preference</strong> — how you want work done. Ignoring it
|
||||
costs consistency, not correctness, and <em>Scribe updates it as
|
||||
costs consistency, not correctness, and <em>Grimoire updates it as
|
||||
the work teaches it</em>.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -25,7 +25,7 @@ const emit = defineEmits<{
|
||||
<span
|
||||
v-if="r.kind === 'preference'"
|
||||
class="rule-chip rule-chip-preference"
|
||||
title="How you want work done. It does not bind, and Scribe updates it as the work teaches it."
|
||||
title="How you want work done. It does not bind, and Grimoire updates it as the work teaches it."
|
||||
>preference</span>
|
||||
<!-- Marked only when something is WRONG: every rule arrives by
|
||||
retrieval now, so "conditional" stopped distinguishing anything.
|
||||
|
||||
@@ -208,7 +208,7 @@ export const useRulebooksStore = defineStore("rulebooks", () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* What Scribe has changed about how it works with the operator.
|
||||
* What Grimoire has changed about how it works with the operator.
|
||||
*
|
||||
* Preferences only — a rule changes when its author changes it, so
|
||||
* including them would bury the unreviewed rows under the operator's own
|
||||
|
||||
@@ -498,8 +498,8 @@ watch(selectedId, () => {
|
||||
* The provenance list below shows values as the record states them, and a
|
||||
* `var()` reference states nothing on its own: rendering
|
||||
* `color-mix(in srgb, var(--accent) 15%, transparent)` as a background resolves
|
||||
* `--accent` against THIS app, so a system that isn't the one Scribe runs on
|
||||
* would be drawn in Scribe's palette — and look right, which is why it would
|
||||
* `--accent` against THIS app, so a system that isn't the one Grimoire runs on
|
||||
* would be drawn in Grimoire's palette — and look right, which is why it would
|
||||
* go unnoticed.
|
||||
*
|
||||
* The preview above resolves values properly, on a probe carrying only that
|
||||
|
||||
@@ -901,7 +901,7 @@ async function confirmDelete() {
|
||||
<div class="edit-field">
|
||||
<label class="edit-label">Goal</label>
|
||||
<!-- A textarea, not a single-line input. A project goal is a
|
||||
paragraph in practice — this one showed as "Maintain Scribe as
|
||||
paragraph in practice — this one showed as "Maintain Grimoire as
|
||||
the reliabl" and gave no way to read the rest without arrowing
|
||||
through it. -->
|
||||
<textarea v-model="editGoal" class="fs-input edit-textarea" rows="4" placeholder="What are you trying to achieve?"></textarea>
|
||||
|
||||
@@ -254,7 +254,7 @@ interface TuningEvent {
|
||||
|
||||
// What a dial's number was measured against, and whether that space has moved
|
||||
// Mirrors the `calibration` block `current_settings` returns per dial.
|
||||
// `stale` is nullable on purpose: a dial moved before Scribe recorded stamps is
|
||||
// `stale` is nullable on purpose: a dial moved before Grimoire recorded stamps is
|
||||
// UNKNOWN rather than fine, and rendering the two the same would hide the case
|
||||
// most likely to be wrong.
|
||||
interface DialCalibration {
|
||||
@@ -289,7 +289,7 @@ const staleDials = computed(() =>
|
||||
dial,
|
||||
why:
|
||||
cal.source === "unstamped"
|
||||
? "changed before Scribe recorded what it was measured against"
|
||||
? "changed before Grimoire recorded what it was measured against"
|
||||
: cal.model_changed && cal.shape_changed
|
||||
? `measured under ${cal.embedding_model} at document shape ${cal.shape_version} — both have changed since`
|
||||
: cal.model_changed
|
||||
@@ -599,9 +599,9 @@ const claudeCodeCommand = computed(() => {
|
||||
--header "Authorization: Bearer ${effectiveApiKey.value}"`;
|
||||
});
|
||||
|
||||
// Plugin install — the recommended path. The Scribe plugin bundles the MCP
|
||||
// connection, a session-start hook that surfaces your rules, and the Scribe
|
||||
// process-skills. The marketplace is the Scribe app's own git repo; persisted
|
||||
// Plugin install — the recommended path. The Grimoire plugin bundles the MCP
|
||||
// connection, a session-start hook that surfaces your rules, and the Grimoire
|
||||
// process-skills. The marketplace is the Grimoire app's own git repo; persisted
|
||||
// per-browser like the MCP fields above.
|
||||
const _MKT_KEY = 'plugin_marketplace_url';
|
||||
const pluginMarketplaceUrl = ref<string>(localStorage.getItem(_MKT_KEY) || '');
|
||||
@@ -799,7 +799,7 @@ const smtp = ref({
|
||||
smtp_username: "",
|
||||
smtp_password: "",
|
||||
smtp_from_address: "",
|
||||
smtp_from_name: "Fabled Scribe",
|
||||
smtp_from_name: "Fabled Grimoire",
|
||||
smtp_use_tls: "true",
|
||||
});
|
||||
const savingSmtp = ref(false);
|
||||
@@ -1319,7 +1319,7 @@ async function handleRestoreFile(event: Event) {
|
||||
if (res.status === 413) {
|
||||
throw new Error(
|
||||
"the file is bigger than the web upload allows. Restore it with " +
|
||||
"`python -m scribe.restore <file>` inside the Scribe container instead.",
|
||||
"`python -m scribe.restore <file>` inside the Grimoire container instead.",
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
@@ -1677,7 +1677,7 @@ async function deleteUser(userId: number) {
|
||||
<section class="settings-section full-width">
|
||||
<h2>Knowledge auto-inject</h2>
|
||||
<p class="section-desc">
|
||||
When enabled, the Scribe plugin quietly surfaces the titles of your most
|
||||
When enabled, the Grimoire plugin quietly surfaces the titles of your most
|
||||
relevant notes on each prompt — never their full text — so Claude can pull
|
||||
one in with <code>get_note(id)</code> only when it helps. Titles only, each
|
||||
note at most once per session, and nothing is shown unless it clears the
|
||||
@@ -1980,7 +1980,7 @@ async function deleteUser(userId: number) {
|
||||
</p>
|
||||
<p v-if="loadingTuning" class="field-hint">Loading…</p>
|
||||
<p v-else-if="!tuningEvents.length" class="field-hint">
|
||||
Nothing has been changed yet — every surface is on the value Scribe
|
||||
Nothing has been changed yet — every surface is on the value Grimoire
|
||||
shipped. Those are starting points measured against one corpus with
|
||||
one embedding model, not answers, so expect this to fill.
|
||||
</p>
|
||||
@@ -2519,7 +2519,7 @@ async function deleteUser(userId: number) {
|
||||
<section class="settings-section full-width">
|
||||
<h2>MCP Access</h2>
|
||||
<p class="settings-description">
|
||||
Connect Claude (Code or Desktop) to this Scribe instance. Claude reads and writes your notes,
|
||||
Connect Claude (Code or Desktop) to this Grimoire instance. Claude reads and writes your notes,
|
||||
tasks, projects, events, and people via the built-in MCP endpoint below.
|
||||
</p>
|
||||
|
||||
@@ -2625,8 +2625,8 @@ async function deleteUser(userId: number) {
|
||||
<!-- Claude Code tab -->
|
||||
<div v-if="mcpClientTab === 'claude-code'">
|
||||
<p class="settings-description">
|
||||
<strong>Recommended — install the Scribe plugin.</strong> One install wires up the MCP
|
||||
connection, a session-start hook that surfaces your rules, and the Scribe process-skills.
|
||||
<strong>Recommended — install the Grimoire plugin.</strong> One install wires up the MCP
|
||||
connection, a session-start hook that surfaces your rules, and the Grimoire process-skills.
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -2639,7 +2639,7 @@ async function deleteUser(userId: number) {
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
When prompted, enter your <strong>Scribe base URL</strong> (<code>{{ origin }}</code>),
|
||||
When prompted, enter your <strong>Grimoire base URL</strong> (<code>{{ origin }}</code>),
|
||||
an <strong>API key</strong> (generate one above), and optionally a
|
||||
<strong>project id</strong> to scope the session-start context.
|
||||
</li>
|
||||
@@ -2657,7 +2657,7 @@ async function deleteUser(userId: number) {
|
||||
<input
|
||||
v-model="pluginMarketplaceUrl"
|
||||
type="text"
|
||||
:placeholder="serverMarketplaceUrl || 'https://git.example.com/you/Scribe.git'"
|
||||
:placeholder="serverMarketplaceUrl || 'https://git.example.com/you/Grimoire.git'"
|
||||
class="settings-input"
|
||||
spellcheck="false"
|
||||
/>
|
||||
@@ -2687,7 +2687,7 @@ async function deleteUser(userId: number) {
|
||||
|
||||
<p class="mcp-hint" style="margin-top: 0.75rem;">
|
||||
<strong>Connect the MCP only (no plugin).</strong> You won't get the session-start rule
|
||||
push or the Scribe skills this way.
|
||||
push or the Grimoire skills this way.
|
||||
</p>
|
||||
<div class="mcp-code-row">
|
||||
<pre class="mcp-code">{{ claudeCodeCommand }}</pre>
|
||||
@@ -2721,7 +2721,7 @@ async function deleteUser(userId: number) {
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
Restart Claude Desktop. The Scribe tools should appear in the available tools list.
|
||||
Restart Claude Desktop. The Grimoire tools should appear in the available tools list.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -2799,9 +2799,9 @@ async function deleteUser(userId: number) {
|
||||
<section class="settings-section full-width">
|
||||
<h2>Plugin marketplace</h2>
|
||||
<p class="section-desc">
|
||||
Git URL of the repo that ships this Scribe plugin (usually this app's own repo).
|
||||
Git URL of the repo that ships this Grimoire plugin (usually this app's own repo).
|
||||
Shown to every user in <strong>MCP Access</strong> so the install command is
|
||||
copyable. Example: https://git.example.com/you/Scribe.git
|
||||
copyable. Example: https://git.example.com/you/Grimoire.git
|
||||
</p>
|
||||
<div class="field url-field">
|
||||
<label for="marketplace-url">Marketplace git URL</label>
|
||||
@@ -2809,7 +2809,7 @@ async function deleteUser(userId: number) {
|
||||
id="marketplace-url"
|
||||
v-model="adminMarketplaceUrl"
|
||||
type="url"
|
||||
placeholder="https://git.example.com/you/Scribe.git"
|
||||
placeholder="https://git.example.com/you/Grimoire.git"
|
||||
class="fs-input input"
|
||||
/>
|
||||
</div>
|
||||
@@ -2927,7 +2927,7 @@ async function deleteUser(userId: number) {
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="smtp-from-name">From Name</label>
|
||||
<input id="smtp-from-name" v-model="smtp.smtp_from_name" type="text" placeholder="Fabled Scribe" class="fs-input input" />
|
||||
<input id="smtp-from-name" v-model="smtp.smtp_from_name" type="text" placeholder="Fabled Grimoire" class="fs-input input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox-field">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scribe",
|
||||
"description": "Scribe 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 Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting, family-canon), and syncs your saved Scribe Processes as skills (/scribe:sync).",
|
||||
"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).",
|
||||
"version": "2026.10.21.2344",
|
||||
"author": {
|
||||
"name": "Bryan Van Deusen"
|
||||
@@ -17,12 +17,12 @@
|
||||
"userConfig": {
|
||||
"api_endpoint": {
|
||||
"type": "string",
|
||||
"title": "Scribe base URL",
|
||||
"description": "Base URL of your Scribe instance, no trailing slash (e.g. https://scribe.example.com)"
|
||||
"title": "Grimoire base URL",
|
||||
"description": "Base URL of your Grimoire instance, no trailing slash (e.g. https://scribe.example.com)"
|
||||
},
|
||||
"api_token": {
|
||||
"type": "string",
|
||||
"title": "Scribe API key",
|
||||
"title": "Grimoire API key",
|
||||
"description": "An fmcp_ API key from Settings \u2192 API Keys (read scope is enough for the session-start hook; write scope to use the tools)",
|
||||
"sensitive": true
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
# Packaging Scribe for an agent client
|
||||
# Packaging Grimoire for an agent client
|
||||
|
||||
This is the maintainer contract for shipping Scribe into an agent client:
|
||||
This is the maintainer contract for shipping Grimoire into an agent client:
|
||||
what every client package shares, what each one adds, and where a new client's
|
||||
files go. One principle governs it: **the server orients,
|
||||
the skills hold the depth, and a client adapter only times delivery and says
|
||||
@@ -13,10 +13,10 @@ another one means adding files, not moving or rewriting any.
|
||||
|
||||
| Piece | Where | What it is |
|
||||
|---|---|---|
|
||||
| **The skills** | `plugin/skills/*/SKILL.md` | Agent Skills (the open SKILL.md format). They state every Scribe reflex in full and name no client. `tests/test_guidance_ownership.py` fails if a skill names a particular client, or references anything outside its own folder. Every client package ships this folder verbatim. |
|
||||
| **The skills** | `plugin/skills/*/SKILL.md` | Agent Skills (the open SKILL.md format). They state every Grimoire reflex in full and name no client. `tests/test_guidance_ownership.py` fails if a skill names a particular client, or references anything outside its own folder. Every client package ships this folder verbatim. |
|
||||
| **The MCP server** | `<base URL>/mcp` | HTTP, `Authorization: Bearer <fmcp_ key>`. Its `_INSTRUCTIONS` is a client-neutral orientation — the workflow across tools (≤1,600 chars); each tool's description carries its contract; in-band responses (`placement`, `report_back`, `systems_hint`, the duplicate gate, the guessed-id refusal) fire in every client. |
|
||||
| **The adapter API** | `<base URL>/api/plugin/*` | Endpoints any client's hooks can call with the same key. `GET`, so read scope is enough: `context` (live session state), `retrieve` (rules, preferences and notes for a message), `prior-art` (records and shape-ledger hints for code being written), `tool-rules` (rules for a command about to run), `moment-tools` (which tools reach a mounted moment), `shape-check` (definitions written this turn that nobody has judged), `claim-session` / `release-session` (task claims), `processes` (stored Processes to expose as skills). `POST`, so write scope: `moment` (rules mounted on the moments a tool call reaches), `rule-opened` (a rule was read), `reply-rules` (the finished reply, held for an unopened rule or — when the turn closed a task — for missing completion sections). |
|
||||
| **The API key** | Scribe → Settings → MCP Access | One `fmcp_` key per install. Write scope covers the MCP tools and every hook; a read key runs only the `GET` hooks. |
|
||||
| **The API key** | Grimoire → Settings → MCP Access | One `fmcp_` key per install. Write scope covers the MCP tools and every hook; a read key runs only the `GET` hooks. |
|
||||
|
||||
## Added by each client
|
||||
|
||||
@@ -39,7 +39,7 @@ another one means adding files, not moving or rewriting any.
|
||||
| `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 Scribe's own (their responses carry `moment_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`. |
|
||||
@@ -66,5 +66,5 @@ Hook config arrives as `CLAUDE_PLUGIN_OPTION_API_ENDPOINT` / `CLAUDE_PLUGIN_OPTI
|
||||
|
||||
1. **Passing the key to the MCP server.** Claude Code substitutes `${user_config.api_token}` into the header. Agent Plugins' `mcp.json` and Gemini's extension config have their own variable and secret handling. Decide per client, and keep the key out of files that get committed.
|
||||
2. **Coexistence in one folder.** Whether a root `plugin.json` (Agent Plugins) beside `.claude-plugin/plugin.json` changes how Claude Code loads the plugin is untested. Test it before shipping both from `plugin/`, or give the second client its own package directory that reuses `plugin/skills/`.
|
||||
3. **Hook parity.** Other clients' hook events differ and are partly implemented. Map each Scribe hook to the nearest event, and where none exists, leave that timing to the in-band server responses rather than writing a copy of the guidance.
|
||||
3. **Hook parity.** Other clients' hook events differ and are partly implemented. Map each Grimoire hook to the nearest event, and where none exists, leave that timing to the in-band server responses rather than writing a copy of the guidance.
|
||||
4. **Process skills.** The Claude Code sync writes `~/.claude/skills`. Another client needs its own skills location, or the MCP skills extension (SEP-2640) once a client supports it.
|
||||
|
||||
+20
-20
@@ -1,44 +1,44 @@
|
||||
# Scribe plugin for Claude Code
|
||||
# Grimoire plugin for Claude Code
|
||||
|
||||
Turns a self-hosted [Scribe](https://git.fabledsword.com/bvandeusen/FabledScribe)
|
||||
Turns a self-hosted [Grimoire](https://git.fabledsword.com/bvandeusen/FabledScribe)
|
||||
instance into a first-class Claude Code extension:
|
||||
|
||||
- **MCP tools** over your notes, tasks, projects, milestones, systems, and
|
||||
rulebook (the `scribe` 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 Scribe surfaces *without being asked*. Rules are
|
||||
Claude Code guidance, so Grimoire surfaces *without being asked*. Rules are
|
||||
never preloaded; they arrive by retrieval when your work matches one.
|
||||
- **Prior-art recall on writes** — a `PreToolUse` hook on Write/Edit checks the
|
||||
file about to be written against your recorded snippets (what's kept at that
|
||||
path, and what resembles the code) and offers them before the helper is
|
||||
rewritten. Titles only, never blocks the edit.
|
||||
- **The shared Scribe skills** — client-neutral Agent Skills, the same files
|
||||
- **The shared Grimoire skills** — client-neutral Agent Skills, the same files
|
||||
any client's package would ship: using-scribe, 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 Scribe Processes as skills** — saved Processes are synced into local
|
||||
- **Your Grimoire Processes as skills** — saved Processes are synced into local
|
||||
`~/.claude/skills/scribe-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`.
|
||||
|
||||
It is designed so you can uninstall `superpowers` and depend on Scribe instead
|
||||
of auto-memory — leave auto-memory at its default; Scribe replaces its job by
|
||||
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
|
||||
holding the one copy, not by switching it off.
|
||||
|
||||
**How the pieces divide the work**: the Scribe server orients
|
||||
**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 Scribe for
|
||||
Claude Code needs said (`hooks/scribe_static_context.md`). Packaging Grimoire for
|
||||
another client: see [PACKAGING.md](PACKAGING.md).
|
||||
|
||||
## Install
|
||||
|
||||
The plugin ships inside the Scribe app repo, so the marketplace *is* that repo —
|
||||
you always get the plugin version that matches your Scribe instance.
|
||||
The plugin ships inside the Grimoire app repo, so the marketplace *is* that repo —
|
||||
you always get the plugin version that matches your Grimoire instance.
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledScribe.git
|
||||
@@ -49,15 +49,15 @@ On install you'll be asked for:
|
||||
|
||||
| Setting | What |
|
||||
|---|---|
|
||||
| **Scribe base URL** | e.g. `https://scribe.example.com` (no trailing slash) |
|
||||
| **Scribe API key** | a write-scoped `fmcp_` key from **Settings → MCP Access** (stored in your OS keychain) |
|
||||
| **Grimoire base URL** | e.g. `https://scribe.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`),
|
||||
**fail-open**: if Scribe is unreachable it injects nothing and never blocks
|
||||
**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
|
||||
@@ -69,7 +69,7 @@ On install you'll be asked for:
|
||||
line names a duplicate family (no canon) or a canon recorded elsewhere for
|
||||
the names being written (its own dedup channel, `exclude_derive`).
|
||||
Fail-open but not fail-silent: a configured instance that does not answer
|
||||
in time is said, once per outage ("Scribe did not answer … this write went
|
||||
in time is said, once per outage ("Grimoire did not answer … this write went
|
||||
UNCHECKED"), so a session can tell "checked, nothing there" from "never
|
||||
checked"; an answer clears the marker. The local by-name arm needs no
|
||||
server and always runs. Toggle in **Settings → General → Knowledge
|
||||
@@ -82,9 +82,9 @@ On install you'll be asked for:
|
||||
- `hooks/hooks.json` → PreToolUse hook on every tool (`hooks/scribe_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 Scribe's own tools, whose responses carry
|
||||
nothing mounted, and Grimoire's own tools, whose responses carry
|
||||
`moment_rules`.
|
||||
- `hooks/hooks.json` → PostToolUse hooks on Scribe's `get_rule`,
|
||||
- `hooks/hooks.json` → PostToolUse hooks on Grimoire's `get_rule`,
|
||||
`rule_outcome`, `update_task` and `add_task_log`: they keep the session's
|
||||
ledgers of rules opened and acted on (`get_rule` also reports to
|
||||
`POST /api/plugin/rule-opened`), and claim the task being worked
|
||||
@@ -102,7 +102,7 @@ On install you'll be asked for:
|
||||
- `hooks/hooks.json` → Stop hook (`hooks/scribe_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 Scribe task (`update_task`/`create_task` with status
|
||||
When the turn closed a Grimoire task (`update_task`/`create_task` with status
|
||||
done), the same request has the server check the reply for the completion
|
||||
sections — where the work sits, what needs you, what comes next. If they are
|
||||
missing it blocks once with the reason the server returns, and records how
|
||||
@@ -120,9 +120,9 @@ On install you'll be asked for:
|
||||
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 Scribe
|
||||
command → generate `~/.claude/skills/scribe-proc-*` stubs from your Grimoire
|
||||
Processes (via `GET /api/plugin/processes`); also **fail-open**, and pruned to
|
||||
match what exists in Scribe.
|
||||
match what exists in Grimoire.
|
||||
- `hooks/hooks.json` → PreCompact hook (`hooks/scribe_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.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
description: Sync your Scribe stored Processes into auto-surfacing local skills
|
||||
description: Sync your Grimoire stored Processes into auto-surfacing local skills
|
||||
allowed-tools: Bash(bash:*), Bash(ls:*)
|
||||
---
|
||||
|
||||
Regenerate the local skill stubs for your Scribe **Processes** so they auto-surface
|
||||
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`
|
||||
whose body calls `get_process(<name>)` for the live procedure.
|
||||
|
||||
@@ -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 Scribe process skills (no Processes, or Scribe unreachable)"
|
||||
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)"
|
||||
```
|
||||
|
||||
Report which `scribe-proc-*` skills are now present.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — PostToolUse write-path trigger on Bash.
|
||||
# Grimoire plugin — PostToolUse write-path trigger on Bash.
|
||||
#
|
||||
# scribe_prior_art.sh fires before a Write/Edit TOOL CALL. Code written any
|
||||
# other way — sed, heredocs, python edit scripts, `cat > file` — never reaches
|
||||
@@ -219,7 +219,7 @@ while IFS= read -r rel_path; do
|
||||
if [ -n "$local_lines" ] && [ "$reached" != 0 ]; then
|
||||
n_recorded=$(scribe_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 Scribe. 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."
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — UserPromptSubmit push channel (knowledge auto-inject, Path A).
|
||||
# Grimoire plugin — UserPromptSubmit push channel (knowledge auto-inject, Path A).
|
||||
#
|
||||
# On each user prompt, asks the operator's Scribe instance for a TITLE-FIRST
|
||||
# On each user prompt, asks the operator's Grimoire instance for a TITLE-FIRST
|
||||
# awareness hint: the few notes that clear the per-user auto-inject gates
|
||||
# (high-confidence threshold, margin gate, session dedup, top-k). Titles + ids
|
||||
# only — never bodies; the agent calls get_note(id) to pull anything it judges
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe — say WHICH session holds a task's claim.
|
||||
# Grimoire — say WHICH session holds a task's claim.
|
||||
#
|
||||
# The server stamps a claim on the write that is the work — a task reaching
|
||||
# `in_progress`, a work log on an open task — so every MCP client gets a claim
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
# Scribe plugin — the pieces the hooks share.
|
||||
# Grimoire plugin — the pieces the hooks share.
|
||||
#
|
||||
# scribe_prior_art.sh fires BEFORE a Write/Edit tool call; scribe_after_write.sh
|
||||
# fires AFTER a Bash tool call and diffs the working tree, so code written by
|
||||
@@ -13,7 +13,7 @@
|
||||
# scribe_defs stdin code → "kind<TAB>name" per definition
|
||||
# scribe_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 "Scribe didn't answer" line, once
|
||||
# scribe_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
|
||||
@@ -500,12 +500,12 @@ scribe_defs() {
|
||||
# ---------------------------------------------------------------------------
|
||||
# ARM 1 — BY NAME, LOCALLY. Does a definition of this already exist?
|
||||
#
|
||||
# The recorded arms ask Scribe what was RECORDED; the ledger arm asks what a
|
||||
# The recorded arms ask Grimoire what was RECORDED; the ledger arm asks what a
|
||||
# BOUND repo's ledger knows. A helper nobody recorded, in a repo nobody bound,
|
||||
# is invisible to both, so it gets defined again in the next file. This arm asks the one question only the
|
||||
# developer's machine can answer, inside the repo, holding the code about to
|
||||
# be written: no index, no storage, no server — it runs even on an install
|
||||
# that has never configured Scribe.
|
||||
# that has never configured Grimoire.
|
||||
#
|
||||
# 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
|
||||
@@ -852,7 +852,7 @@ scribe_local_dups() {
|
||||
# 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
|
||||
# still owes its static floor when Scribe is unconfigured.
|
||||
# still owes its static floor when Grimoire is unconfigured.
|
||||
# Declared here, not just assigned inside the function: `scribe_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
|
||||
@@ -890,7 +890,7 @@ scribe_unreached() {
|
||||
[ $((now - last)) -lt "$_SCRIBE_UNREACHED_QUIET" ] && return 0
|
||||
fi
|
||||
printf '%s' "$now" > "$marker" 2>/dev/null || true
|
||||
printf '> Scribe 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"
|
||||
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() {
|
||||
@@ -1305,12 +1305,12 @@ scribe_turn_fact() {
|
||||
# The second key is a `.scribe` 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 Scribe should be holding goes in it.
|
||||
# Nothing Grimoire should be holding goes in it.
|
||||
#
|
||||
# {"instance": "https://scribe.example.com", "project_id": 2,
|
||||
# "project": "FabledScribe"}
|
||||
#
|
||||
# instance WHICH Scribe the id belongs to, and the reason this file is
|
||||
# instance WHICH Grimoire the id belongs to, and the reason this file is
|
||||
# not just a number. A project id means nothing on its own: id 2
|
||||
# is a different project on every instance, so a marker that
|
||||
# travels — a copied directory, a shared machine, a repo someone
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Scribe plugin — JSON, read without jq.
|
||||
# Grimoire plugin — JSON, read without jq.
|
||||
#
|
||||
# WHY THIS FILE EXISTS. Every hook here parsed JSON with `jq`, and every hook
|
||||
# opened with `command -v jq >/dev/null 2>&1 || exit 0`. jq is not installed by
|
||||
@@ -47,7 +47,7 @@
|
||||
# ordering a turn is bounded by is unaffected by the dropped one.
|
||||
#
|
||||
# A KEY CONTAINING `.` OR `[` WOULD MAKE AN AMBIGUOUS PATH. Nothing in the two
|
||||
# JSON dialects this reads — Claude Code hook events and Scribe's own API — has
|
||||
# JSON dialects this reads — Claude Code hook events and Grimoire's own API — has
|
||||
# one, and inventing an escaping scheme for a case neither producer can emit
|
||||
# would cost every caller a decode for nothing. Stated so the next person hits
|
||||
# a comment rather than a mystery.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe — PreToolUse moment arm: rules MOUNTED on a moment arrive when the
|
||||
# Grimoire — PreToolUse moment arm: rules MOUNTED on a moment arrive when the
|
||||
# moment happens.
|
||||
#
|
||||
# The other PreToolUse arms search: they turn the act into a query and hope a
|
||||
@@ -67,7 +67,7 @@ now=$(date +%s 2>/dev/null) || now=0
|
||||
# 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
|
||||
# an unmounted moment is exactly what the evidence is for, and including
|
||||
# Scribe's own tools (closing a task is a moment too). The open itself is
|
||||
# Grimoire's own tools (closing a task is a moment too). The open itself is
|
||||
# not an act. A large event (a file write) is kept as its tool name alone.
|
||||
if [ "$now" -gt 0 ] && [ "$tool_key" != "get_rule" ]; then
|
||||
acts_file="$cache_dir/${safe_sid}.acts"
|
||||
@@ -92,7 +92,7 @@ esac
|
||||
# ── Which tools can reach a mounted rule: cached, first line a timestamp ──
|
||||
#
|
||||
# Five minutes. A mount or a mapping made in this session reaches the hook
|
||||
# within that window; the MCP door delivers Scribe's own moments at once.
|
||||
# within that window; the MCP door delivers Grimoire's own moments at once.
|
||||
tools_file="$cache_dir/${safe_sid}.tools"
|
||||
stamp=""
|
||||
[ -f "$tools_file" ] && stamp=$(head -n 1 "$tools_file" 2>/dev/null)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — PreCompact: tell the summarizer what must survive.
|
||||
# Grimoire plugin — PreCompact: tell the summarizer what must survive.
|
||||
#
|
||||
# HOW A PRECOMPACT HOOK REACHES THE MODEL (as read out of Claude Code 2.1.273):
|
||||
#
|
||||
@@ -59,19 +59,19 @@ cat <<'EOF'
|
||||
Preserve the following literally in the summary — copied through, not
|
||||
paraphrased or counted:
|
||||
|
||||
- Every Scribe record the conversation refers to, by id AND title: tasks,
|
||||
- Every Grimoire record the conversation refers to, by id AND title: tasks,
|
||||
issues, milestones, notes, rules, systems. A bare "#4061" is not enough; a
|
||||
record whose name is lost has to be looked up again before it can be used.
|
||||
- Which task is in progress and what its status was last set to, plus the
|
||||
milestone it sits under.
|
||||
- Work that was done but NOT yet recorded in Scribe — an edit with no work-log,
|
||||
- Work that was done but NOT yet recorded in Grimoire — an edit with no work-log,
|
||||
a fix not filed as an issue, a decision not written down. Carry these over as
|
||||
outstanding; they exist nowhere else once this conversation is summarized.
|
||||
- Any rule or preference that was retrieved and still governs the work, by id
|
||||
and title.
|
||||
- Anything the operator asked for that has not been done yet, in their words.
|
||||
|
||||
Everything else here can be recovered from the repository or from Scribe. These
|
||||
Everything else here can be recovered from the repository or from Grimoire. These
|
||||
cannot: they are this session's only copy.
|
||||
EOF
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — PreToolUse write-path trigger (prior-art recall).
|
||||
# Grimoire plugin — PreToolUse write-path trigger (prior-art recall).
|
||||
#
|
||||
# Auto-inject (scribe_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 Scribe
|
||||
# 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
|
||||
# that path or in its directory, plus snippets resembling the code about to be
|
||||
# written. Titles + ids only, never bodies.
|
||||
@@ -278,7 +278,7 @@ if [ -n "$body" ]; then
|
||||
fi
|
||||
|
||||
# ARM 1½ — the RECORD nudge. The local arm just proved the thing being
|
||||
# written already exists elsewhere in this repo, and Scribe returned no record
|
||||
# written already exists elsewhere in this repo, and Grimoire returned no record
|
||||
# of anything for it. That is the one moment "record it" is earned rather than
|
||||
# noise: the duplication is demonstrated, not guessed. Gated on BOTH sides so
|
||||
# an ordinary new helper (no other copies) and an already-recorded one (the
|
||||
@@ -289,7 +289,7 @@ fi
|
||||
if [ -n "$local_lines" ] && [ "$reached" = 1 ]; then
|
||||
n_recorded=$(scribe_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 Scribe. 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."
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe — record that this session OPENED a rule, not merely saw it named.
|
||||
# Grimoire — record that this session OPENED a rule, not merely saw it named.
|
||||
#
|
||||
# WHAT THIS ANSWERS
|
||||
#
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe — record that this session said what a rule DID, not merely that it
|
||||
# Grimoire — record that this session said what a rule DID, not merely that it
|
||||
# read one.
|
||||
#
|
||||
# THE THIRD LEDGER, AND WHY THE OTHER TWO ARE NOT ENOUGH.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe — Stop hook: the reply moment.
|
||||
# Grimoire — Stop hook: the reply moment.
|
||||
#
|
||||
# A reply is the one act no tool call marks, and it is where "please check
|
||||
# this on your end" or "it's done" gets said — so every arm that fires on a
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — SessionStart push channel (two tiers + compaction re-grounding).
|
||||
# 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 Scribe's reflexes
|
||||
# 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 Scribe is unavailable. It carries only what
|
||||
# /scribe: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.
|
||||
#
|
||||
# Tier 2 (DYNAMIC, best-effort enrichment): curls the operator's Scribe instance
|
||||
# Tier 2 (DYNAMIC, best-effort enrichment): curls the operator's Grimoire instance
|
||||
# for active-project context and appends it. Config comes from
|
||||
# the plugin's userConfig, exported to hooks as:
|
||||
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
|
||||
@@ -24,7 +24,7 @@
|
||||
# fires after a compaction (SessionStart input `source` == "compact"), when
|
||||
# earlier turns have just been summarized and in-flight state is most at risk.
|
||||
# On that source we lead with a banner telling the model to reload project +
|
||||
# in-flight tasks from Scribe. This stays the durable path: record-as-you-go
|
||||
# in-flight tasks from Grimoire. This stays the durable path: record-as-you-go
|
||||
# plus a post-compaction reload from the instance.
|
||||
#
|
||||
# The other half: a host hook cannot make the model flush before a compaction,
|
||||
@@ -153,7 +153,7 @@ manifest="$here/../.claude-plugin/plugin.json"
|
||||
if [ -f "$manifest" ]; then
|
||||
plugin_version=$(scribe_json_pick "$(scribe_json_flat < "$manifest")" '.version')
|
||||
if [ -n "$plugin_version" ]; then
|
||||
append "> Scribe 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."
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -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="> ⚠️ Scribe: 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-scribe skill still applies."
|
||||
fi
|
||||
elif [ -n "$url" ] && [ -z "$token" ]; then
|
||||
status="> ⚠️ Scribe: live context disabled this session — the API key is not configured (Scribe base URL is). Set it with \`/plugin\` → Scribe → 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 SCRIBE_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="> ⚠️ Scribe: live context disabled this session — neither the Scribe base URL nor the API key reached this hook. Configure the plugin (\`/plugin\` → Scribe), 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 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()\`."
|
||||
fi
|
||||
|
||||
[ -n "$dyn" ] && append "$dyn"
|
||||
@@ -265,17 +265,17 @@ fi
|
||||
if [ -n "$dyn" ] && [ -z "$(scribe_json_pick "$body_flat" '.project.id')" ]; then
|
||||
host=$(scribe_url_host "$url")
|
||||
if [ -n "$marker_why" ]; then
|
||||
append "> ⚠️ Scribe: the marker file \`${marker}\` ${marker_why}, so no project context was loaded. Fix the file, or ignore it and bind this directory another way."
|
||||
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 "> ⚠️ Scribe: \`${marker}\` names project ${marker_id}, which this account cannot read on ${host} — it may belong to a different Scribe instance, or the project may have been deleted. Check with \`list_projects()\` and correct the file."
|
||||
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 "> ℹ️ Scribe: 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 Scribe 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 \`.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."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Compaction re-grounding: lead with a reload banner when this fire is a compact.
|
||||
if [ "$source" = "compact" ]; then
|
||||
prepend "> ⟳ This session was just COMPACTED — earlier turns are now a summary, so in-flight detail may be lost. Any rules that had been retrieved went into that summary with everything else, so treat yourself as holding none: before the next consequential act, ask again with \`search(content_type=\"rule\")\` rather than trusting a half-remembered one. Re-run \`enter_project()\` for the active project, check its recent milestones and open tasks, and reconcile what you are mid-way through against what Scribe records. Scribe is the record."
|
||||
prepend "> ⟳ This session was just COMPACTED — earlier turns are now a summary, so in-flight detail may be lost. Any rules that had been retrieved went into that summary with everything else, so treat yourself as holding none: before the next consequential act, ask again with \`search(content_type=\"rule\")\` rather than trusting a half-remembered one. Re-run \`enter_project()\` for the active project, check its recent milestones and open tasks, and reconcile what you are mid-way through against what Grimoire records. Grimoire is the record."
|
||||
fi
|
||||
|
||||
# Nothing at all to inject → stay silent.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe — release this session's task claims as it ends.
|
||||
# Grimoire — release this session's task claims as it ends.
|
||||
#
|
||||
# A claim records a session's attention, and a session that ends has none left
|
||||
# to give. This is the mechanical half of the hand-off: it needs nothing from
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — Stop hook: the turn's new shapes are judged by the agent
|
||||
# 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
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# Scribe — Claude Code adapter
|
||||
# Grimoire — Claude Code adapter
|
||||
|
||||
This session is connected to **Scribe**, the operator's system of record for
|
||||
their work, through the `scribe` MCP tools. How to work with Scribe is stated
|
||||
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
|
||||
and whenever you are unsure what Scribe expects. Each tool's contract is in its
|
||||
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,
|
||||
shape-accounting, family-canon) carry their arcs.
|
||||
|
||||
What only Claude Code needs said:
|
||||
|
||||
- **Keep one copy — in Scribe, not Claude Code's local memory.** Claude
|
||||
- **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 Scribe instead, and using-scribe says how
|
||||
Scribe works alongside them.
|
||||
plans and project notes go to Grimoire instead, and using-scribe 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, Scribe may add rules, preferences,
|
||||
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
|
||||
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
|
||||
long session, log it to Scribe, then tell the operator it's a good moment to
|
||||
long session, log it to Grimoire, then tell the operator it's a good moment to
|
||||
`/compact` and name what you logged. You can't run it yourself; suggest it at
|
||||
seams, not every turn.
|
||||
- **When the operator wraps up, hand off first.** Log where each task you
|
||||
@@ -31,5 +31,5 @@ What only Claude Code needs said:
|
||||
- **Stored Processes arrive as skills** (`scribe-proc-*`), refreshed at session
|
||||
start. After a Process is added or edited, `/scribe:sync` makes it available
|
||||
straight away.
|
||||
- **If the Scribe tools are unavailable, say so** rather than silently falling
|
||||
- **If the Grimoire tools are unavailable, say so** rather than silently falling
|
||||
back to local notes.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — sync stored Processes into auto-surfacing local skills.
|
||||
# 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
|
||||
# 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 Scribe (the DB stays the single source of truth — the stub is a pointer).
|
||||
# from Grimoire (the DB stays the single source of truth — the stub is a pointer).
|
||||
#
|
||||
# WHY LOCAL ~/.claude/skills (not the plugin dir): the plugin is git-cloned and
|
||||
# identical on every install, so instance-specific stubs can't live inside it.
|
||||
@@ -65,9 +65,9 @@ while [ "$i" -lt "$count" ]; do
|
||||
printf 'name: scribe-proc-%s\n' "$slug"
|
||||
printf 'description: %s\n' "$desc"
|
||||
printf -- '---\n\n'
|
||||
printf '<!-- GENERATED by the Scribe plugin (scribe_sync_processes.sh). Do not edit here; edit the Process in Scribe and re-sync with /scribe:sync. -->\n\n'
|
||||
printf 'This is a saved **Scribe Process**: `%s`.\n\n' "$name"
|
||||
printf 'Do not improvise it. Call `get_process("%s")` via the Scribe MCP server to fetch the live procedure, then follow the returned body verbatim — including any "clarify first" steps it contains.\n' "$name"
|
||||
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 '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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe — PreToolUse rule arm for ACTIONS.
|
||||
# Grimoire — PreToolUse rule arm for ACTIONS.
|
||||
#
|
||||
# The sibling of scribe_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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Scribe plugin — what happened in the LAST TURN of a transcript.
|
||||
# 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
|
||||
# in `mode=lines` from a Claude Code transcript, and answers the four questions
|
||||
|
||||
@@ -12,7 +12,7 @@ real options and a reasoned choice — not a single path defended after the fact
|
||||
|
||||
## Recall first
|
||||
|
||||
`search` Scribe before generating from scratch — a prior decision, note, or
|
||||
`search` Grimoire before generating from scratch — a prior decision, note, or
|
||||
brainstorm on this often already exists. Build on it instead of repeating it.
|
||||
|
||||
## Open up
|
||||
@@ -30,7 +30,7 @@ brainstorm on this often already exists. Build on it instead of repeating it.
|
||||
|
||||
## Capture the decision
|
||||
|
||||
When a direction is chosen, record it in Scribe (`create_note`, e.g. tag
|
||||
When a direction is chosen, record it in Grimoire (`create_note`, e.g. tag
|
||||
`decision`): the choice, the alternatives weighed, and the reason. That's what
|
||||
keeps the same question from being re-litigated later — and what a future
|
||||
session reads to understand *why*, not just *what*.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: reporting-back
|
||||
description: Use when you are about to write the reply the operator will read and the delivered reply shape is not enough — work finished, stopping on a blocker, asking them to decide or to do something, answering "where are we", or proposing an approach. The long-form reference behind the reply shapes Scribe delivers: why sections are chosen rather than filled, who decides what, taking placement from the record, and a completion report worked in full. Triggers on reporting completion, handing off, asking a question, or summarising progress.
|
||||
description: Use when you are about to write the reply the operator will read and the delivered reply shape is not enough — work finished, stopping on a blocker, asking them to decide or to do something, answering "where are we", or proposing an approach. The long-form reference behind the reply shapes Grimoire delivers: why sections are chosen rather than filled, who decides what, taking placement from the record, and a completion report worked in full. Triggers on reporting completion, handing off, asking a question, or summarising progress.
|
||||
metadata:
|
||||
moments: reply.report
|
||||
---
|
||||
@@ -14,7 +14,7 @@ still unreadable to them. Shape it around **where the work stands**.
|
||||
|
||||
## The shapes arrive on their own
|
||||
|
||||
Scribe delivers the default shape for each kind of reply as product, at the
|
||||
Grimoire delivers the default shape for each kind of reply as product, at the
|
||||
moment it is due: the core ("Reply shape · Every reply") with each turn, the
|
||||
completion report when a task closes, the asks when you put a question to the
|
||||
operator, the plan when you plan. A shape this session has already seen comes
|
||||
@@ -61,7 +61,7 @@ and it costs the operator the decision twice.
|
||||
## Take the placement from the record
|
||||
|
||||
A remembered milestone title or "next step" reads exactly like a real one when
|
||||
it is wrong. So take placement from Scribe:
|
||||
it is wrong. So take placement from Grimoire:
|
||||
|
||||
- `update_task` and `create_task` return a **`placement`** block — the project,
|
||||
the milestone, `position` (step N of M), `progress`, and `next` (the next open
|
||||
|
||||
@@ -41,7 +41,7 @@ through recall/auto-inject; this skill is the active reflex around that.
|
||||
turn's shapes — reuse from memory leaves no such evidence.
|
||||
- If auto-inject already surfaced a snippet title that looks relevant, that's
|
||||
your cue to `get_snippet` it rather than start from scratch.
|
||||
- **Prior art offered beside a write is not noise — read it.** When Scribe notes
|
||||
- **Prior art offered beside a write is not noise — read it.** When Grimoire notes
|
||||
that a snippet is already recorded for the file you just wrote or edited, open
|
||||
it before you go any further. Either it's the helper you were about to
|
||||
duplicate — reuse it and drop yours — or it isn't, and the record needs the new
|
||||
@@ -59,7 +59,7 @@ through recall/auto-inject; this skill is the active reflex around that.
|
||||
YOUR edit's responsibility: if the edit changes the recorded shape,
|
||||
`update_snippet(id, code=…)` with the new form as part of the same task; if
|
||||
it doesn't, `verify_snippet(id, status="ok", commit_sha=…)` costs one call
|
||||
and re-stamps the record as checked. Scribe never reads the repo — this
|
||||
and re-stamps the record as checked. Grimoire never reads the repo — this
|
||||
moment, in the session that has the context, is the only place the record
|
||||
gets kept true.
|
||||
|
||||
@@ -115,7 +115,7 @@ shape is asked about once.
|
||||
|
||||
## A shared snippet is a suggestion, not a standard
|
||||
|
||||
Scribe is multi-user, so a search can return snippets other people own. Those
|
||||
Grimoire is multi-user, so a search can return snippets other people own. Those
|
||||
come back marked `shared: true` with an `owner`.
|
||||
|
||||
- Read one as **that person's suggestion**, not as the way things are done here.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: systematic-debugging
|
||||
description: Use when diagnosing a bug, failure, or unexpected behavior — investigate methodically instead of guessing. Triggers on "why is this failing/breaking", a stack trace, a flaky test, or any "it should work but doesn't". On resolution, capture the issue in Scribe so it isn't re-debugged from scratch.
|
||||
description: Use when diagnosing a bug, failure, or unexpected behavior — investigate methodically instead of guessing. Triggers on "why is this failing/breaking", a stack trace, a flaky test, or any "it should work but doesn't". On resolution, capture the issue in Grimoire so it isn't re-debugged from scratch.
|
||||
metadata:
|
||||
moments: work.debug
|
||||
---
|
||||
@@ -12,7 +12,7 @@ guessed fix that "seems to work" often just moves the bug somewhere else.
|
||||
|
||||
## Recall first
|
||||
|
||||
Before digging in, `search` Scribe for the symptom — a prior issue
|
||||
Before digging in, `search` Grimoire for the symptom — a prior issue
|
||||
(`list_tasks(kind="issue")` or `search`) may already hold the cause and the fix.
|
||||
Don't re-debug what's already solved.
|
||||
|
||||
@@ -32,7 +32,7 @@ Don't re-debug what's already solved.
|
||||
|
||||
## Capture the issue (so it's findable)
|
||||
|
||||
When resolved, record it in Scribe as its own issue (`create_task(kind="issue")`):
|
||||
When resolved, record it in Grimoire as its own issue (`create_task(kind="issue")`):
|
||||
**symptom → root cause → fix → how it was verified** in the body, optionally
|
||||
linked to the task it arose from (`arose_from_id`) and the subsystem it touches
|
||||
(`system_ids`). Even a problem fixed in passing is worth two lines — that's how
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: using-scribe
|
||||
description: Use at the START of every session, and before answering anything about the operator's work or starting any task — establishes the Scribe-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 Scribe not in files.
|
||||
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
|
||||
---
|
||||
|
||||
# Using Scribe
|
||||
# Using Grimoire
|
||||
|
||||
Scribe is the operator's self-hosted system of record (notes, tasks, issues,
|
||||
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
|
||||
**already holds** — so make reading it a reflex, not something you wait to be
|
||||
@@ -15,7 +15,7 @@ asked for.
|
||||
|
||||
## Do this first (every session)
|
||||
|
||||
If the working directory maps to a Scribe project, call `enter_project(id)` —
|
||||
If the working directory maps to a Grimoire project, call `enter_project(id)` —
|
||||
it returns the project's goal, the milestones and open tasks worked on most
|
||||
recently, its Systems and the titles of its own rules in one shot.
|
||||
|
||||
@@ -26,30 +26,30 @@ Then **ask before you act**: before anything hard to reverse or outward-facing,
|
||||
search the rules for what you are about to do. Reflex 2 below is why asking,
|
||||
not loading, is how the operator's rules reach you.
|
||||
|
||||
## Scribe holds these functions — keep one copy
|
||||
## Grimoire holds these functions — keep one copy
|
||||
|
||||
Scribe is the home for the operator's **rules, recall, and planning** — the
|
||||
Grimoire is the home for the operator's **rules, recall, and planning** — the
|
||||
jobs your client's own local memory files would otherwise do. Route those jobs
|
||||
to Scribe instead of also writing them locally: codify rules with
|
||||
`create_rule` / `create_project_rule`, capture durable knowledge as Scribe
|
||||
notes, and keep plans in Scribe milestones (via `start_planning`). One copy, in
|
||||
Scribe; let any existing local memory shrink as Scribe takes over.
|
||||
to Grimoire instead of also writing them locally: codify rules with
|
||||
`create_rule` / `create_project_rule`, capture durable knowledge as Grimoire
|
||||
notes, and keep plans in Grimoire milestones (via `start_planning`). One copy, in
|
||||
Grimoire; let any existing local memory shrink as Grimoire takes over.
|
||||
|
||||
Two constraints on *how* that's achieved:
|
||||
|
||||
- **Steer behaviour; leave the client's own settings as they are.** Scribe works
|
||||
- **Steer behaviour; leave the client's own settings as they are.** Grimoire works
|
||||
alongside a client's built-in memory at its defaults. You replace memory's
|
||||
functions by *doing the work in Scribe*, so there is no reason to ask the
|
||||
functions by *doing the work in Grimoire*, so there is no reason to ask the
|
||||
operator to switch a built-in feature off — a setting they didn't knowingly
|
||||
change is breakage they won't know to restore.
|
||||
- **A Scribe-shaped hole is acceptable.** If Scribe is later removed, the
|
||||
- **A Grimoire-shaped hole is acceptable.** If Grimoire is later removed, the
|
||||
operator recovers context over time — that's fine. Local memory doesn't need
|
||||
to be kept as a self-sufficient fallback.
|
||||
|
||||
## The reflex
|
||||
|
||||
1. **Recall before acting.** Before answering a question about the operator's
|
||||
work, or starting a task, `search` Scribe (and `list_tasks` / `list_notes`)
|
||||
work, or starting a task, `search` Grimoire (and `list_tasks` / `list_notes`)
|
||||
for related prior work — an existing task, decision, or note — instead of
|
||||
re-deriving it or opening a duplicate. When a project is in scope, pass its
|
||||
`project_id` so results stay scoped.
|
||||
@@ -129,7 +129,7 @@ Two constraints on *how* that's achieved:
|
||||
|
||||
**Preferences shape how work is done, never what gets recorded.** They
|
||||
govern your conduct — how you report, how carefully you pace, which form
|
||||
you reach for. What ends up in Scribe is decided by what the record is:
|
||||
you reach for. What ends up in Grimoire is decided by what the record is:
|
||||
a preference never makes a task into a note, downgrades a rule, or keeps
|
||||
something out of the corpus that belongs there.
|
||||
|
||||
@@ -151,7 +151,7 @@ Two constraints on *how* that's achieved:
|
||||
3. **Update over duplicate.** When recording, prefer updating an existing
|
||||
note/rule/task over creating a new one. Search first; revise what's there.
|
||||
|
||||
4. **When you plan, plan in Scribe.** Work with an *arc* — several steps toward
|
||||
4. **When you plan, plan in Grimoire.** Work with an *arc* — several steps toward
|
||||
one goal — gets a plan, and a plan is a milestone: `start_planning(project_id,
|
||||
title)` creates one whose `body` holds the design, each step is its own task
|
||||
under it (`create_task(milestone_id=...)`), progress goes in work-logs
|
||||
@@ -174,7 +174,7 @@ Two constraints on *how* that's achieved:
|
||||
`/clear`, the operator wrapping up for the day — each is the last moment the
|
||||
reasoning behind the work lives anywhere but here. Log on the task you were
|
||||
holding where it stands, what you tried and ruled out, and the next move:
|
||||
write down what the next session needs, because it arrives with Scribe's
|
||||
write down what the next session needs, because it arrives with Grimoire's
|
||||
record and nothing else. Moving a task to `in_progress` or logging on it also
|
||||
claims it for this session — that claim is what hands the work back to you
|
||||
after a compaction, and it ends on its own when you stop, so there is nothing
|
||||
@@ -211,14 +211,14 @@ Two constraints on *how* that's achieved:
|
||||
cover, and they plainly belong to that area, add them with `update_system`
|
||||
— which files are which area is your call, as naming the area was.
|
||||
|
||||
8. **Name the record, never just its number.** Whenever you refer to a Scribe
|
||||
8. **Name the record, never just its number.** Whenever you refer to a Grimoire
|
||||
record — in a message to the operator, a commit message, a task body, a
|
||||
work-log — write the id *and* its title: `#3244 "the staleness signal"`,
|
||||
`milestone 323 "rule versioning"`. Not `#3244`.
|
||||
|
||||
You have the record open; the operator does not. A bare id reads as
|
||||
complete to you and as homework to them — they have to look it up to know
|
||||
what their own conversation is about, or guess. Scribe's own duplicate gate
|
||||
what their own conversation is about, or guess. Grimoire's own duplicate gate
|
||||
already writes `id 412: "debounce helper"` for exactly this reason; match
|
||||
it everywhere else.
|
||||
|
||||
@@ -231,7 +231,7 @@ Two constraints on *how* that's achieved:
|
||||
|
||||
**An id exists only once a create call returns it.** Never write the id you
|
||||
expect a record to get — every session and user draws from one sequence,
|
||||
so the number goes to whoever creates next, and Scribe refuses a body that
|
||||
so the number goes to whoever creates next, and Grimoire refuses a body that
|
||||
cites an id not yet assigned. Records that must cite each other are created
|
||||
together — `create_records` or `start_planning(steps=...)` — with
|
||||
`{{ref:N}}` where the Nth record's id belongs.
|
||||
@@ -263,7 +263,7 @@ Two constraints on *how* that's achieved:
|
||||
order you did things in: which task or milestone it belongs to, what now
|
||||
works, what needs them, and what comes next. Take the placement from the
|
||||
`placement` block that `create_task` / `update_task` return — the milestone,
|
||||
step N of M, the next open step — rather than from memory. Scribe delivers
|
||||
step N of M, the next open step — rather than from memory. Grimoire delivers
|
||||
the shape for each kind of reply when it is due — the core with each turn,
|
||||
the completion report when a task closes, the asks when you put a question —
|
||||
and `list_reply_shapes` has them all; the `reporting-back` skill holds the
|
||||
@@ -302,7 +302,7 @@ moment rather than on every turn:
|
||||
|
||||
## You are the judge of what the record says
|
||||
|
||||
Scribe's shape exists to support how you work, so you are the judge of record
|
||||
Grimoire's shape exists to support how you work, so you are the judge of record
|
||||
for the work itself: what a shape is, which canon a thing belongs to, whether
|
||||
a finding holds, whether something is done. `classify_shapes`, `update_task`,
|
||||
`create_snippet` and the rest are where your judgment lands — under your own
|
||||
@@ -342,8 +342,8 @@ token: `resolve_design_system(id)` for the values, or
|
||||
stated outside the system, so it can never follow a palette change — and
|
||||
nothing will tell you it drifted.
|
||||
|
||||
## Other Scribe process-skills
|
||||
## Other Grimoire process-skills
|
||||
|
||||
Scribe also ships focused process-skills — writing-plans, reporting-back,
|
||||
Grimoire also ships focused process-skills — writing-plans, reporting-back,
|
||||
systematic debugging, verification, and brainstorming. Reach for the matching one when its
|
||||
situation arises, the same way you reach for this skill.
|
||||
|
||||
@@ -13,7 +13,7 @@ a `.scribe` file naming the project:
|
||||
{"instance": "https://scribe.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 Scribe — so a marker that has travelled to
|
||||
it is a different project on every Grimoire — so a marker that has travelled to
|
||||
another instance is ignored rather than followed to the wrong project. A bare
|
||||
`2` also works when writing the file by hand. When work plainly belongs to a
|
||||
project and the directory names none, offer to write the marker;
|
||||
|
||||
@@ -71,7 +71,7 @@ said twice at two strengths, which is worse than either alone: a session that
|
||||
retrieves the softer copy has been told that binding guidance is optional.
|
||||
|
||||
**First ask whether it's a rule at all.** A rule is prose you have to remember
|
||||
and apply; Scribe's other entities are structure a tool can resolve and check.
|
||||
and apply; Grimoire's other entities are structure a tool can resolve and check.
|
||||
Visual standards belong in a **design system**, not a rulebook — a token can be
|
||||
inherited, resolved per mode, rendered to a stylesheet and diffed against code,
|
||||
and none of that survives being written as a rule. A repeatable procedure is a
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: writing-plans
|
||||
description: Use when a piece of work has an arc — several steps toward one goal, worth tracking as a unit — and you want the approach reviewable before you start. Triggers when the user asks you to plan, design an approach, or scope an effort, or when work is about to sprawl across several steps. Not for single-step work. The plan lives in a Scribe milestone (via start_planning), not a local file.
|
||||
description: Use when a piece of work has an arc — several steps toward one goal, worth tracking as a unit — and you want the approach reviewable before you start. Triggers when the user asks you to plan, design an approach, or scope an effort, or when work is about to sprawl across several steps. Not for single-step work. The plan lives in a Grimoire milestone (via start_planning), not a local file.
|
||||
metadata:
|
||||
moments: work.plan
|
||||
---
|
||||
@@ -56,7 +56,7 @@ the project has the same title or reads as the same plan, they return it as
|
||||
`existing_milestone` and create nothing. Add to that plan, or pass `force=true`
|
||||
only when you have read it and this really is separate work.
|
||||
|
||||
## When it does: start the plan in Scribe, not a file
|
||||
## When it does: start the plan in Grimoire, not a file
|
||||
|
||||
Call **`start_planning(project_id, title)`** before designing or implementing —
|
||||
so the milestone exists to write into, rather than being backfilled from work
|
||||
@@ -78,7 +78,7 @@ rules. The plan lives in that milestone:
|
||||
**Never write an id you have not been given.** A plan body that says "see
|
||||
#4012" before #4012 exists is a guess, and other sessions and users are
|
||||
creating records from the same sequence at the same time — the number goes to
|
||||
whoever creates next, and the plan then points at their record. Scribe refuses
|
||||
whoever creates next, and the plan then points at their record. Grimoire refuses
|
||||
a body citing an id that has not been assigned. Where the plan and its steps
|
||||
need to cite each other, write a placeholder instead: `{{ref:N}}` is the Nth
|
||||
step in the list, `{{ref:milestone}}` is the milestone. They are replaced with
|
||||
@@ -88,7 +88,7 @@ the real id and title as the records are created.
|
||||
record, not a file on disk. A plan is never a task: `kind="plan"` is refused,
|
||||
and `start_planning` makes a milestone.
|
||||
|
||||
Before designing from scratch, **recall**: `search` Scribe for a related prior
|
||||
Before designing from scratch, **recall**: `search` Grimoire for a related prior
|
||||
decision or note. Often the thinking (or half of it) already exists.
|
||||
|
||||
## What a good plan contains
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ dependencies = [
|
||||
"alembic>=1.13",
|
||||
"httpx>=0.27",
|
||||
# Seals the backup destination's S3 secret at rest (services/secrets.py).
|
||||
# Already in the image through pyjwt; named here because Scribe now
|
||||
# Already in the image through pyjwt; named here because Grimoire now
|
||||
# imports it directly.
|
||||
"cryptography",
|
||||
"hypercorn>=0.17",
|
||||
|
||||
@@ -92,7 +92,7 @@ def style_source(path: pathlib.Path) -> str:
|
||||
# A rule that paints text with a colour token AND its own -bg tint of the same
|
||||
# token. The pair looks harmonious and is close to illegible: a 12% tint of a
|
||||
# hue sits near the surface, so the hue as text on it lands around 2:1 against
|
||||
# an AA floor of 4.5. Measured across the whole Scribe ladder in 2026-08:
|
||||
# an AA floor of 4.5. Measured across the whole Grimoire ladder in 2026-08:
|
||||
# every one of the six pairs failed on the dark palette, worst 1.60:1.
|
||||
#
|
||||
# The fix is always the same and always available — the token's `-fg` sibling,
|
||||
|
||||
@@ -282,7 +282,7 @@ def check_shellcheck() -> None:
|
||||
# network, so "silent" would be the wrong assertion for it.
|
||||
#
|
||||
# UNREACHABLE is different for the two write-path hooks: a call
|
||||
# that was owed and did not come back is SAID, once per outage ("> Scribe did
|
||||
# that was owed and did not come back is SAID, once per outage ("> Grimoire did
|
||||
# not answer …"), so a session can tell "checked, nothing there" from "never
|
||||
# checked". That line — or silence, when the once-per-outage marker in
|
||||
# ${TMPDIR:-/tmp}/scribe-priorart/ was set by a run in the last ten minutes —
|
||||
@@ -424,7 +424,7 @@ STATIC_EMITTERS = {
|
||||
}
|
||||
# The hooks that say so when a configured instance does not answer.
|
||||
OUTAGE_SPEAKERS = {"scribe_prior_art.sh", "scribe_after_write.sh"}
|
||||
OUTAGE_LINE = "> Scribe did not answer the prior-art check"
|
||||
OUTAGE_LINE = "> Grimoire did not answer the prior-art check"
|
||||
|
||||
|
||||
def _run_hook(script: Path, event: str, env_extra: dict[str, str]) -> subprocess.CompletedProcess:
|
||||
@@ -492,7 +492,7 @@ def check_fail_open() -> None:
|
||||
def check_local_prior_art_needs_no_instance() -> None:
|
||||
"""The prior-art hook's local arm must answer with no credentials.
|
||||
|
||||
The other arms ask Scribe what was RECORDED. This one asks the repo what
|
||||
The other arms ask Grimoire what was RECORDED. This one asks the repo what
|
||||
EXISTS, which needs no instance — and that is the whole reason it catches
|
||||
the case the recorded arms structurally cannot: a helper nobody thought to
|
||||
record. If it ever silently starts depending on configuration, it stops
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""One-shot import of FabledRulebook .md files into the Scribe DB.
|
||||
"""One-shot import of FabledRulebook .md files into the Grimoire DB.
|
||||
|
||||
Usage:
|
||||
python -m scripts.import_fabledrulebook \\
|
||||
@@ -169,7 +169,7 @@ def main() -> None:
|
||||
parser.add_argument("--source-dir", required=True,
|
||||
help="Path to the FabledRulebook directory")
|
||||
parser.add_argument("--user-id", type=int, required=True,
|
||||
help="Scribe user id who will own the rulebook")
|
||||
help="Grimoire user id who will own the rulebook")
|
||||
parser.add_argument("--rulebook-title", default="FabledSword family")
|
||||
parser.add_argument("--subscribe-projects", type=_comma_ints,
|
||||
help="Comma-separated project ids to auto-subscribe")
|
||||
|
||||
@@ -33,12 +33,12 @@ class Config:
|
||||
SMTP_USERNAME: str = os.environ.get("SMTP_USERNAME", "")
|
||||
SMTP_PASSWORD: str = _read_secret("SMTP_PASSWORD", "SMTP_PASSWORD_FILE", "")
|
||||
SMTP_FROM_ADDRESS: str = os.environ.get("SMTP_FROM_ADDRESS", "")
|
||||
SMTP_FROM_NAME: str = os.environ.get("SMTP_FROM_NAME", "Fabled Scribe")
|
||||
SMTP_FROM_NAME: str = os.environ.get("SMTP_FROM_NAME", "Fabled Grimoire")
|
||||
SMTP_USE_TLS: bool = os.environ.get("SMTP_USE_TLS", "true").lower() in ("1", "true", "yes")
|
||||
BASE_URL: str = os.environ.get("BASE_URL", "http://localhost:5000").rstrip("/")
|
||||
TRUST_PROXY_HEADERS: bool = os.environ.get("TRUST_PROXY_HEADERS", "").lower() in ("1", "true", "yes")
|
||||
|
||||
# Git URL of the repo that ships this Scribe plugin. There is one correct
|
||||
# Git URL of the repo that ships this Grimoire plugin. There is one correct
|
||||
# value per deployment (the repo serving the plugin), so it's a config
|
||||
# default — not something each user types. Surfaced in Settings → MCP Access
|
||||
# as the default install-command marketplace; an admin can still override it
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""In-app MCP server. Exposes Fabled Scribe data via the MCP streamable-HTTP transport.
|
||||
"""In-app MCP server. Exposes Fabled Grimoire data via the MCP streamable-HTTP transport.
|
||||
|
||||
Auth uses the existing `api_keys` table: a Bearer token in the Authorization
|
||||
header resolves to a user, and every tool call acts on that user's data only.
|
||||
|
||||
@@ -10,7 +10,7 @@ from quart import Quart
|
||||
|
||||
## What this block is — read before editing it
|
||||
#
|
||||
# ONE OWNER PER PIECE OF GUIDANCE. Scribe's guidance to agents lives where it
|
||||
# ONE OWNER PER PIECE OF GUIDANCE. Grimoire's guidance to agents lives where it
|
||||
# can be delivered, and each topic is stated in full exactly once:
|
||||
# - Tool docstrings: each tool's contract, delivered with its schema.
|
||||
# - In-band tool responses: behaviour prose cannot be trusted to trigger —
|
||||
@@ -38,9 +38,9 @@ from quart import Quart
|
||||
# sit past the fold. A block pressed against the cap is being used as a
|
||||
# rulebook.
|
||||
_INSTRUCTIONS = """
|
||||
Scribe is the operator's system of record, and yours: recall before acting,
|
||||
Grimoire is the operator's system of record, and yours: recall before acting,
|
||||
record as you go, keep one copy here, not in local memory. Each
|
||||
practice is stated in full in a skill or a tool's description.
|
||||
practice is stated in full in a skill or a tool description.
|
||||
|
||||
- Start with enter_project(id): the project, open work, Systems, design
|
||||
system. `inception`: ask what it inherits, then decide_project_inception.
|
||||
@@ -63,7 +63,7 @@ practice is stated in full in a skill or a tool's description.
|
||||
- In UI work the design system binds: resolve_design_system before
|
||||
writing a value.
|
||||
|
||||
Creates are duplicate-gated: a near-match returns the existing id to update.
|
||||
Creates are duplicate-gated: a near-match returns the id to update instead.
|
||||
shared:true records are another user's suggestion, not settled practice.
|
||||
"""
|
||||
|
||||
@@ -337,7 +337,7 @@ class StrictArgsMCPServer(MCPServer):
|
||||
try:
|
||||
return await super().call_tool(name, arguments, context)
|
||||
except UnexpectedToolError as exc:
|
||||
# ValueError is how every Scribe tool refuses: what was refused,
|
||||
# ValueError is how every Grimoire tool refuses: what was refused,
|
||||
# why, and what to do instead, written for the agent. SDK 2.x
|
||||
# masks it like a crash, which would leave the agent retrying
|
||||
# blind. Other exceptions ARE crashes; their text can carry
|
||||
|
||||
@@ -408,7 +408,7 @@ async def judge_lesson_link(
|
||||
"""Say whether a lesson is an instance of a rule: `verdict` is "confirm"
|
||||
or "reject".
|
||||
|
||||
Reach for this when Scribe proposes a pair — a lesson and a rule that keep
|
||||
Reach for this when Grimoire proposes a pair — a lesson and a rule that keep
|
||||
arriving together in different situations — or whenever you are reading a
|
||||
lesson and recognise the rule it falls under. Confirming makes the rule
|
||||
reachable through the situation the lesson describes; rejecting records
|
||||
|
||||
@@ -24,7 +24,7 @@ from scribe.services import moment_delivery
|
||||
|
||||
|
||||
async def list_milestones(project_id: int) -> dict:
|
||||
"""List milestones for a Scribe project, ordered by order_index.
|
||||
"""List milestones for a Grimoire project, ordered by order_index.
|
||||
|
||||
Returns every milestone, done ones included: id, title, description,
|
||||
status (active/done), order_index, progress (total, completed, pct,
|
||||
@@ -98,7 +98,7 @@ async def create_milestone(
|
||||
status: str = "active",
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
"""Create a milestone within a Scribe project.
|
||||
"""Create a milestone within a Grimoire project.
|
||||
|
||||
A milestone can serve as a plan container — put the design/intent in `body`
|
||||
and track each step as a child task (create_task(milestone_id=...)). For a
|
||||
@@ -148,7 +148,7 @@ async def update_milestone(
|
||||
status: str = "",
|
||||
order_index: int = -1,
|
||||
) -> dict:
|
||||
"""Update a Scribe milestone. Only explicitly provided fields are changed.
|
||||
"""Update a Grimoire milestone. Only explicitly provided fields are changed.
|
||||
|
||||
Args:
|
||||
project_id: Project the milestone belongs to (preserved for API parity;
|
||||
|
||||
@@ -34,7 +34,7 @@ async def list_notes(
|
||||
search_text: str = "",
|
||||
project_id: int = 0,
|
||||
) -> dict:
|
||||
"""List notes (non-task documents) stored in Scribe.
|
||||
"""List notes (non-task documents) stored in Grimoire.
|
||||
|
||||
Optionally filter by a single tag (plain string, no # prefix) or by
|
||||
`search_text`, which matches on MEANING — the same semantic match every
|
||||
@@ -70,7 +70,7 @@ async def list_notes(
|
||||
|
||||
|
||||
async def get_note(note_id: int, project_id: int = 0) -> dict:
|
||||
"""Fetch the full content of a single Scribe note by its ID.
|
||||
"""Fetch the full content of a single Grimoire note by its ID.
|
||||
|
||||
Returns id, title, body (markdown), tags, project_id, created_at,
|
||||
updated_at — plus `systems` (the areas this note is filed under) or, for
|
||||
@@ -120,7 +120,7 @@ async def create_note(
|
||||
expires_when: str = "",
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
"""Create a new note in Scribe.
|
||||
"""Create a new note in Grimoire.
|
||||
|
||||
WHAT ELSE COULD HOLD THIS? A note is the right home when the answer is
|
||||
"nothing": it records what you know, nobody owes anything on it, and
|
||||
@@ -259,7 +259,7 @@ async def update_note(
|
||||
expires_when: str = "",
|
||||
clear: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Update an existing Scribe note. Only explicitly provided fields are changed.
|
||||
"""Update an existing Grimoire note. Only explicitly provided fields are changed.
|
||||
|
||||
Args:
|
||||
note_id: ID of the note to update.
|
||||
@@ -376,7 +376,7 @@ async def find_duplicate_records(kind: str = "note", threshold: float = 0.0) ->
|
||||
|
||||
|
||||
async def delete_note(note_id: int) -> dict:
|
||||
"""Move a Scribe note to the trash (recoverable). Restore via restore(batch_id)."""
|
||||
"""Move a Grimoire note to the trash (recoverable). Restore via restore(batch_id)."""
|
||||
uid = current_user_id()
|
||||
# Read the title BEFORE the delete: afterwards the row is trashed and the
|
||||
# confirmation could only echo the number back. A deletion the operator
|
||||
|
||||
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def list_projects() -> dict:
|
||||
"""List all Scribe projects for the current user.
|
||||
"""List all Grimoire projects for the current user.
|
||||
|
||||
Returns id, title, description, goal, status (active/paused/completed/archived), color,
|
||||
and a short auto-generated summary for each project.
|
||||
@@ -329,7 +329,7 @@ async def enter_project(project_id: int) -> dict:
|
||||
|
||||
|
||||
async def get_project(project_id: int) -> dict:
|
||||
"""Fetch a Scribe project by ID.
|
||||
"""Fetch a Grimoire project by ID.
|
||||
|
||||
Returns full project fields, a milestone_summary list (every milestone,
|
||||
with description, progress and `next_step` — the earliest still-open step,
|
||||
@@ -382,7 +382,7 @@ async def create_project(
|
||||
seed_systems: bool | None = None,
|
||||
platforms: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Create a new project in Scribe — and decide what it inherits.
|
||||
"""Create a new project in Grimoire — and decide what it inherits.
|
||||
|
||||
A project's inheritance is a decision, not a default:
|
||||
before calling, ask the operator the three inception questions and pass
|
||||
@@ -463,7 +463,7 @@ async def update_project(
|
||||
status: str = "",
|
||||
color: str = "",
|
||||
) -> dict:
|
||||
"""Update an existing Scribe project. Only explicitly provided fields are changed.
|
||||
"""Update an existing Grimoire project. Only explicitly provided fields are changed.
|
||||
|
||||
Args:
|
||||
project_id: ID of the project to update.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Repo -> project binding MCP tools.
|
||||
|
||||
Let the operator map the working git repository to a Scribe project so the
|
||||
Let the operator map the working git repository to a Grimoire project so the
|
||||
SessionStart hook auto-loads that project's context — without pinning a project
|
||||
id in plugin config (which would force a single project for every repo). Run
|
||||
`bind_repo` once per repo; the binding is keyed on the normalized git remote, so
|
||||
@@ -14,7 +14,7 @@ from scribe.services import repo_bindings as repo_bindings_svc
|
||||
|
||||
|
||||
async def bind_repo(repo_url: str, project_id: int, ref: str = "") -> dict:
|
||||
"""Bind a git repository to a Scribe project for session-start context.
|
||||
"""Bind a git repository to a Grimoire project for session-start context.
|
||||
|
||||
After this, any session started in that repo auto-loads the project's
|
||||
context (the SessionStart hook sends the repo's remote; the server resolves
|
||||
@@ -30,7 +30,7 @@ async def bind_repo(repo_url: str, project_id: int, ref: str = "") -> dict:
|
||||
Args:
|
||||
repo_url: the repo's git remote (e.g. the output of
|
||||
`git remote get-url origin` — ssh or https form, both work).
|
||||
project_id: the Scribe project this repo represents.
|
||||
project_id: the Grimoire project this repo represents.
|
||||
ref: branch the ledger follows ("" = leave as is / default branch on
|
||||
a new binding; "-" = clear to the default branch).
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,7 @@ from scribe.services import retrieval_tuning as tuning_svc
|
||||
|
||||
|
||||
async def retrieval_surfaces() -> dict:
|
||||
"""Every retrieval surface Scribe pushes on, with its floor and its budget.
|
||||
"""Every retrieval surface Grimoire pushes on, with its floor and its budget.
|
||||
|
||||
Read this before changing either, and read it beside
|
||||
`retrieval_telemetry(days=…, near_miss_samples=5)` — this tool says what is
|
||||
@@ -55,7 +55,7 @@ async def retrieval_surfaces() -> dict:
|
||||
- `stale: true` with `shape_changed` — the documents are cut differently.
|
||||
The scale still holds, but what a record embeds has changed, so which
|
||||
records clear a bar has.
|
||||
- `stale: null`, `source: "unstamped"` — the dial was moved before Scribe
|
||||
- `stale: null`, `source: "unstamped"` — the dial was moved before Grimoire
|
||||
recorded this. It was measured under SOMETHING and there is no way to say
|
||||
what, which is a different answer from "it is fine". Treat it as worth
|
||||
re-measuring, and the next tuning call stamps it.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""MCP tools for the Scribe Rulebook system.
|
||||
"""MCP tools for the Grimoire Rulebook system.
|
||||
|
||||
Rulebook / topic / rule CRUD and the rule-to-rule
|
||||
edges. Thin wrappers over services/rulebooks.py — ownership is enforced in the
|
||||
|
||||
@@ -213,13 +213,13 @@ async def search(
|
||||
project_id: int = 0,
|
||||
system_id: int = 0,
|
||||
) -> dict:
|
||||
"""Semantic search over the user's existing notes and tasks — Scribe's recall.
|
||||
"""Semantic search over the user's existing notes and tasks — Grimoire's recall.
|
||||
|
||||
Reach for this BEFORE answering a question about the user's work or starting
|
||||
a task: the user's second-brain almost always already holds related prior
|
||||
art. Check for an existing ticket before opening a new one (search with
|
||||
content_type='task'), and for prior notes/decisions before re-deriving them.
|
||||
Treating Scribe as the first place to look — not a place to only write — is
|
||||
Treating Grimoire as the first place to look — not a place to only write — is
|
||||
the difference between it being a trustworthy record and a write-only log.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -51,7 +51,7 @@ async def list_snippets(
|
||||
usually what you want, since a helper you need here may well have
|
||||
been written somewhere else.
|
||||
repo: Narrow to snippets recorded in this repo, matched exactly — the
|
||||
same repo string used when recording, e.g. "Scribe".
|
||||
same repo string used when recording, e.g. "Grimoire".
|
||||
path: Narrow to snippets recorded at this path. Matches the exact file
|
||||
OR anything beneath it, so "frontend/src" finds
|
||||
"frontend/src/lib/x.ts" as well as itself.
|
||||
@@ -386,7 +386,7 @@ async def verify_snippet(
|
||||
) -> dict:
|
||||
"""Record whether a snippet's recorded location and code still match source.
|
||||
|
||||
YOU do the checking — Scribe has no copy of the repo and deliberately never
|
||||
YOU do the checking — Grimoire has no copy of the repo and deliberately never
|
||||
gets one. This tool only remembers your verdict so it becomes queryable and
|
||||
so the operator can see what has rotted.
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ async def list_tasks(
|
||||
project_id: int = 0,
|
||||
kind: str = "",
|
||||
) -> dict:
|
||||
"""List tasks in Scribe.
|
||||
"""List tasks in Grimoire.
|
||||
|
||||
Args:
|
||||
status: Filter by status — one of: todo, in_progress, done, cancelled. Omit for all.
|
||||
@@ -160,7 +160,7 @@ async def get_task(
|
||||
log_limit: int = _WORK_LOG_ENTRIES,
|
||||
log_chars: int = _WORK_LOG_CHARS,
|
||||
) -> dict:
|
||||
"""Fetch a single Scribe task by ID.
|
||||
"""Fetch a single Grimoire task by ID.
|
||||
|
||||
Returns id, title, body, status, priority, tags, project_id, milestone_id,
|
||||
parent_id, parent_title, due_date, created_at, updated_at — plus `systems`
|
||||
@@ -260,7 +260,7 @@ async def create_task(
|
||||
arose_from_id: int = 0,
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
"""Create a new task in Scribe.
|
||||
"""Create a new task in Grimoire.
|
||||
|
||||
IS ANYTHING ACTUALLY OWED? A task carries a status and someone is on the
|
||||
hook to move it. If nothing is owed — you are recording what you learned,
|
||||
@@ -375,7 +375,7 @@ async def update_task(
|
||||
arose_from_id: int = 0,
|
||||
kind: str = "",
|
||||
) -> dict:
|
||||
"""Update an existing Scribe task. Only explicitly provided fields are changed.
|
||||
"""Update an existing Grimoire task. Only explicitly provided fields are changed.
|
||||
|
||||
Args:
|
||||
task_id: ID of the task to update.
|
||||
@@ -469,7 +469,7 @@ async def update_task(
|
||||
|
||||
|
||||
async def add_task_log(task_id: int, content: str) -> dict:
|
||||
"""Append a timestamped progress log entry to a Scribe task.
|
||||
"""Append a timestamped progress log entry to a Grimoire task.
|
||||
|
||||
Use this to record work sessions, decisions, or status updates over time
|
||||
without overwriting the task's main body. Each entry is stored separately
|
||||
@@ -650,7 +650,7 @@ async def start_planning(
|
||||
steps: list[dict] | None = None,
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
"""Begin a plan in Scribe (the preferred home for plans — not a local .md file).
|
||||
"""Begin a plan in Grimoire (the preferred home for plans — not a local .md file).
|
||||
|
||||
Reach for this when the work has an ARC — several steps toward one goal,
|
||||
worth tracking as a unit. Work without one (a fix, a one-file change, a
|
||||
@@ -732,7 +732,7 @@ async def start_planning(
|
||||
|
||||
|
||||
async def delete_task(task_id: int) -> dict:
|
||||
"""Move a Scribe task (or plan) to the trash (recoverable). Sub-tasks go with it.
|
||||
"""Move a Grimoire task (or plan) to the trash (recoverable). Sub-tasks go with it.
|
||||
Restore via restore(batch_id)."""
|
||||
uid = current_user_id()
|
||||
# Read the title BEFORE the delete: afterwards the row is trashed and the
|
||||
|
||||
@@ -6,7 +6,7 @@ from scribe.models.base import TimestampMixin, iso
|
||||
|
||||
|
||||
class RepoBinding(Base, TimestampMixin):
|
||||
"""Maps a git repository to the Scribe project it represents.
|
||||
"""Maps a git repository to the Grimoire project it represents.
|
||||
|
||||
The SessionStart hook sends the working repo's normalized remote
|
||||
(`repo_key`, e.g. ``git.fabledsword.com/bvandeusen/fabledscribe``) and the
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Restore a Scribe backup into an empty instance, from the command line.
|
||||
"""Restore a Grimoire backup into an empty instance, from the command line.
|
||||
|
||||
python -m scribe.restore /backups/scribe-20261015T020000Z.tar
|
||||
|
||||
This is how a fresh instance is brought back after losing the old one. Run it
|
||||
inside the Scribe container, which already has the database settings and has
|
||||
inside the Grimoire container, which already has the database settings and has
|
||||
already migrated the schema on start. It reads either the scheduled backup's
|
||||
tar (services/backup_file.py) or the JSON the admin download produces.
|
||||
|
||||
@@ -16,7 +16,7 @@ purpose and with its own warning.)
|
||||
Unlike the upload, nothing limits the file's size here, so this is also the
|
||||
way in for a backup too big for the web upload.
|
||||
|
||||
Afterwards, restart Scribe: search indexes are not part of a backup and are
|
||||
Afterwards, restart Grimoire: search indexes are not part of a backup and are
|
||||
rebuilt from the restored records at startup.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -60,7 +60,7 @@ async def run(path: Path) -> int:
|
||||
print(f"There is no file at {path}.", file=sys.stderr)
|
||||
return 2
|
||||
except (ArchiveError, ValueError) as exc:
|
||||
print(f"{path} isn't a backup this Scribe can read: {exc}", file=sys.stderr)
|
||||
print(f"{path} isn't a backup this Grimoire can read: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
accounts = await _account_count()
|
||||
@@ -68,14 +68,14 @@ async def run(path: Path) -> int:
|
||||
print(
|
||||
f"This database already holds {accounts} account(s). A restore adds a second "
|
||||
"copy of everything rather than replacing it, so it only runs into an empty "
|
||||
"instance. Start Scribe on an empty database and run this again.",
|
||||
"instance. Start Grimoire on an empty database and run this again.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 3
|
||||
|
||||
stats = await restore_full_backup(payload)
|
||||
print(json.dumps({"restored": stats}, indent=2, default=str))
|
||||
print("Restored. Restart Scribe so the search indexes are rebuilt.", file=sys.stderr)
|
||||
print("Restored. Restart Grimoire so the search indexes are rebuilt.", file=sys.stderr)
|
||||
return 0
|
||||
finally:
|
||||
await engine.dispose()
|
||||
@@ -84,7 +84,7 @@ async def run(path: Path) -> int:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python -m scribe.restore",
|
||||
description="Restore a Scribe backup (.tar or .json) into an empty instance.",
|
||||
description="Restore a Grimoire backup (.tar or .json) into an empty instance.",
|
||||
)
|
||||
parser.add_argument("file", type=Path, help="the backup file")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
@@ -73,7 +73,7 @@ async def restore():
|
||||
try:
|
||||
data = read_archive(io.BytesIO(await request.get_data()))
|
||||
except ArchiveError as exc:
|
||||
return jsonify({"error": f"That file isn't a Scribe backup: {exc}"}), 400
|
||||
return jsonify({"error": f"That file isn't a Grimoire backup: {exc}"}), 400
|
||||
if not data:
|
||||
return jsonify({"error": "No backup data provided"}), 400
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Scribe-plugin support endpoints.
|
||||
"""Grimoire-plugin support endpoints.
|
||||
|
||||
Consumed by the Scribe Claude Code plugin (not the web UI). `GET /api/plugin/
|
||||
Consumed by the Grimoire Claude Code plugin (not the web UI). `GET /api/plugin/
|
||||
context` is curled by the plugin's SessionStart hook to push the operator's
|
||||
always-on rules + active-project context into the session — Scribe's own push
|
||||
always-on rules + active-project context into the session — Grimoire's own push
|
||||
channel. Auth is the standard `@login_required` path, which accepts a `Bearer
|
||||
fmcp_<key>` API key (a read-scoped key suffices for this GET).
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ from scribe.services.settings import get_admin_setting, set_setting
|
||||
plugin_bp = Blueprint("plugin", __name__, url_prefix="/api/plugin")
|
||||
|
||||
# Setting key for the git URL of the marketplace that serves this plugin (the
|
||||
# Scribe app's own repo). Instance-global (stored on an admin account) so every
|
||||
# Grimoire app's own repo). Instance-global (stored on an admin account) so every
|
||||
# user's Settings page shows the real, copyable install command.
|
||||
_MARKETPLACE_KEY = "plugin_marketplace_url"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Rulebook / topic REST endpoints.
|
||||
|
||||
Wraps services/rulebooks.py. Standard Scribe auth: get_current_user_id() is the
|
||||
Wraps services/rulebooks.py. Standard Grimoire auth: get_current_user_id() is the
|
||||
authenticated owner; the service enforces ownership scoping.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -275,7 +275,7 @@ async def verify_snippet_route(snippet_id: int):
|
||||
"""Record a drift-check verdict. Body: {"status": ..., "detail", "path"}.
|
||||
|
||||
The CHECK itself runs wherever the code is — an agent with the working tree
|
||||
— because Scribe has no checkout and shouldn't have one. This endpoint just
|
||||
— because Grimoire has no checkout and shouldn't have one. This endpoint just
|
||||
stores what was found. It's here for parity with the MCP tool and so the UI
|
||||
can clear a stale marker after the operator fixes a record by hand."""
|
||||
uid = get_current_user_id()
|
||||
|
||||
@@ -4,10 +4,10 @@ Two kinds of destination, both instance-wide and set by an admin:
|
||||
|
||||
- **folder**: a directory inside the container, usually a mounted share. A
|
||||
backup service that watches the share (or a person) takes it from there.
|
||||
- **s3**: any S3-compatible bucket, addressed by path. Scribe only puts and
|
||||
- **s3**: any S3-compatible bucket, addressed by path. Grimoire only puts and
|
||||
lists, so an append-only key is enough.
|
||||
|
||||
Either way Scribe ADDS one dated file per run and never removes one: keeping
|
||||
Either way Grimoire ADDS one dated file per run and never removes one: keeping
|
||||
and thinning the history is the destination's job (services/backup_file.py
|
||||
says why the file is shaped the way it is for that).
|
||||
|
||||
@@ -201,7 +201,7 @@ async def deliver(
|
||||
backup_file.write_archive, payload, folder, at, app_version=app_version,
|
||||
)
|
||||
except PermissionError:
|
||||
raise DestinationError(f"Scribe isn't allowed to write in {folder}.") from None
|
||||
raise DestinationError(f"Grimoire isn't allowed to write in {folder}.") from None
|
||||
return Delivered(path.name, path.stat().st_size, str(folder))
|
||||
|
||||
target = s3_target(config, secret or "")
|
||||
@@ -240,18 +240,18 @@ async def check_destination(
|
||||
await asyncio.to_thread(probe.write_bytes, b"")
|
||||
await asyncio.to_thread(probe.unlink)
|
||||
except PermissionError:
|
||||
raise DestinationError(f"Scribe isn't allowed to write in {folder}.") from None
|
||||
raise DestinationError(f"Grimoire isn't allowed to write in {folder}.") from None
|
||||
except OSError as exc:
|
||||
raise DestinationError(f"Writing in {folder} failed: {exc.strerror or exc}.") from None
|
||||
count = await asyncio.to_thread(_count_backups, folder)
|
||||
return f"{folder} is writable and holds {count} Scribe backup(s)."
|
||||
return f"{folder} is writable and holds {count} Grimoire backup(s)."
|
||||
|
||||
target = s3_target(config, secret or "")
|
||||
try:
|
||||
objects = await list_objects(target, backup_file.NAME_PREFIX, transport=transport)
|
||||
except S3Error as exc:
|
||||
raise DestinationError(str(exc)) from None
|
||||
return f"Bucket {config.s3_bucket} answered and holds {len(objects)} Scribe backup(s)."
|
||||
return f"Bucket {config.s3_bucket} answered and holds {len(objects)} Grimoire backup(s)."
|
||||
|
||||
|
||||
def _count_backups(folder: Path) -> int:
|
||||
|
||||
@@ -184,11 +184,11 @@ def read_archive(fileobj: IO[bytes]) -> dict:
|
||||
with tar:
|
||||
manifest = json.loads(_read_member(tar, MANIFEST_NAME))
|
||||
if manifest.get("format") != ARCHIVE_FORMAT:
|
||||
raise ArchiveError("not a Scribe backup archive (no Scribe manifest)")
|
||||
raise ArchiveError("not a Grimoire backup archive (no Grimoire manifest)")
|
||||
if manifest.get("format_version", 0) > ARCHIVE_FORMAT_VERSION:
|
||||
raise ArchiveError(
|
||||
f"archive format {manifest['format_version']} is newer than "
|
||||
f"this Scribe reads ({ARCHIVE_FORMAT_VERSION}); upgrade first"
|
||||
f"this Grimoire reads ({ARCHIVE_FORMAT_VERSION}); upgrade first"
|
||||
)
|
||||
payload = dict(manifest.get("header") or {})
|
||||
for name, count in (manifest.get("sections") or {}).items():
|
||||
|
||||
@@ -4,7 +4,7 @@ path-style addressing (endpoint/bucket/key), and the two calls a backup needs.
|
||||
The signing is checked against AWS's published SigV4 examples in
|
||||
tests/test_services_backup_s3.py.
|
||||
|
||||
Scribe puts and lists; it never deletes. The bucket keeps the history and
|
||||
Grimoire puts and lists; it never deletes. The bucket keeps the history and
|
||||
thins it on its own policy, so a key that may only add objects is enough —
|
||||
and is the safer key to hand a service, because a stolen one cannot wipe the
|
||||
history it writes into.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Lightweight diagnostic instrumentation for crash investigation.
|
||||
|
||||
The Scribe app + its Postgres have been crashing recurrently with no
|
||||
The Grimoire app + its Postgres have been crashing recurrently with no
|
||||
clear cause in the logs. This module adds three things designed to make
|
||||
the crash class identifiable from logs alone:
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ _EMAIL_LOGO_SVG = (
|
||||
|
||||
|
||||
def _email_html(title: str, body: str) -> str:
|
||||
"""Wrap email body content in the standard Fabled Scribe template."""
|
||||
"""Wrap email body content in the standard Fabled Grimoire template."""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -56,7 +56,7 @@ def _email_html(title: str, body: str) -> str:
|
||||
<!-- Header -->
|
||||
<div style="background:linear-gradient(135deg,#7c3aed 0%,#6d28d9 100%);padding:24px 28px;text-align:center;">
|
||||
<div style="margin-bottom:8px;">
|
||||
{_EMAIL_LOGO_SVG}<span style="display:inline-block;vertical-align:middle;color:#ffffff;font-size:18px;font-weight:700;letter-spacing:0.01em;">Fabled Scribe</span>
|
||||
{_EMAIL_LOGO_SVG}<span style="display:inline-block;vertical-align:middle;color:#ffffff;font-size:18px;font-weight:700;letter-spacing:0.01em;">Fabled Grimoire</span>
|
||||
</div>
|
||||
<p style="margin:0;color:#ede9fe;font-size:13px;letter-spacing:0.03em;">{title}</p>
|
||||
</div>
|
||||
@@ -68,7 +68,7 @@ def _email_html(title: str, body: str) -> str:
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="border-top:1px solid #ede9fe;padding:16px 28px;text-align:center;background:#faf5ff;">
|
||||
<p style="margin:0;color:#a78bfa;font-size:12px;">Sent by your Fabled Scribe instance.</p>
|
||||
<p style="margin:0;color:#a78bfa;font-size:12px;">Sent by your Fabled Grimoire instance.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -146,7 +146,7 @@ async def send_email(to: str, subject: str, html_body: str) -> None:
|
||||
username = config.get("smtp_username", "")
|
||||
password = config.get("smtp_password", "")
|
||||
from_address = config.get("smtp_from_address", "")
|
||||
from_name = config.get("smtp_from_name", "Fabled Scribe")
|
||||
from_name = config.get("smtp_from_name", "Fabled Grimoire")
|
||||
use_tls = config.get("smtp_use_tls", "true") == "true"
|
||||
|
||||
msg = EmailMessage()
|
||||
@@ -176,6 +176,6 @@ async def send_test_email(to: str) -> None:
|
||||
"""Send a branded test email."""
|
||||
body = """
|
||||
<p style="margin:0 0 12px;color:#1e1b4b;font-size:15px;font-weight:600;">SMTP is configured correctly</p>
|
||||
<p style="margin:0;color:#6b7280;font-size:14px;">Your Fabled Scribe instance can send email notifications.</p>
|
||||
<p style="margin:0;color:#6b7280;font-size:14px;">Your Fabled Grimoire instance can send email notifications.</p>
|
||||
"""
|
||||
await send_email(to, "Fabled Scribe - Test Email", _email_html("Test Email", body))
|
||||
await send_email(to, "Fabled Grimoire - Test Email", _email_html("Test Email", body))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Knowledge service — one query across every record kind Scribe holds.
|
||||
"""Knowledge service — one query across every record kind Grimoire holds.
|
||||
|
||||
ACL (services/access.py): these queries honour shares, so a record shared with
|
||||
you can be *found* as well as opened by id — at two different widths:
|
||||
@@ -77,7 +77,7 @@ def location_matches(data: dict | None, parts: dict[str, str]) -> bool:
|
||||
if all(
|
||||
_path_matches((loc.get(key) or "").strip(), want)
|
||||
if key == "path"
|
||||
# Repo names are recorded free-form ("Scribe" / "FabledScribe" /
|
||||
# Repo names are recorded free-form ("Grimoire" / "FabledScribe" /
|
||||
# "fabledscribe") — case is never the distinguishing thing.
|
||||
else (loc.get(key) or "").strip().lower() == want.lower()
|
||||
if key == "repo"
|
||||
|
||||
@@ -23,7 +23,7 @@ install called its server is not something a mapping should depend on.
|
||||
WHY THE DEFAULTS ARE CODE AND THE CORRECTIONS ARE ROWS
|
||||
|
||||
The defaults cover the actions every install shares — the harness's own tools,
|
||||
Scribe's own tools, the commonest command shapes. They ship, and improve, with
|
||||
Grimoire's own tools, the commonest command shapes. They ship, and improve, with
|
||||
the product. What no default can know is how one operator works, so an install
|
||||
ADDS mappings and REMOVES defaults that misfire for it, in-session, through
|
||||
`map_action` / `unmap_action`. A removal is stored rather than applied to the
|
||||
@@ -124,7 +124,7 @@ def _defaults() -> tuple[Action, ...]:
|
||||
for moment in reached:
|
||||
on(moment, "Skill", f"{SKILL_FIELD}={BUNDLED_SKILL_PLUGIN}:{skill}")
|
||||
|
||||
# Scribe's own tools — every install has these, so their moments ship.
|
||||
# Grimoire's own tools — every install has these, so their moments ship.
|
||||
on("work.start", "update_task", "status=in_progress")
|
||||
on("work.finish", "update_task", "status=done", "status=cancelled")
|
||||
on("work.finish", "update_milestone", "status=done")
|
||||
|
||||
@@ -5,7 +5,7 @@ differ only in how they carry the answer:
|
||||
|
||||
- the plugin's catch-all PreToolUse hook → `/api/plugin/moment`, with the
|
||||
session ledger, so a rule already named this session is cited not repeated;
|
||||
- Scribe's own MCP tools → `attach_moment_rules`, in the tool's response, so
|
||||
- Grimoire's own MCP tools → `attach_moment_rules`, in the tool's response, so
|
||||
a client without the plugin still gets `work.finish` when a task closes;
|
||||
- the Stop hook → `deliver_moments`, for the reply moments no tool call marks.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ moment is generic; the action that reaches it is local.
|
||||
|
||||
HOW TO WRITE ONE
|
||||
|
||||
Domain-neutral: Scribe's work may be software, configuration,
|
||||
Domain-neutral: Grimoire's work may be software, configuration,
|
||||
infrastructure or anything else a person drives through an agent, so a
|
||||
definition names the moment in words every one of those recognises. The test
|
||||
beside this module refuses software-only vocabulary in both fields. Concrete
|
||||
|
||||
@@ -88,7 +88,7 @@ async def notify_security_event(
|
||||
</table>
|
||||
<p style="margin:16px 0 0;color:#9ca3af;font-size:12px;">If this wasn't you, change your password immediately.</p>
|
||||
"""
|
||||
await send_email(email, f"Fabled Scribe - {label}", _email_html("Security Alert", body))
|
||||
await send_email(email, f"Fabled Grimoire - {label}", _email_html("Security Alert", body))
|
||||
except Exception:
|
||||
logger.exception("Failed to send security notification for user %d", user_id)
|
||||
|
||||
@@ -110,7 +110,7 @@ async def send_password_reset_email(email: str, reset_url: str) -> None:
|
||||
<p style="margin:0;color:#9ca3af;font-size:12px;">If the button doesn't work, copy and paste this link:</p>
|
||||
<p style="margin:4px 0 0;color:#9ca3af;font-size:12px;word-break:break-all;">{reset_url}</p>
|
||||
"""
|
||||
await send_email(email, "Fabled Scribe - Password Reset", _email_html("Password Reset", body))
|
||||
await send_email(email, "Fabled Grimoire - Password Reset", _email_html("Password Reset", body))
|
||||
|
||||
|
||||
async def send_password_reset_success_email(email: str) -> None:
|
||||
@@ -122,14 +122,14 @@ async def send_password_reset_success_email(email: str) -> None:
|
||||
</p>
|
||||
<p style="margin:0;color:#6b7280;font-size:13px;">If you didn't make this change, please contact your administrator immediately.</p>
|
||||
"""
|
||||
await send_email(email, "Fabled Scribe - Password Changed", _email_html("Password Changed", body))
|
||||
await send_email(email, "Fabled Grimoire - Password Changed", _email_html("Password Changed", body))
|
||||
|
||||
|
||||
async def send_invitation_email(email: str, invite_url: str, invited_by_username: str) -> None:
|
||||
"""Send a branded invitation email with a registration link."""
|
||||
body = f"""
|
||||
<p style="margin:0 0 16px;color:#374151;font-size:15px;">
|
||||
<strong>{invited_by_username}</strong> has invited you to join Fabled Scribe.
|
||||
<strong>{invited_by_username}</strong> has invited you to join Fabled Grimoire.
|
||||
Click the button below to create your account.
|
||||
</p>
|
||||
<div style="text-align:center;margin:24px 0;">
|
||||
@@ -143,7 +143,7 @@ async def send_invitation_email(email: str, invite_url: str, invited_by_username
|
||||
<p style="margin:0;color:#9ca3af;font-size:12px;">If the button doesn't work, copy and paste this link:</p>
|
||||
<p style="margin:4px 0 0;color:#9ca3af;font-size:12px;word-break:break-all;">{invite_url}</p>
|
||||
"""
|
||||
await send_email(email, "Fabled Scribe - You're Invited!", _email_html("You're Invited!", body))
|
||||
await send_email(email, "Fabled Grimoire - You're Invited!", _email_html("You're Invited!", body))
|
||||
|
||||
|
||||
def utc_day_start(day: date) -> datetime:
|
||||
@@ -239,7 +239,7 @@ async def check_due_tasks() -> None:
|
||||
{task_rows}
|
||||
</table>
|
||||
"""
|
||||
await send_email(email, f"Fabled Scribe - {len(user_tasks)} Task(s) Due", _email_html("Task Reminders", body))
|
||||
await send_email(email, f"Fabled Grimoire - {len(user_tasks)} Task(s) Due", _email_html("Task Reminders", body))
|
||||
await log_audit(
|
||||
"task_reminder",
|
||||
user_id=user_id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Session-context rendering for the Scribe plugin's SessionStart hook.
|
||||
"""Session-context rendering for the Grimoire plugin's SessionStart hook.
|
||||
|
||||
The plugin's hook curls `GET /api/plugin/context` at session start and injects
|
||||
the returned text as `additionalContext`, giving Scribe a push channel into
|
||||
the returned text as `additionalContext`, giving Grimoire a push channel into
|
||||
the session. This module renders that text.
|
||||
|
||||
Design note — altitude: we inject rule *titles* grouped by topic (a compact
|
||||
@@ -166,7 +166,7 @@ WRITEPATH_DEFAULT_THRESHOLD = SURFACES["write_path"].floor_default
|
||||
# install writes its rules — so the default errs deliberately toward SILENCE
|
||||
# rather than toward recall, on an asymmetry that is itself structural: this
|
||||
# hint fires on EVERY write. A missed rule is recoverable, because the rule is
|
||||
# still in Scribe and the agent can search it. A hint that cries wolf is not:
|
||||
# still in Grimoire and the agent can search it. A hint that cries wolf is not:
|
||||
# it teaches the reader to skip the whole block, and the surface is lost along
|
||||
# with the true positives it would have carried.
|
||||
#
|
||||
@@ -515,7 +515,7 @@ async def build_process_manifest(user_id: int) -> dict:
|
||||
(note_type='process'). Instance-agnostic: derived from whatever Processes the
|
||||
calling install owns, no operator-specific coupling.
|
||||
|
||||
SCOPE: this is the most consequential passive surface Scribe has — every
|
||||
SCOPE: this is the most consequential passive surface Grimoire has — every
|
||||
entry becomes a skill file on the operator's machine that auto-surfaces and
|
||||
is followed as written. It therefore uses the BROWSE scope (via the
|
||||
no-query knowledge list): a Process shared directly with the operator is
|
||||
@@ -548,7 +548,7 @@ async def build_process_manifest(user_id: int) -> dict:
|
||||
if it.get("shared"):
|
||||
owner = it.get("owner") or "another user"
|
||||
description = (
|
||||
f'A shared Scribe process "{title}", authored by {owner} — NOT the'
|
||||
f'A shared Grimoire process "{title}", authored by {owner} — NOT the'
|
||||
f" operator's own."
|
||||
+ (f" {preview}" if preview else "")
|
||||
+ f' Use only when the operator asks to run the "{title}" process'
|
||||
@@ -559,7 +559,7 @@ async def build_process_manifest(user_id: int) -> dict:
|
||||
)
|
||||
else:
|
||||
description = (
|
||||
f'Run the operator\'s saved Scribe process "{title}".'
|
||||
f'Run the operator\'s saved Grimoire process "{title}".'
|
||||
+ (f" {preview}" if preview else "")
|
||||
+ f' Use when the operator asks to run the "{title}" process by'
|
||||
f" name. If a request merely RESEMBLES this process, the live"
|
||||
@@ -582,7 +582,7 @@ async def build_process_manifest(user_id: int) -> dict:
|
||||
entry["owner"] = it.get("owner")
|
||||
procs.append(entry)
|
||||
|
||||
# The most consequential passive surface Scribe has (see SCOPE above): a
|
||||
# The most consequential passive surface Grimoire has (see SCOPE above): a
|
||||
# Process installed as a skill and never once opened must be
|
||||
# distinguishable from one never installed. The honest event is "installed on the operator's
|
||||
# machine", which is a surfacing in effect: the skill description is in
|
||||
@@ -738,7 +738,7 @@ async def build_autoinject_hint(
|
||||
# to explain itself.
|
||||
if kept:
|
||||
lines.append(
|
||||
"> Possibly relevant from your Scribe records — open any in full with "
|
||||
"> Possibly relevant from your Grimoire records — open any in full with "
|
||||
"`get_note(id)`, or `get_snippet` / `get_process` / `get_lesson` for "
|
||||
"those kinds. Each line is a record's name, its kind and System, and "
|
||||
"the passage that matched; a line marked `seen` is a pointer to one "
|
||||
@@ -1269,7 +1269,7 @@ async def build_write_path_hint(
|
||||
Note the repo↔project mapping is deliberately one-way: the hook sends a git
|
||||
remote, which the ROUTE resolves to `project_id` through the repo bindings.
|
||||
It is never used as the location `repo` filter — a snippet's `repo` is a
|
||||
free-text label the operator typed ("Scribe"), not a remote URL, and matching
|
||||
free-text label the operator typed ("Grimoire"), not a remote URL, and matching
|
||||
one against the other would silently return nothing.
|
||||
|
||||
Returns {"context": str, "note_ids": list[int], "sync_note_ids": list[int],
|
||||
@@ -1559,7 +1559,7 @@ async def build_write_path_hint(
|
||||
# title being right there, but the load-bearing sentence is the one no
|
||||
# other surface says: keeping the record true is part of THIS edit.
|
||||
lines.append(
|
||||
f"> Recorded in Scribe AT `{path}` — the snippet(s) below record "
|
||||
f"> Recorded in Grimoire AT `{path}` — the snippet(s) below record "
|
||||
"the file this edit is changing. Reuse/extend the recorded shape "
|
||||
"rather than writing a parallel one; and if this edit changes what "
|
||||
"a record captures, updating it is part of the edit: "
|
||||
@@ -1573,7 +1573,7 @@ async def build_write_path_hint(
|
||||
|
||||
if menu:
|
||||
lines.append(
|
||||
f"> Prior art already recorded in Scribe for `{path}` — open one with "
|
||||
f"> Prior art already recorded in Grimoire for `{path}` — open one with "
|
||||
"`get_snippet(id)` for a snippet, `get_task(id)` for an issue, "
|
||||
"`get_lesson(id)` for a lesson, `get_note(id)` otherwise. Reuse a "
|
||||
"snippet rather than writing a fresh one-off; read an issue before "
|
||||
@@ -2073,10 +2073,10 @@ async def build_session_context(
|
||||
LIVE STATE ONLY. It says only what the server alone knows about THIS
|
||||
session: the
|
||||
active project, its open work, its design system, or that the working repo
|
||||
is unbound. How to work with Scribe is the skill's to say, and it says it
|
||||
is unbound. How to work with Grimoire is the skill's to say, and it says it
|
||||
once.
|
||||
"""
|
||||
lines: list[str] = ["# Scribe — live session state"]
|
||||
lines: list[str] = ["# Grimoire — live session state"]
|
||||
|
||||
project_dict: dict | None = None
|
||||
if project_id:
|
||||
@@ -2143,7 +2143,7 @@ async def build_session_context(
|
||||
"",
|
||||
f"## Project {project_id} could not be loaded",
|
||||
f"This session asked for project {project_id}, but this account "
|
||||
"cannot read it — the id may belong to a different Scribe "
|
||||
"cannot read it — the id may belong to a different Grimoire "
|
||||
"instance, or the project may have been deleted. "
|
||||
"`list_projects` shows what is readable here.",
|
||||
]
|
||||
@@ -2151,14 +2151,14 @@ async def build_session_context(
|
||||
lines += [
|
||||
"",
|
||||
"## Repository not yet bound",
|
||||
f"This repo (`{unbound_repo}`) isn't mapped to a Scribe project, so "
|
||||
f"This repo (`{unbound_repo}`) isn't mapped to a Grimoire project, so "
|
||||
"no project context was loaded. Bind it once with "
|
||||
f'`bind_repo(repo_url="{unbound_repo}", project_id=<id>)` '
|
||||
"(call `list_projects` to find the id) and future sessions here will "
|
||||
"auto-load that project's context.",
|
||||
]
|
||||
else:
|
||||
lines += ["", "No Scribe project is bound to this working directory."]
|
||||
lines += ["", "No Grimoire project is bound to this working directory."]
|
||||
|
||||
context = "\n".join(line for line in lines if line is not None)
|
||||
if len(context) > _MAX_CHARS:
|
||||
|
||||
@@ -25,7 +25,7 @@ HOW A GUESS IS RECOGNISED
|
||||
A guess is always just ABOVE the highest id that exists — that is the only
|
||||
place "the next id" can be. So a `#N` is refused when it sits in
|
||||
(max_id, max_id + GUESS_WINDOW]. Numbers below the max are real records (or
|
||||
were), and numbers far above it are not Scribe ids at all — a PR, a forge
|
||||
were), and numbers far above it are not Grimoire ids at all — a PR, a forge
|
||||
issue — so both pass. What this cannot catch, by construction: a guess that
|
||||
another session has ALREADY used. That reference points at a real record, and
|
||||
nothing in the text distinguishes it from a deliberate one. The placeholders
|
||||
@@ -107,7 +107,7 @@ async def refuse_guessed_ids(*texts: str | None) -> None:
|
||||
"records first and cite the ids they come back with, or create them "
|
||||
"together with create_records / start_planning(steps=...) and write "
|
||||
"{{ref:N}} where the Nth record's id belongs. If this number is not a "
|
||||
"Scribe record (a PR, an issue elsewhere), write it without the '#'."
|
||||
"Grimoire record (a PR, an issue elsewhere), write it without the '#'."
|
||||
)
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ _NAMED_RE = re.compile(
|
||||
)
|
||||
|
||||
# Words after which `#N` belongs to a different numbering: a forge's (a PR, a
|
||||
# CI run, a job) or one of Scribe's own id spaces that is not a note's. Rules,
|
||||
# CI run, a job) or one of Grimoire's own id spaces that is not a note's. Rules,
|
||||
# milestones, Systems, projects and work-logs each draw from their own
|
||||
# sequence, so "rule #153" is not note 153 — and note 153 is somebody's record.
|
||||
_OTHER_SPACE = frozenset({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user