Frontend foundation for Issues + Systems (spec #825, S4a).
- frontend/src/api/systems.ts: typed client (System + list/create/update/delete)
over /api/projects/<id>/systems, matching the rulebooks api style.
- frontend/src/stores/systems.ts: Pinia store keyed by project (fetch/create/
update/archive/unarchive/delete), toast-on-error.
- frontend/src/components/SystemsSection.vue: a Systems management section —
cards (color swatch, name, description, 'N open' issue-count badge) with
inline create/edit, archive (hidden behind a 'show archived' toggle), and a
delete-confirm modal. v1 quality: loading skeleton, empty state, error toasts,
keyboard a11y, focus rings; reuses existing CSS tokens (no new colors).
- ProjectView.vue: new 'Systems' tab (between Notes and Rules), rendering
<SystemsSection :project-id>, wired like the existing rules tab.
S4b (next) adds issue-editor controls (kind=issue/system multi-select/arose-from),
open-issues lists, and the dashboard surface. NEEDS operator browser verification
(CI typechecks but can't render).
Refs plan 825 (S4a).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third slice of Issues + Systems (spec #825).
routes/systems.py (nested /api/projects/<id>/...): GET/POST systems (list adds
per-system open_issue_count via one grouped query), GET/PATCH/DELETE a system
(GET returns records split into issues/tasks/notes), GET .../systems/<id>/records
(kind/open_only filters), GET .../issues (project's open issues for the project
view + dashboard roll-up). login_required; project access via get_project_for_user;
writes gated by can_write_project (clean 403); system.project_id verified to match
the path. Blueprint registered in app.py.
services/systems.py: + open_issue_counts_by_system (one grouped query) and
list_issues (project issues, open by default).
Tests: structural (blueprint registered + in app, handlers callable, service
contracts take user_id) — matches the house route-test pattern.
Refs plan 825 (S3).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second slice of Issues + Systems (spec #825).
New mcp/tools/systems.py: create_system, list_systems, get_system (records
split into issues/tasks/notes), update_system (incl. archive via status),
list_system_records (kind/open_only filters), delete_system. Registered in
register_all; read tools (get_system, list_systems, list_system_records) added
to the read-only-key allowlist (write tools default-deny).
create_task/update_task: kind now accepts 'issue'; new system_ids (set-semantics
associations) and arose_from_id (provenance, 0=unchanged/-1=clear) args.
create_note/update_note: new system_ids arg (notes associate with systems too).
services/notes.create_note: arose_from_id passthrough (update_note already
handles it via setattr).
Tests: MCP system tools + create_task issue-wiring (kind/provenance/systems),
service layer mocked.
Refs plan 825 (S2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First slice of the Issues + Systems feature (spec #825, plan #819 T2).
Schema (migration 0065):
- task_kind CHECK expands work|plan -> work|plan|issue (same-change, rule 36)
- notes.arose_from_id: optional self-FK for issue->originating-task provenance
(distinct from parent_id sub-task hierarchy)
- systems: per-project, self-describing (name + description) subsystem/area
- record_systems: M2M join linking any note/task/issue to systems (mutable)
Models: System + RecordSystem; note.py gains arose_from_id (+ index, to_dict).
Service services/systems.py: CRUD, archive, soft-delete, set/list associations,
records-for-system, open-issue count — all gated via services/access.py project
permissions (rule 78, no bare-owner filters). Unit tests lock the ACL gating;
the migration is exercised by CI's integration lane (alembic upgrade head).
is_task stays a derived property (status is not None) — unchanged. T1 (typing-
axis rationalization) intentionally NOT bundled; this only adds the enum value.
Refs plan 825 (S1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Superpowers was uninstalled but its replacements were never built (only
using-scribe shipped) — a live functional hole. Author the 4 the operator
wants back, each integrated with Scribe's toolset rather than generic copies:
- writing-plans -> start_planning / kind=plan task, not local .md
- systematic-debugging -> capture issue (symptom->cause->fix, tag issue) on resolve
- verification -> log results to the task work-log; honest done
- brainstorming -> recall prior thinking first; capture the decision note
Skipped TDD + receiving-code-review per operator (well-covered by Claude/them).
Manifest + using-scribe list now advertise only the 4 that ship. Remove the
stale docs/superpowers/*.md reference in _INSTRUCTIONS (superpowers is gone).
Plugin 0.1.6 -> 0.1.7.
Refs plan 821 (Phase 3 of 755).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deliver 'don't silently lose work at compaction' via the mechanism that
actually works. Verified contract: a PreCompact hook CANNOT make the model
flush to Scribe (host hooks can't trigger model tool calls, and can't know the
in-flight task ids), and its additionalContext only shapes the one-shot summary.
The correct tool is SessionStart scoped to source=compact, which fires AFTER
compaction and injects context the model reads.
Our SessionStart hook is matcher-less, so it already fires on compact — it just
said nothing compaction-specific. Now it reads the stdin event and,
when source==compact, leads with a banner telling the model to reload the active
project + in-flight tasks from Scribe and reconcile half-remembered state.
Durable path = record-as-you-go (A4/B8) + this post-compaction reload.
Refs plan 812 (A7); supersedes the literal 'PreCompact hook' idea.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finish the breakfix/issue-logging gap as a lightweight convention: when
recording a solved problem, capture symptom -> root cause -> fix and tag it
'issue' so it's findable instead of re-diagnosed. Pairs with the B9 trigger
('log when a problem is found'). No schema change — a structured note_type/
task_kind=issue is deferred to a joint schema pass with B7.
Refs plan 812 (B8 convention; B7 deferred).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fail-open but no longer silent. When the dynamic context fetch yields nothing,
append a short status line to the injected context so a session can tell
'couldn't load live context' apart from 'Scribe had nothing to say':
- endpoint+token present but fetch empty/failed -> 'instance unreachable / request failed'
- endpoint present but token absent -> fingerprints the known Claude Code
userConfig export gap ('API token did not reach this hook')
A fully unconfigured install (no url AND no token) stays quiet — static-only is
the intended mode there. Static Tier 1 still always carries the mandate.
Refs plan 812 item A2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the '(This instance's rules carry the specifics.)' pointer — universal
_INSTRUCTIONS must not assume this install has a particular rulebook. State the
ACL principle on its own so it holds for any Scribe install/fork.
Refs plan 812 (instance-agnostic product principle).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP instructions are domain-neutral except a thin layer of dev vocabulary
and one project-specific paragraph (B10 audit, task 812). Make the data store's
own instructions serve any domain, and add the missing positive write-mandate.
B9 (neutralize):
- 'before writing code' -> 'before you dive in'
- Note examples 'dev-logs' -> 'logs of what happened'
- record trigger 'a merge, a shipped feature, a finished plan' + 'dev-log note'
-> 'finishing a task, or hitting/discovering a problem that changes direction'
(folds in B8: log pivots, not just wins; mirrors the static-tier wording)
- recall examples 'ticket/dev-log' -> 'task/prior note' (server + SKILL.md)
- 'Engineering and workflow rules' -> 'Workflow and standards rules'
- slim the 'developing Scribe itself' ACL paragraph to a neutral one-liner
(project-specific specifics already live in rules #47/#78)
A4 (write-mandate): state up front that Scribe is the system of record — record
work here, recall before acting, don't keep project work in local files.
Refs plan 812 (B9, A4, B8-trigger); B10 audit work-log.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plain-language 'related prior work' instead of 'prior art'; replace the
dev-shaped 'meaningful landing (a merge, a shipped feature, a finished plan)'
with concrete neutral triggers — log on task completion and when a problem is
found, so direction pivots are captured, not just successes. Keeps the static
mandate domain-neutral (pre-empts B9 drift in plan 812).
Refs task 809 / plan 812 item A1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SessionStart push channel was single-tier: it curled /api/plugin/context
with a Bearer token and, on any failure (missing/unexported token, network
error, missing curl), injected nothing and exited 0 — silently. A known
upstream Claude Code gap (sensitive userConfig not reliably exported to hook
subprocesses) trips this routinely, so a fresh session gets no signal to reach
for Scribe and falls back to local file-memory (root cause of unlogged work on
remote/rc sessions).
Split into two tiers:
- Tier 1 (static, keyless, networkless, always fires): inject bundled
scribe_static_context.md — the load-bearing behavioral mandate. Cannot be
suppressed by the upstream key bug.
- Tier 2 (dynamic, best-effort, fails open): existing curl for live rules +
active-project context, appended below the static block. Lights up as
enrichment once the key reaches the hook.
Only jq is now required (JSON envelope); curl/token gate the dynamic tier only.
Bump plugin 0.1.5 -> 0.1.6 so clients pick up the change.
Refs milestone 55; task 809; decision note 810.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Direction change (operator, see plan task #755 work-log): the plugin must
NOT depend on disabling a native Claude function to work. It earns its place
by steering behavior, not by toggling autoMemoryEnabled.
Memory doctrine (no dual-write):
- using-scribe SKILL.md gains "Scribe holds these functions — don't keep a
second copy": route rules/recall/planning to Scribe, don't also write them
to native auto-memory, never instruct disabling a native function, and
accept a "Scribe-shaped hole" if the plugin is removed (recover over time).
- mcp/server.py _INSTRUCTIONS: drop the paragraph that told the model to
create/refresh a "rules live in Scribe" pointer in CLAUDE.md / ~/.claude
memory. That was an active dual-write instruction; the SessionStart hook is
the bridge now. Replaced with the no-dual-write / no-settings-dependency
doctrine. Supersedes plan #755 Phase 6 ("set autoMemoryEnabled:false").
Project-scope discipline (stop cross-project bleed):
- using-scribe SKILL.md gains "Stay inside the active project's scope": pass
project_id to every read, only reference/offer work on the in-scope project,
ask before switching.
- _INSTRUCTIONS scope bullet extended from reads to referencing/offering, and
flags get_recent as cross-project.
- get_recent docstring gains a scope note steering to scoped list_* when a
project is active.
plugin.json 0.1.4 -> 0.1.5 so clients' caches actually refresh (re-shipping
under the same version does not bust the cache).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Drift-audit Group 7: learned_summary, observations_raw, and
observations_updated_at were populated by the curator/LLM-profile machinery
removed in the Phase-8 pivot. Nothing has written them since and the profile
API returned permanently-empty fields. Remove them from the model + to_dict
and drop the columns (migration 0062). Verified zero frontend/backend/test
consumers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 7 frontend cleanup (no behavioral change):
- SettingsView: remove the 'auto-consolidate task bodies' toggle and its
saveAutoConsolidate handler. The auto_consolidate_tasks setting has zero
backend readers (curator removed in Phase 8); the control did nothing.
- AppSettings type: drop the dead assistant_name / default_model hints (kept
the open string index signature the store actually uses). Delete the fully
orphaned types/chat.ts (zero importers).
- notes/tasks Pinia stores: remove the list/filter/sort/pagination surface
that backed the removed /notes and /tasks list views (verified no consumer
uses the tasks/notes arrays, refresh, or any filter/sort/pagination method).
Kept currentNote/currentTask, loading, fetch/create/update/delete, convert,
patchStatus, startPlanning, backlinks, tags.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 7 (renamed/removed lingers) + Group 5 #9:
- docker-compose.prod.yml pulled fabledassistant:latest, a tag CI stopped
publishing after the rename. Point it at fabledscribe:latest (the name CI
and quickstart use). The internal DB name stays fabledassistant by design.
- Remove the unused hard-delete delete_note imports from the notes and tasks
route modules (they delete via trash; the import was an attractive nuisance
that bypassed soft-delete).
- delete_rule MCP tool: docstring/warning said 'permanently delete' but the
body moves the rule to recoverable trash. Corrected to match.
- Delete services/calendar_sync.py: fully orphaned (zero importers) and it
read Config attrs that no longer exist, so any re-wiring would crash.
- Remove dead services: notes.search_notes_for_context and logging.log_generation
(zero callers; log_generation wrote a 'generation' category no stats/UI surface).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 9 (param-cliff / unbounded search work):
- semantic_search_notes: the O(rows) cosine-similarity scoring loop ran
synchronously on the event loop, so every RAG injection / search stalled
other requests proportional to the user's embedding count. Move the scoring
into asyncio.to_thread (results unchanged). The deeper fix — bounding the
candidate set via pgvector ORDER BY/LIMIT — is noted as separate infra work.
- _semantic_knowledge_search: documented the best-effort top-N semantics —
is the capped candidate-window size (not the true match count),
matches beyond the cap aren't page-reachable, and each page recomputes the
full merge. Prevents the silent-truncation trap; cached ranked-id paging /
pgvector is the fix if exhaustive pagination is ever required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 8 (lifecycle gaps):
- change_password no longer 500s for OAuth-only users: short-circuit when
password_hash is None (verify_password would crash on None) so the route
returns a clean 4xx instead of a 500.
- register_with_invitation no longer locks the invitee out on a username
collision: create the user FIRST, then mark the token used, so a failed
creation (409) leaves the single-use invite valid for retry.
- update_event re-arms reminder_sent_at when start_dt/reminder_minutes change,
so a rescheduled event fires again instead of being permanently suppressed.
- Migration 0061: uq_topic_per_rulebook / uq_rule_per_topic become PARTIAL
unique indexes (WHERE deleted_at IS NULL). Trashing 'X' then recreating it
no longer 500s on the dead row's title. Model __table_args__ updated to match.
Deferred: per-occurrence reminders for recurring events (event_scheduler) —
needs a per-occurrence reminder-state design, not a one-line gate tweak.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 5 #7/#8 + Group 7 (CalDAV write-path):
- Point events no longer fabricate a 60-min DTEND: caldav.create_event emits
DTSTART-only when there's no end and no duration, so the next pull doesn't
read it back as duration_minutes=60 and silently lengthen the event.
- Recurrence edits now propagate: caldav.update_event gains a recurrence param
(sentinel = leave unchanged; value/empty = set/clear RRULE), and _push_update
passes the local event's rule so a changed/cleared RRULE isn't overwritten
by the stale remote rule on the next pull.
- Event deletions propagate to CalDAV: trash.delete captures an event's
caldav_uid before soft-deleting and fires _push_delete, so a UI/MCP delete
removes the remote copy instead of leaving it to linger. (delete_event the
service primitive is kept — still tested/usable — rather than removed.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 6 + Group 5 #6 (enum-extension / status drift):
- ProjectStatus gains 'paused' — routes and frontend already treated it as
first-class, but the enum (the source of truth) omitted it and the error
strings lied. A future CHECK derived from the enum would have rejected
existing paused rows.
- create_project/update_project now validate status via ProjectStatus at the
service layer (canonical gate; notes.status has no DB CHECK), so the MCP
create/update_project path can't persist a typo'd status. MCP docstrings
realigned to the 4-value domain; route error strings corrected.
- get_milestone_progress: cancelled tasks are excluded from the percent
denominator (and now reported in status_counts), so a milestone whose only
open task was cancelled reaches 100% instead of stalling below it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI fix for aef5009: test_resolve_bearer_none_for_invalid referenced
resolve_bearer but the import lived inside an earlier test only. Hoist it
to the module import. Production code unaffected (1 failed / 284 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 5 (high-severity contract drift):
- MCP read-only keys could call every write tool: the Bearer resolver
discarded api_key.scope and dispatch had no gate. Add resolve_bearer()
(returns user_id + scope) and a scope gate in the /mcp ASGI wrapper that
buffers the JSON-RPC body and rejects tools/call for any tool outside a
read all-list when scope=='read' (default-deny for unknown/new tools).
- Shared project notes/tasks panel was empty for non-owners: get_project_notes_route
now queries notes/milestones with the project OWNER's uid (mirrors the
already-fixed milestones route).
- Shared editors couldn't save/delete shared NOTES (tasks worked): the three
notes write routes now resolve via get_note_for_user, gate on can_write_note,
and write as the owner — matching the tasks routes.
- Event timezone drift: naive datetimes from the MCP date+time split are now
localized to the user's tz at a single canonical service point (create_event
/update_event), so MCP- and UI-created events agree. tz-aware inputs
(REST/CalDAV) pass through untouched.
- create_note validates status/priority (TaskStatus/TaskPriority), closing the
MCP create_task path that let out-of-enum values persist (no DB CHECK).
Tests cover resolve_bearer scope + the write-tool classifier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 4 (retention / unbounded growth):
- CalDAV pull now reconciles deletions: a previously-synced event whose
caldav_uid no longer appears remotely within the synced window is
soft-deleted (one batch_id per run, restorable), so a remote delete
propagates locally instead of orphaning forever. Guarded on a non-empty
fetch so a spurious empty result can't wipe every local copy. Also wrap
the blocking fetch in a 120s wait_for and log run duration.
- Notifications: hourly loop now purges read notifications older than 30d
(unread kept). Table no longer grows without bound.
- Auth tokens: new daily sweep deletes password-reset / invitation tokens
whose validity window ended >7d ago; wired via start_auth_token_retention_loop
in app startup. Both tables previously only flipped used=True, never pruned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 3 (soft-delete lifecycle gaps). Trashed rows were
leaking into reads and being mutated/resurrected by writes:
- update SELECTs now exclude trashed rows: update_milestone,
update_project, update_event, and get_milestone_in_project (the latter
backs all four milestone routes). Mutating a trashed row silently
persisted and reappeared on restore.
- MCP get_recent (notes/projects/events) and list_tags now filter
deleted_at IS NULL, so trashed items stop surfacing in the agent's
bootstrap context and tag counts.
- convert_task_to_note clears recurrence_rule + recurrence_next_spawn_at
so a demoted note can't spawn children via the (now-live) sweep.
- caldav pull skips locally-trashed events (by caldav_uid) instead of
resurrecting them via update or creating a duplicate live copy.
- trash _cascade now stamps the FULL sub-task subtree (iterative descent),
not just direct children, so deeply nested sub-tasks restore as one
batch. Test updated for the new descent query.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 2 (Phase-8 amputation — live wiring, no consumer):
- Recurring tasks never recurred: spawn_recurring_tasks() had no caller.
Register it as a 15-min interval job in the event scheduler (which
app.py already starts/stops). Also add a deleted_at IS NULL guard to
the spawn query in the same change, so a trashed recurring parent can
never resurrect children once the sweep is live.
- Event reminders were stamped reminder_sent_at but never delivered.
_fire_reminders now creates an 'event_reminder' in-app notification
before stamping, so a delivery failure stays retryable. Frontend
NotificationsPanel renders the new type (⏰ + message); message logic
pulled into a notifMessage() helper.
- Remove the dead _fire_push_notif no-op stub (push left in Phase 8) and
its three create_task call sites — no more throwaway tasks per share.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 1 (authz/IDOR). Multi-user is live, so these were
exploitable ACL bypasses:
- trash.py: add _owner_clause() and apply it to _exists_alive, restore,
purge, list_trash, and purge_expired. A batch_id is a bearer token;
without an owner predicate a leaked/guessed id let one tenant read
(list_trash), restore, or PERMANENTLY purge another's content. Topics
and rules carried no owner check at all (_OWNER mapped them to None) —
ownership now derives through the parent rulebook (or owning project,
for project-scoped rules).
- purge_expired is now per-user; trash_scheduler iterates every user and
applies that user's own trash_retention_days window, instead of
applying user 1's window to everyone (early data loss for other users).
- rulebooks subscribe/unsubscribe_project now assert project ownership,
matching the suppression endpoints.
- topic/rule DELETE routes return 404 when nothing owned was removed.
Regression test locks in that every model — including topics/rules —
gets a real owner clause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lets a project mute individual rules or whole topics from rulebooks it
subscribes to, without unsubscribing the rulebook. Two new association
tables (migration 0060), 4 MCP tools (suppress/unsuppress × rule/topic),
4 REST endpoints, and an inline "× skip" affordance plus collapsed
"Suppressed (N)" section in the project's Rules tab.
get_applicable_rules now emits suppressed_rules and suppressed_topics
(detail objects with rulebook/topic context, not just IDs) so the UI
can render the suppressed list without a follow-up lookup. The main
rules projection grew topic_id and rulebook_id columns for the per-row
suppress affordance.
Project deletion cascades the suppression rows via hard DELETE — they
are pure associations with no soft-delete column, and restoring a
deleted project should start fresh, not inherit stale mutes.
Project-scoped rules (Rule.project_id) are deliberately not suppressible
— delete them with delete_rule instead.
Implements plan-task #187.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New enter_project(project_id) MCP tool composes get_project +
get_applicable_rules + get_project_milestone_summary + recent
open-tasks + recent notes into one round-trip, intended to be called
at session start (or whenever the active project changes) so Claude
has the full project context loaded before it starts mutating.
_INSTRUCTIONS now points Claude at enter_project for project-scoped
work, alongside the existing list_always_on_rules instruction. No
schema change; pure composition over existing services.
Closes the four-slice rules-consolidation plan (Scribe task #508):
S1+S2 (always_on flag + Scribe-first prompt, 658348f), S3 (project-
scoped rules, 43a860c), and now S4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rules can now belong to either a rulebook topic OR a single project,
enforced by a CHECK constraint (exactly-one of topic_id/project_id).
Adds the create_project_rule MCP tool + REST endpoint, surfaces
project-scoped rules in get_project/get_task/start_planning under a
new project_rules field, and adds a project Rules tab section with an
inline create form so the operator can author project rules from the
UI without rulebook ceremony.
- migration 0059: rules.project_id (FK projects ON DELETE CASCADE),
topic_id now nullable, CHECK ck_rule_topic_xor_project, index on
project_id
- model: Rule gains project_id; to_dict exposes it
- service: create_project_rule with project-ownership guard; list_rules
with project_id filter UNIONs subscription-derived + project-scoped;
get_applicable_rules adds a project_rules field; get_rule / update_rule
/ delete_rule fetch via a shared _fetch_owned_rule that handles both
rulebook and project ownership paths
- trash: project delete cascades to project-scoped rules
- MCP: create_project_rule tool registered; _INSTRUCTIONS mentions both
create_rule and create_project_rule paths
- REST: POST /api/projects/<id>/rules (statement required, title derived
if omitted)
- frontend: Rule type gains nullable topic_id + project_id; createProjectRule
client; ProjectRulesTab.vue gains a "Project rules" section with inline
create form and per-rule expand/delete
- tests: register count → 18; create_project_rule unit tests (required
fields, title derivation, explicit-title pass-through); applicable_rules
shape tests now include project_rules; trash cascade test updated to
expect 5 executions
S1+S2 (always_on flag + Scribe-first prompt) shipped in 658348f.
S4 (enter_project handshake) follows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds rulebooks.always_on (migration 0058) and a new list_always_on_rules
MCP tool so a session-start eager pull can fetch standing rules without
needing an active-project notion. Updates _INSTRUCTIONS so Claude calls
the new tool at session start and codifies engineering rules in Scribe
rather than CLAUDE.md / auto-memory.
Seeds FabledSword family rulebook to always_on=true on migrate, matching
its design role as the cross-project standards rulebook.
Frontend: badge in RulebookListPane for always-on rulebooks; toggle in
RulebookDetailPane header bound to a new toggleAlwaysOn store action.
This is S1+S2 of the rules-consolidation plan (Scribe task #508). S3
(project-scoped rules) and S4 (enter_project handshake) follow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Flutter app (separate fabled_app repo) no longer adds value over
web/PWA access. Strip the in-repo surface that referenced it:
- delete docs/android-app.md
- drop README docs-table row and feature-list mention
- drop the two Flutter roadmap bullets from docs/features.md
- remove the Flutter port subsection from docs/design-system.md
The standalone fabled_app repo is untouched here; archival/deletion
of that repo is a separate decision.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The prior wording ("not in local .md files") was a footer after a
how-to and lost to the much louder superpowers brainstorming /
writing-plans skill flow, which terminates by saving to
docs/superpowers/specs/*.md and docs/superpowers/plans/*.md.
Reorder so start_planning is named as the FIRST action, explicitly
override the .md skill paths, and extend the rule to cover specs as
well as plans (matches the rulebook's rule 27).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stateful session manager strands Claude Code after a container redeploy:
it reconnects with a now-unknown Mcp-Session-Id, the server 404s, and the
client won't re-initialize on a 404 (claude-code #60949). Stateless makes
each request self-contained (bearer-auth only) so post-deploy reconnect
works without a manual /mcp retry.