Dashboard:
- 'Done recently' chip-cloud -> compact uniform list (Active-now row style),
showing 5 with inline expand to the rest (backend already returns up to 8).
- New 'Projects' rail card: each active project with 'N open · M done'.
Backend already computed done_count (dashboard.py) — now surfaced in the
/api/dashboard payload per active project.
MCP Access (Connect Claude / Claude Code):
- Progressive disclosure: lead with the pre-filled plugin-install snippet;
fold server name, scope, marketplace URL, and the MCP-only path into a
single 'Customize' expander. Desktop tab keeps its own server-name field.
- Marketplace URL now defaults to this instance's own repo via
config.PLUGIN_MARKETPLACE_URL (env-overridable); /api/plugin/marketplace-url
falls back to it, so the field + install snippet are pre-filled out of the
box instead of showing a generic placeholder.
Refs #761
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SKILL.md gained the 'Where a new rule goes' section (rule-scope model) in
50b6902 but plugin.json was not bumped, so autoUpdate clients stay on 0.1.3
and never reinstall the new skill content. Bump to propagate.
(MCP tool descriptions are unaffected by this — they are served live by the
remote app and refresh on the next session's MCP handshake, not via the
plugin bundle.)
Refs #755
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the always-on / subscribed / project-rule distinction explicit at the
authoring surface so it can't silently regress (for this operator or other
users). Previously the tools said only 'cross-project rulebook rule' and a
bare 'subscribe a project' — nothing steered project-specific detail away
from shared rulebooks, which is how a Scribe-pinned rule ends up binding
every family project.
Principle encoded in 5 places: a rule's home is chosen by WHO it should bind,
and both rulebook tiers are SHARED so their rules stay general — they differ
in reach (all projects vs opt-in by theme), not generality. Project-specific
detail goes in create_project_rule.
- server.py MCP instructions: add the 3-tier authoring principle
- create_rule / create_rulebook / create_project_rule / subscribe_* docstrings
- using-scribe SKILL.md: a 'Where a new rule goes' note for the pull path
Refs #755
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SessionStart push channel cannot reliably deliver a sensitive API
token to the hook subprocess (upstream Claude Code bug anthropics/
claude-code#62442 — sensitive plugin userConfig is not persisted and is
absent on a normal session). Stop depending on that push for standing
rules: make the using-scribe bootstrap skill own the load instead.
- description: name the FIRST ACTION (list_always_on_rules + enter_project
when a repo/project is in scope) so it auto-surfaces at session start
- add a 'Do this first' block instructing an active pull; demote the
SessionStart hook to a bonus, not a precondition (it fail-opens and may
be absent)
- reflex step 2: rules come from list_always_on_rules(), not from an
assumed SessionStart injection
The hook + hooks.json are left in place: they fail-open and resume adding
value automatically if #62442 is fixed or the token is made non-sensitive.
Refs #755 (Phase 1: push channel descoped to optional; pull is load-bearing)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Superseded by the plugin's own SessionStart hook (plugin/hooks/). This root
scripts/ copy read the now-deleted project .mcp.json (dead scribe-dev /
devassistant host), so it could never fire. Single Scribe environment now,
reached only via the Scribe plugin MCP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SessionStart hook asked for a project_id via plugin userConfig, which pins
one install to a single project — wrong for an operator working across many
repos/projects. Resolve the active project server-side from the working repo's
git remote instead (a stable identifier, not a dir-name guess).
- repo_bindings table (migration 0064) + RepoBinding model: (user, repo_key) ->
project, FKs CASCADE.
- services/repo_bindings: normalize_repo_key collapses ssh/https/scp/creds/port/
.git to host/owner/repo; resolve/set/list/delete.
- GET /api/plugin/context takes ?repo=<remote>; unbound repo -> a "bind this
repo" hint with a ready bind_repo() call. project_id kept as manual override.
- MCP tools: bind_repo / list_repo_bindings / unbind_repo.
- Hook sends ?repo=$(git remote get-url origin) URL-encoded; all project_id
handling removed. plugin.json drops the project_id userConfig (0.1.2 -> 0.1.3).
- Tests: normalize equivalence classes + unbound-hint rendering.
Refs task 755 (Scribe-as-plugin push channel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SessionStart push-channel hook passed the key via
SCRIBE_TOKEN="${user_config.api_token}" in hooks.json, but api_token is
sensitive:true. Claude Code keeps sensitive userConfig in the keychain and
does not interpolate it into hook command strings (only into mcpServers
headers), so the hook received the literal placeholder, sent it as the Bearer
token, and the context endpoint 401'd -> fail-open -> no context injected.
Read the harness-exported CLAUDE_PLUGIN_OPTION_<key> env vars instead (SCRIBE_*
still override for the settings.json dogfooding path), and treat any unexpanded
${...} literal as unset so the hook fails open cleanly instead of 401-ing.
Bump 0.1.1 -> 0.1.2 so installs refresh the cache.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/reload-plugins reported '0 plugin MCP servers'. Root cause: plugin.json had
"mcpServers": "./.mcp.json" — a string path, which is neither a valid inline
object nor a recognized reference (per docs, plugin MCP servers are a root
.mcp.json OR an inline object in plugin.json), so it parsed to zero servers.
Inline the mcpServers object directly in plugin.json and remove the separate
.mcp.json. The user_config substitution syntax was already correct
(plugins-reference: values substitute as ${user_config.KEY} in MCP configs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Settings install command had a <your-scribe-repo> placeholder — not
copyable. Add an instance-global 'plugin_marketplace_url' setting (admin sets
it to the app's own repo) that every user's MCP Access reads, so the
/plugin marketplace add command is copyable out of the box. Keeps it universal
(each deployment configures its own repo) rather than hardcoding one.
- services/settings.get_admin_setting(key): admin-scoped global read.
- routes/plugin: GET /api/plugin/marketplace-url (any user) + PUT (admin).
- SettingsView: Admin → 'Plugin marketplace' field to set it; MCP Access
marketplace field falls back to the configured value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per operator: the plugin install should supersede the bare MCP connection in
Settings, since the plugin incorporates the MCP and adds the session-start hook
+ skills. The Claude Code tab now leads with /plugin marketplace add + install
(with a persisted marketplace-URL field and the base-URL/key/project-id prompts
spelled out), and the old 'claude mcp add' command moves into a collapsed
'Advanced: connect the MCP only' disclosure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v2 backup silently dropped the entire rulebook system (rulebooks, topics,
rules), the project subscription/suppression join tables, and events — so a
'full' backup wasn't. v3 adds all of them with FK re-mapping on restore, and a
_not_included field that names the still-deferred tables (ACL groups/shares,
api_keys, embeddings, transient/operational) so the gap is explicit, not silent.
restore_full_backup routes v2 and v3 through one path; v3-only sections are
guarded by data.get so a v2 payload still restores cleanly.
Tests: version/coverage constants, pure join-table row helpers, and the export
contract via a mocked session (CI has no DB; full round-trip is a manual check).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per operator: the plugin lives in the app repo so it ships and versions in
lockstep with the app and the /api/plugin/context contract it targets (same
co-location rationale as the former in-repo MCP). A git-cloned marketplace
supports relative plugin sources, so the FabledScribe repo IS the marketplace.
- .claude-plugin/marketplace.json — source ./plugin
- plugin/.claude-plugin/plugin.json — userConfig (base URL, api key, project id)
- plugin/.mcp.json — http scribe server, ${user_config.*} substitution
- plugin/hooks/ — SessionStart push-channel hook (fail-open)
- plugin/skills/using-scribe — bootstrap skill
- plugin/README.md — install via the FabledScribe repo marketplace
Phase 2 of plan #755. Install/userConfig-substitution test pending.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 of plan #755 (Scribe-as-plugin). Gives Scribe its own session-start
push channel so always-on rules + active-project context surface without being
asked — the gap behind 'I have to prompt for everything'.
- services/plugin_context.build_session_context: renders always-on rule titles
grouped by topic (under the 10k additionalContext cap; full text stays one
list_always_on_rules/get_rule call away) + optional project goal/open-task
count + a recall/update-over-create reflex line. Capped at 9000 chars.
- routes/plugin GET /api/plugin/context (login_required already accepts Bearer
fmcp_ keys; read scope suffices).
- tests: titles-not-statements, project scoping, length cap (pure mocks).
- scripts/scribe_session_context.sh: dogfood SessionStart hook, fail-open,
reads url+token from .mcp.json. Superseded in Phase 2 by the plugin-bundled
hook using userConfig.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old standalone fable-mcp wheel/download flow is gone from code (no route,
no Dockerfile build, no FABLE_MCP_DIST_DIR). Update api-keys-and-mcp,
api-reference, architecture, configuration, development to describe the
in-app HTTP MCP at /mcp (Bearer auth). Untrack the 18 committed
docs/superpowers/ files so the existing .gitignore takes effect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP surface advertised writing well but recall poorly, and project
scoping had no anchor that survived past enter_project's snapshot:
- search / list_notes dropped the project_id their services already
support, so a scoped search was impossible — every query swept all
projects and bled unrelated work into the session.
- The tool descriptions were mechanical ("Semantic search over the
user's notes and tasks") with no trigger telling Claude WHEN to reach
for them; the server instructions were all write-discipline and said
nothing about searching before answering or starting work.
Changes:
- search, list_notes: add project_id param, wired to the service.
- search, list_notes, list_tasks: trigger-worded descriptions that push
passing the active project's id and reserve project_id=0 for a
deliberate cross-project sweep.
- _INSTRUCTIONS: add a 'Reach for Scribe to RECALL, not just to record'
block — search before answering/starting, check for an existing ticket
before create_task, scope reads to the active project (which does not
stick on the server).
Paired with always-on rule #75 in the FabledSword-family rulebook.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Header wordmark Fabled -> Scribe; fable:calendar-changed event ->
scribe:calendar-changed; SettingsView CSS comment.
- Drop dead Project.auto_summary + summary_updated_at columns (migration
0063) -- the Ollama-era summarizer is gone; model + 2 frontend types +
projects test updated.
- Remove pivot vestiges: diagnostics _curator_busy()/curator_busy
heartbeat field, tz BRIEFING_DAY_START_HOUR/user_briefing_date dead
aliases, the ignored 'model' param on get_embedding (+ its test).
ruff src/ clean; CI is the gate. Part of scribe plan #599.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames src/fabledassistant -> src/scribe and all imports, plus the
default DB name and DB user/password (fabled -> scribe) in config +
compose. 952 refs / 154 files. Reverses the old 'internal name stays
fabledassistant' convention.
Code-only: live databases are still physically named 'fabledassistant'.
Deployed environments must set POSTGRES_DB / POSTGRES_USER (or rename the
DB) since the defaults now resolve to 'scribe'. Repo (FabledScribe), git
host (fabledsword), MCP (fabled-git) and the image name (fabledscribe)
are intentionally unchanged.
ruff check src/ clean locally; CI (typecheck + pytest) is the gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main pushes now move :latest (in addition to the immutable :<sha>), so a
merge to main updates production's pointer directly — no separate release
needed just to refresh :latest. The v* release tag's distinct job becomes
the dated :<version> marker (it still refreshes :latest harmlessly). Still
no :main tag. Rules 47/46 + 10/4 updated to match on both instances.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ACL constraint (scope every read/mutation by owner + shares via
services/access.py) is a security-correctness invariant that should
always be loaded, and it's FabledScribe-specific — so it belongs in
Scribe's own contained _INSTRUCTIONS, not the cross-project FabledSword
rulebook. The redundant rulebook rule will be retired once this ships
to prod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
:latest (release-only) is the single production pointer; a :main moving
tag just duplicated it. main pushes still gate + build (the :<sha> image
is the rollback point), but no longer publish a :main alias. The tag was
new and unreferenced, so nothing depends on it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously main pushes were deliberately skipped — CI only ran on dev
and v* tags. This conflicted with the intended policy (CI on dev AND
main). Now main is a first-class gated, built line: dev->:dev, main->:main,
v* tag->:latest + :<version>, every build also tagged with the commit sha.
Per-ref concurrency already supersedes rapid pushes, so dev and main run
independently without stacking identical work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an always-on _INSTRUCTIONS directive: when work touches Scribe and
no project is in scope, search for a related project and propose
enter_project (confirm first), or offer to create one (confirm name/goal
first) — never silently adopt or create. Pairs with the enter_project
handshake and the host-memory pointer directive. Closes scribe task #585.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Optional FKs on update_task previously had no way to express 'remove' —
0 meant leave-unchanged and any positive int meant set, so a milestone
(or project) could only be cleared via the web UI. Now -1 clears the FK
(NULL); clearing project_id also clears milestone_id since a milestone
can't outlive its project. update_note already NULLs on None, so the
change is confined to the tool wrapper. Closes scribe task #586.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a project subscribes to a rulebook, the agent should ensure the
host's persistent memory carries a pointer that engineering/workflow
rules live in Scribe (loaded via list_always_on_rules / enter_project),
plus a one-line note of the current project's work. Pairs with the
existing 'don't duplicate rules into memory' directive: memory holds the
pointer + project context, Scribe holds the rules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a 'keep task state honest' directive to the MCP _INSTRUCTIONS: set
in_progress on start, log progress with add_task_log as you go, set done the
moment work completes (never leave finished work at todo), and write a dated
dev-log note on the project at significant landings. Reinforced in the
update_task status docstring. App-layer + always-loaded, no rule/config needed
— closes the gap where finished work (e.g. a shipped plan) sat open because the
lifecycle was available but never prescribed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 3 of #583. New DashboardView at /dashboard composes the approved layout:
done-recently strip, Active-now project panels (project -> active milestones ->
open tasks, in-progress flagged, + no-milestone group), and a rail with
upcoming events / week stats / quick-create (Task/Note/Process). '/' now
redirects to /dashboard; AppHeader gains a Dashboard link and relabels
Knowledge -> Browse (route unchanged). Empty + loading states included.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 2 of #583. Minimal login-gated blueprint returning build_dashboard(uid);
registered in the app factory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 1 of #583. build_dashboard(user_id) assembles the /dashboard payload:
most-recently-active projects (ranked by max child updated_at) each broken
into active milestones -> open tasks (in_progress->priority->recency, capped 5),
recently-completed (7d/8), upcoming events (7d), week stats. Owner-scoped,
trashed excluded; each section isolated via _safe so one failure doesn't blank
the page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 6 of #582. NoteType gains 'process'. NoteEditorView branches to a plain
monospace textarea (labeled Prompt) for processes instead of the TipTap
rich-text editor — prompts are plain markdown and rich-text round-tripping
would mangle them. Title/tags/save path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 5 of #582. Add 'process' to the KnowledgeItem/activeType/KnowledgeCounts
types, a Processes entry in the type-filter row, a Workflow-icon quick-create
button (createNew('process') -> /notes/new?type=process), and a Process card
badge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 4 of #582. Add 'process' to the knowledge route _VALID_TYPES and to the
get_knowledge_counts facet + total. query_knowledge/_apply_type_filter already
handle arbitrary note_type, so listing by type=process works unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 3 of #582. Tells Claude that note_type=process notes are reusable saved
prompts and to fire them via list_processes/get_process on 'run the X process'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 2 of #582. New mcp/tools/processes.py mirrors entities.py — tools wrap
notes_svc directly. get_process is the fire mechanism (returns the full prompt
via resolve_process; surfaces other_matches on an ambiguous name). Registered
in register_all.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 1 of the Stored Processes plan (#582). resolve_process(user_id, name_or_id)
resolves a note_type=process note owner-scoped + non-trashed, precedence
numeric id -> exact case-insensitive title -> substring; returns
(note, other_candidates) so an ambiguous fuzzy match can be disambiguated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 8 (final item). _fire_reminders previously gated on the
base row (reminder_sent_at IS NULL AND start_dt > now), so a recurring event
reminded at most once ever — once the first occurrence passed, no future
occurrence qualified.
Now recurring events are evaluated every sweep against their next occurrence
(rrulestr.after(now)), and reminder_sent_at stores the start of the occurrence
last reminded about. Each new occurrence has a distinct marker, so it re-arms
and fires exactly once per occurrence. One-shot events keep the classic
NULL gate. Also adds the deleted_at filter so trashed events stop reminding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>