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:
@@ -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({
|
||||
|
||||
@@ -143,7 +143,7 @@ async def is_bound(project_id: int, raw_repo: str) -> str:
|
||||
async def keys_for_project(user_id: int, project_id: int) -> list[str]:
|
||||
"""Every repo key bound to a project — the snippet→forge join.
|
||||
|
||||
Recorded snippet locations carry free-form repo names ("Scribe"), which
|
||||
Recorded snippet locations carry free-form repo names ("Grimoire"), which
|
||||
can't address a forge API. The project's binding is the identity that can:
|
||||
a snippet reaches its forge repo through the project it belongs to.
|
||||
"""
|
||||
@@ -160,7 +160,7 @@ async def keys_for_project(user_id: int, project_id: int) -> list[str]:
|
||||
async def bindings_for_key(raw_repo: str) -> list[RepoBinding]:
|
||||
"""All bindings (ANY user) for a repo key — the webhook's entry point.
|
||||
|
||||
A push webhook carries no Scribe caller, only the repository it happened
|
||||
A push webhook carries no Grimoire caller, only the repository it happened
|
||||
to; the flag it writes is about each record's truth, so every user who
|
||||
bound the repo gets their project's snippets considered — each write still
|
||||
lands as that record's owner.
|
||||
|
||||
@@ -69,7 +69,7 @@ def block_reason(missing: list[str]) -> str:
|
||||
listed = ", ".join(known_sections(missing)) or "the completion sections"
|
||||
shape = reply_shapes.SHAPES["completion"]
|
||||
return (
|
||||
f"This turn closed a Scribe task, and the reply that ends it is missing: {listed}. "
|
||||
f"This turn closed a Grimoire task, and the reply that ends it is missing: {listed}. "
|
||||
"The person the work is for reads this reply to find out where it stands. Rewrite it "
|
||||
f"as a completion report — {shape.reminder} Take where it sits from `placement`, the "
|
||||
"task or plan by id and title (`list_reply_shapes` has the shape in full)."
|
||||
|
||||
@@ -1192,7 +1192,7 @@ async def _reserve_note_slot(
|
||||
"""Guarantee `slot.kinds` one line, if one clears the arm's own bar.
|
||||
|
||||
WHY A SLOT AT ALL. Ranking by raw cosine is blind to what KIND of record
|
||||
answers what kind of ask, and the corpus makes that fatal: Scribe's
|
||||
answers what kind of ask, and the corpus makes that fatal: Grimoire's
|
||||
project records are about software work, so a task about building a
|
||||
helper outranks the snippet that IS one. Snippets can be ~0.5% of a
|
||||
corpus, and no floor fixes 200:1. A
|
||||
|
||||
@@ -25,7 +25,7 @@ WHY THE DEFAULTS BELOW ARE STARTING POINTS AND NOT ANSWERS
|
||||
A cosine score is a distance in BAAI/bge-small-en-v1.5's vector space, measured
|
||||
against one corpus. It cannot transfer to an install with different records,
|
||||
and a shipped default cannot be defended from any one install's telemetry.
|
||||
Scribe ships a starting point and the means to correct it, so the values carry
|
||||
Grimoire ships a starting point and the means to correct it, so the values carry
|
||||
the measurement that motivated them without claiming to be right for anybody
|
||||
else.
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ _reported = False
|
||||
# redaction nobody made; eating a query costs the ability to tune the bar.
|
||||
_SECRET_PATTERNS: tuple[tuple[str, "re.Pattern[str]"], ...] = (
|
||||
# Vendor-prefixed credentials. The prefix IS the tell, so no entropy
|
||||
# guessing is needed — `fmcp_` is Scribe's own API key format.
|
||||
# guessing is needed — `fmcp_` is Grimoire's own API key format.
|
||||
("token", re.compile(
|
||||
r"\b(?:fmcp_|flt_|ghp_|gho_|ghs_|ghu_|github_pat_|glpat-|gitlab-ci-token:"
|
||||
r"|xox[abprs]-|sk-[A-Za-z0-9]*-?|AKIA|ASIA)[A-Za-z0-9_\-]{12,}"
|
||||
|
||||
@@ -856,7 +856,7 @@ async def get_rule_version(rule_id: int, version_id: int, user_id: int):
|
||||
)).scalar_one_or_none()
|
||||
|
||||
|
||||
# ── Drift: what Scribe changed about how it works with you ─────────────
|
||||
# ── Drift: what Grimoire changed about how it works with you ─────────────
|
||||
#
|
||||
# A preference is the one record kind the AGENT rewrites in the ordinary
|
||||
# course of working, which is the property that makes it useful and the
|
||||
@@ -908,7 +908,7 @@ async def recent_preference_drift(user_id: int, limit: int = 20) -> list[dict]:
|
||||
"""Preferences that have been rewritten, most recently changed first.
|
||||
|
||||
ONE ROW PER PREFERENCE, carrying its LATEST rewrite — not every version of
|
||||
every preference. The question this answers is "what has Scribe changed
|
||||
every preference. The question this answers is "what has Grimoire changed
|
||||
about how it works with me lately", and a preference rewritten eight times
|
||||
this week is one answer to that, not eight. The full history of any one
|
||||
preference is still a click away in its editor, which is where "how did
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Sealing a credential Scribe stores so the database alone cannot read it.
|
||||
"""Sealing a credential Grimoire stores so the database alone cannot read it.
|
||||
|
||||
Fernet (AES-128-CBC with an HMAC) under a key derived from the instance's
|
||||
SECRET_KEY. A copy of the database, or of a backup made from it, then holds
|
||||
|
||||
@@ -378,7 +378,7 @@ _DATA_FIELDS = (
|
||||
# wrong" — which is worse than having no record, because it is surfaced with the
|
||||
# same authority either way.
|
||||
#
|
||||
# WHERE THE CHECK RUNS. Not here. Scribe has no checkout of the operator's repos
|
||||
# WHERE THE CHECK RUNS. Not here. Grimoire has no checkout of the operator's repos
|
||||
# and must not acquire one (the instance stays agnostic about where code
|
||||
# lives; giving the server repo access would make every install a credential
|
||||
# problem). The agent already has the working tree, so IT does the
|
||||
@@ -989,7 +989,7 @@ async def record_verification(
|
||||
):
|
||||
"""Record the result of a drift check against the snippet's source.
|
||||
|
||||
The CHECK happens agent-side — Scribe has no checkout and shouldn't want one
|
||||
The CHECK happens agent-side — Grimoire has no checkout and shouldn't want one
|
||||
(see the drift-check note above). This just remembers the verdict, stamped
|
||||
with a hash of the code it was checked against so it expires by itself when
|
||||
the snippet is edited.
|
||||
@@ -1173,7 +1173,7 @@ async def attach_live_body(note, data: dict) -> None:
|
||||
data["body_source"] = "cache"
|
||||
data["body_freshness"] = "no-recorded-location"
|
||||
return
|
||||
# Recorded location repos are free-form names ("Scribe"), which can't
|
||||
# Recorded location repos are free-form names ("Grimoire"), which can't
|
||||
# address a forge API — the project's repo BINDING is the identity that
|
||||
# can. Try the location string first (it may be a real remote),
|
||||
# then fall back to the bindings of the snippet's project.
|
||||
|
||||
@@ -209,7 +209,7 @@ def render_claims(
|
||||
lines += [
|
||||
"",
|
||||
"## In flight — the work this session had claimed",
|
||||
"Scribe's record of what you were doing before the context was "
|
||||
"Grimoire's record of what you were doing before the context was "
|
||||
"lost. Carry on from here; the full log is `get_task(id)`.",
|
||||
]
|
||||
for c in ours[:_OWN_CAP]:
|
||||
|
||||
Reference in New Issue
Block a user