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.