Compare commits

..
21 Commits
Author SHA1 Message Date
bvandeusen 837489e4f2 Merge pull request 'CI: build on main (and drop the :main tag)' (#57) from dev into main
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 45s
CI & Build / Build & push image (push) Successful in 14s
2026-06-03 12:44:44 -04:00
bvandeusenandClaude Opus 4.8 9a0d5f3109 ci: drop the :main tag — main builds publish only the immutable :<sha>
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 44s
CI & Build / Build & push image (push) Successful in 14s
: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>
2026-06-03 11:52:21 -04:00
bvandeusenandClaude Opus 4.8 5a930319ba ci: gate and build main too (:main image); :latest stays release-only
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 45s
CI & Build / Build & push image (push) Successful in 15s
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>
2026-06-03 11:27:09 -04:00
bvandeusen 266af7870d Merge pull request 'MCP instruction hardening + milestone-unset' (#56) from dev into main 2026-06-03 11:19:09 -04:00
bvandeusenandClaude Opus 4.8 f446573c3d feat(mcp): proactive project bootstrapping at session start
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 1m6s
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>
2026-06-03 11:01:55 -04:00
bvandeusenandClaude Opus 4.8 82d6812c7f feat(mcp): milestone_id=-1 clears a task's milestone (update_task)
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>
2026-06-03 10:59:47 -04:00
bvandeusenandClaude Opus 4.8 8c9ca45479 feat(mcp): instruct agents to keep a Scribe-rules pointer in host memory
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 1m1s
CI & Build / Build & push image (push) Successful in 1m6s
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>
2026-06-03 10:42:23 -04:00
bvandeusenandClaude Opus 4.8 e023c21aa1 docs(mcp): instruct agents to drive task lifecycle + log work
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 48s
CI & Build / Build & push image (push) Successful in 1m48s
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>
2026-06-03 09:24:26 -04:00
bvandeusen e3d7007417 Merge pull request 'Drift-audit remediation + Stored Processes + Dashboard' (#55) from dev into main 2026-06-03 08:11:14 -04:00
bvandeusenandClaude Opus 4.8 65c85bab15 feat(dashboard): DashboardView landing + route + nav
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 44s
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>
2026-06-02 23:24:32 -04:00
bvandeusen 0e980ee4b0 Merge pull request 'Project rule + topic suppressions' (#54) from dev into main
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 48s
CI & Build / Build & push image (push) Successful in 22s
2026-06-01 08:01:59 -04:00
bvandeusen b5870d4694 Merge pull request 'Rules consolidation: Scribe-first check, project-scoped rules, enter_project handshake' (#53) from dev into main 2026-06-01 01:16:55 -04:00
bvandeusen c810d63bee Merge pull request 'MCP plan-prompt tune + Flutter docs removal' (#52) from dev into main
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 45s
CI & Build / Build & push image (push) Successful in 24s
2026-06-01 00:13:31 -04:00
bvandeusen a3a056d6fd v26.05.30.1 — MCP-first pivot + Rulebook + Plans + Soft-delete 2026-05-29 22:50:34 -04:00
bvandeusen 2414437061 Merge pull request 'feat: closeout + tool-use fixes + task-as-record + version pinning' (#50) from dev into main 2026-05-13 23:27:33 +00:00
bvandeusen 76dc75a03b Merge pull request 'feat: journal closeout for profile observations + LLM tool-use fixes' (#49) from dev into main 2026-05-13 01:31:19 +00:00
bvandeusen c8765959ea Merge pull request 'Release v26.04.29.4 — recurrence UI + journal weather wiring fixes' (#48) from dev into main 2026-04-30 02:08:19 +00:00
bvandeusen f85b92a885 Release v26.04.29.3 — Event-store data integrity + EventSlideOver redesign 2026-04-29 20:04:43 +00:00
bvandeusen b81c4aa600 Release v26.04.29.2 — Anti-placeholder rule on create_event 2026-04-29 16:10:53 +00:00
bvandeusen 404698521f Release v26.04.29.1 — Date durability + journal voice tuning 2026-04-29 14:37:45 +00:00
bvandeusen 88b351a96e Merge pull request 'Release v26.04.28.1 — Design system polish, journal config in Profile, doc cleanup' (#44) from dev into main 2026-04-28 13:12:54 +00:00
7 changed files with 342 additions and 33 deletions
+28 -21
View File
@@ -1,12 +1,19 @@
# CI runs first; build only proceeds if all checks pass.
#
# Push to dev: typecheck + lint + test + build :dev + :<sha>
# Tag v* (release): typecheck + lint + test + build :latest + :<sha> + :<version>
# Push to dev: typecheck + lint + test + build :dev + :<sha>
# Push to main: typecheck + lint + test + build :<sha> (no moving tag)
# Tag v* (release): typecheck + lint + test + build :latest + :<version> + :<sha>
#
# main pushes are NOT gated here: a merge to main only happens after
# dev has already passed CI, and the release tag is the sole trigger
# for a production image. Re-running CI on the merge commit just burns
# runner time without changing the outcome.
# Both dev and main are gated AND built. dev pushes move the :dev tag; main
# pushes publish only the immutable :<sha> image — no :main tag, because
# :latest (release-only) is the single production pointer and a :main alias
# would just duplicate it. Running CI on the main merge commit is intentional:
# main is validated and its :<sha> image is the rollback point. The v* release
# tag is the ONLY trigger that publishes :latest plus the immutable :<version>.
#
# Successive pushes to the SAME ref supersede each other (see concurrency
# below), so rapid pushes don't stack identical work; dev and main runs are
# independent refs and never cancel one another.
#
# To cut a release:
# Create a release via the Forgejo UI on main with a v* tag name.
@@ -16,11 +23,8 @@
# gating on branch push is already enough.
#
# NOTE on the `if:` guards below: Forgejo Actions does not consistently
# honor `on.push.branches` as a filter — merge commits landing on main
# still trigger the workflow, producing redundant runs on the same SHA
# that was already gated on dev. Every job therefore repeats the ref
# check so main pushes trigger the workflow but every job skips
# immediately (no runner time, no duplicate work).
# honor `on.push.branches` as a filter, so every job repeats the ref check
# explicitly — permitting dev, main, and v* tags, rejecting anything else.
#
# Required secrets (repo → Settings → Secrets → Actions):
# REGISTRY_USER — your Forgejo username
@@ -29,7 +33,7 @@ name: CI & Build
on:
push:
branches: [dev]
branches: [dev, main]
tags: ["v*"]
paths:
- "src/**"
@@ -67,8 +71,8 @@ env:
jobs:
typecheck:
name: TypeScript typecheck
# Skip on main merge-commit pushes — see workflow header comment.
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')
# Gate dev, main, and v* tags; reject any other ref (see header note).
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -92,7 +96,7 @@ jobs:
lint:
name: Python lint
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -106,7 +110,7 @@ jobs:
test:
name: Python tests
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -138,11 +142,9 @@ jobs:
build:
name: Build & push image
needs: [typecheck, lint, test]
# Build on dev branch pushes and version tag pushes only.
# Mirrors the ref guard on the gate jobs above — main merge-commit
# pushes skip here too, so no production image is ever built from a
# raw main push (only from the v* tag the release creates).
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')
# Build on dev, main, and v* tag pushes. dev → :dev, main → (sha only),
# tag → :latest + :<version>; every build also gets an immutable :<sha>.
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -168,6 +170,11 @@ jobs:
refs/heads/dev)
TAGS="$TAGS,${{ env.IMAGE }}:dev"
;;
refs/heads/main)
# main publishes only the immutable :<sha> image (set above) —
# no :main tag; :latest (release-only) is the production pointer.
BUILD_VERSION="main"
;;
refs/tags/*)
TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}"
BUILD_VERSION="${{ github.ref_name }}"
+4 -2
View File
@@ -45,7 +45,8 @@ router.afterEach(() => {
<!-- Center: primary navigation (desktop) -->
<div class="nav-center">
<div class="nav-pill-bar">
<router-link to="/knowledge" class="nav-link" :class="{ 'router-link-active': isKnowledgeActive }">Knowledge</router-link>
<router-link to="/dashboard" class="nav-link">Dashboard</router-link>
<router-link to="/knowledge" class="nav-link" :class="{ 'router-link-active': isKnowledgeActive }">Browse</router-link>
<router-link to="/calendar" class="nav-link">Calendar</router-link>
<router-link to="/projects" class="nav-link">Projects</router-link>
<router-link to="/rules" class="nav-link">Rulebooks</router-link>
@@ -90,7 +91,8 @@ router.afterEach(() => {
<!-- Mobile dropdown -->
<div v-if="mobileMenuOpen" class="mobile-menu">
<router-link to="/knowledge" class="nav-link" :class="{ 'router-link-active': isKnowledgeActive }">Knowledge</router-link>
<router-link to="/dashboard" class="nav-link">Dashboard</router-link>
<router-link to="/knowledge" class="nav-link" :class="{ 'router-link-active': isKnowledgeActive }">Browse</router-link>
<router-link to="/calendar" class="nav-link">Calendar</router-link>
<router-link to="/projects" class="nav-link">Projects</router-link>
<router-link to="/rules" class="nav-link">Rulebooks</router-link>
+8 -3
View File
@@ -5,10 +5,15 @@ const router = createRouter({
history: createWebHistory(),
routes: [
{
// Knowledge is the landing page in the MCP-first architecture
// (chat / journal / workspace surfaces have been removed).
// The dashboard ("what to work on") is the landing page; Knowledge
// remains as the exhaustive "Browse" surface.
path: "/",
redirect: "/knowledge",
redirect: "/dashboard",
},
{
path: "/dashboard",
name: "dashboard",
component: () => import("@/views/DashboardView.vue"),
},
{
path: "/knowledge",
+208
View File
@@ -0,0 +1,208 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { apiGet } from "@/api/client";
import { relativeTime } from "@/composables/useRelativeTime";
interface TaskRow { id: number; title: string; status: string; priority: string }
interface MilestoneBlock { id: number; title: string; progress_pct: number; open_tasks: TaskRow[] }
interface ActiveProject {
id: number; title: string; color: string | null; last_activity: string;
open_count: number; progress_pct: number;
milestones: MilestoneBlock[]; no_milestone: TaskRow[];
}
interface DoneItem { id: number; title: string; project_title: string | null; completed_at: string }
interface UpcomingEvent { id: number; title: string; start_dt: string | null; all_day: boolean }
interface WeekStats { completed_this_week: number; open_total: number; in_progress: number; active_plans: number }
interface DashboardData {
active_projects: ActiveProject[];
recently_completed: DoneItem[];
upcoming_events: UpcomingEvent[];
week_stats: WeekStats;
}
const data = ref<DashboardData | null>(null);
const loading = ref(true);
onMounted(async () => {
try {
data.value = await apiGet<DashboardData>("/api/dashboard");
} catch {
data.value = { active_projects: [], recently_completed: [], upcoming_events: [], week_stats: { completed_this_week: 0, open_total: 0, in_progress: 0, active_plans: 0 } };
} finally {
loading.value = false;
}
});
function fmtEvent(e: UpcomingEvent): string {
if (!e.start_dt) return "";
const d = new Date(e.start_dt);
const day = d.toLocaleDateString(undefined, { weekday: "short" });
if (e.all_day) return day;
return `${day} ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}`;
}
</script>
<template>
<div class="dash-root">
<header class="dash-head">
<h1>Dashboard</h1>
<p class="dash-sub">What to work on, across your most-active projects.</p>
</header>
<div v-if="loading" class="dash-empty">Loading</div>
<template v-else-if="data">
<!-- Done recently -->
<section v-if="data.recently_completed.length" class="done-strip">
<span class="dash-label"> Done recently</span>
<router-link
v-for="d in data.recently_completed"
:key="d.id"
:to="`/tasks/${d.id}`"
class="done-chip"
>
{{ d.title }}
<span class="done-meta">{{ d.project_title || "—" }} · {{ relativeTime(d.completed_at) }}</span>
</router-link>
</section>
<div class="dash-cols">
<!-- Active now -->
<main class="dash-main">
<div class="dash-label">Active now</div>
<div v-if="!data.active_projects.length" class="dash-empty card">
No active projects yet <router-link to="/projects">create one</router-link>.
</div>
<article v-for="p in data.active_projects" :key="p.id" class="proj-panel">
<header class="proj-head">
<span class="proj-dot" :style="{ background: p.color || 'var(--color-primary)' }" />
<router-link :to="`/projects/${p.id}`" class="proj-title">{{ p.title }}</router-link>
<span class="proj-meta">{{ relativeTime(p.last_activity) }} · {{ p.open_count }} open</span>
</header>
<div class="bar"><div class="bar-fill" :style="{ width: p.progress_pct + '%' }" /></div>
<div v-for="m in p.milestones" :key="m.id" class="ms-block">
<div class="ms-head">
<span class="ms-title">{{ m.title }}</span>
<span class="ms-pct">{{ m.progress_pct }}%</span>
</div>
<router-link
v-for="t in m.open_tasks"
:key="t.id"
:to="`/tasks/${t.id}`"
class="task-row"
:class="{ 'task-inprogress': t.status === 'in_progress' }"
>
<span class="task-mark">{{ t.status === 'in_progress' ? '▸' : '○' }}</span>
<span class="task-title">{{ t.title }}</span>
<span v-if="t.priority !== 'none'" class="task-pri" :class="`pri-${t.priority}`">{{ t.priority }}</span>
</router-link>
</div>
<div v-if="p.no_milestone.length" class="ms-block">
<div class="ms-head"><span class="ms-title ms-none">No milestone</span></div>
<router-link
v-for="t in p.no_milestone"
:key="t.id"
:to="`/tasks/${t.id}`"
class="task-row"
:class="{ 'task-inprogress': t.status === 'in_progress' }"
>
<span class="task-mark">{{ t.status === 'in_progress' ? '▸' : '○' }}</span>
<span class="task-title">{{ t.title }}</span>
<span v-if="t.priority !== 'none'" class="task-pri" :class="`pri-${t.priority}`">{{ t.priority }}</span>
</router-link>
</div>
<router-link :to="`/projects/${p.id}`" class="proj-more">+ more in {{ p.title }} </router-link>
</article>
</main>
<!-- Right rail -->
<aside class="dash-rail">
<div class="dash-label">Upcoming · 7 days</div>
<div class="rail-card">
<template v-if="data.upcoming_events.length">
<div v-for="e in data.upcoming_events" :key="e.id" class="evt-row">
<span class="evt-when">{{ fmtEvent(e) }}</span>
<span class="evt-title">{{ e.title }}</span>
</div>
</template>
<p v-else class="rail-empty">Nothing scheduled.</p>
</div>
<div class="dash-label">This week</div>
<div class="rail-card stats">
<span> {{ data.week_stats.completed_this_week }} done</span>
<span> {{ data.week_stats.open_total }} open</span>
<span class="stats-sub">{{ data.week_stats.in_progress }} in progress · {{ data.week_stats.active_plans }} plans</span>
</div>
<div class="quick-add">
<router-link to="/tasks/new" class="qa-btn">+ Task</router-link>
<router-link to="/notes/new" class="qa-btn">+ Note</router-link>
<router-link to="/notes/new?type=process" class="qa-btn">+ Process</router-link>
</div>
</aside>
</div>
</template>
</div>
</template>
<style scoped>
.dash-root { max-width: 1100px; margin: 0 auto; padding: 1.5rem; }
.dash-head h1 { margin: 0; font-family: 'Fraunces', Georgia, serif; }
.dash-sub { margin: 0.2rem 0 1.25rem; color: var(--color-muted); font-size: 0.9rem; }
.dash-label { display: block; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.6rem; }
.dash-empty { color: var(--color-muted); padding: 1rem 0; }
.dash-empty.card { padding: 1rem; border: 1px dashed var(--color-border); border-radius: 10px; }
.done-strip { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 1.5rem; }
.done-chip { display: inline-flex; flex-direction: column; gap: 1px; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 14px; padding: 4px 12px; font-size: 0.82rem; color: var(--color-text); text-decoration: none; }
.done-chip:hover { border-color: var(--color-primary); }
.done-meta { font-size: 0.7rem; color: var(--color-muted); }
.dash-cols { display: flex; gap: 1.25rem; align-items: flex-start; }
.dash-main { flex: 1.7; min-width: 0; }
.dash-rail { flex: 1; min-width: 240px; }
.proj-panel { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 12px; padding: 0.9rem 1rem; margin-bottom: 0.9rem; }
.proj-head { display: flex; align-items: center; gap: 0.5rem; }
.proj-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.proj-title { font-weight: 700; color: var(--color-text); text-decoration: none; }
.proj-title:hover { color: var(--color-primary); }
.proj-meta { margin-left: auto; font-size: 0.74rem; color: var(--color-muted); }
.bar { height: 5px; background: var(--color-border); border-radius: 3px; margin: 0.55rem 0 0.2rem; }
.bar-fill { height: 5px; background: var(--color-primary); border-radius: 3px; }
.ms-block { margin-top: 0.7rem; }
.ms-head { display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.3rem; }
.ms-title { font-size: 0.82rem; font-weight: 600; color: var(--color-text); }
.ms-title.ms-none { color: var(--color-muted); font-weight: 500; }
.ms-pct { margin-left: auto; font-size: 0.72rem; color: var(--color-muted); }
.task-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.55rem; border-radius: 7px; text-decoration: none; color: var(--color-text); font-size: 0.86rem; }
.task-row:hover { background: var(--color-hover); }
.task-inprogress { border-left: 3px solid var(--color-primary); padding-left: calc(0.55rem - 3px); }
.task-mark { color: var(--color-muted); }
.task-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-pri { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; padding: 1px 6px; border-radius: 8px; border: 1px solid var(--color-border); color: var(--color-muted); }
.pri-high { color: #c0556b; border-color: #c0556b66; }
.proj-more { display: inline-block; margin-top: 0.6rem; font-size: 0.78rem; color: var(--color-primary); text-decoration: none; }
.rail-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 12px; padding: 0.7rem 0.85rem; margin-bottom: 1.25rem; }
.evt-row { display: flex; gap: 0.6rem; padding: 0.3rem 0; border-bottom: 1px solid var(--color-border); font-size: 0.85rem; }
.evt-row:last-child { border-bottom: none; }
.evt-when { color: var(--color-muted); white-space: nowrap; }
.evt-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-empty { margin: 0; color: var(--color-muted); font-size: 0.85rem; }
.stats { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.9rem; }
.stats-sub { color: var(--color-muted); font-size: 0.78rem; }
.quick-add { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.qa-btn { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 8px; padding: 6px 12px; font-size: 0.82rem; color: var(--color-text); text-decoration: none; }
.qa-btn:hover { border-color: var(--color-primary); color: var(--color-primary); }
@media (max-width: 760px) { .dash-cols { flex-direction: column; } }
</style>
+38 -1
View File
@@ -36,7 +36,20 @@ Mechanics:
- Tags are plain strings (no `#` prefix). Empty list clears tags; omit to leave
unchanged on updates.
- For optional integer FKs (project_id, milestone_id, parent_id), use 0 to mean
"not set".
"not set". On update_task, -1 clears an existing FK (e.g. milestone_id=-1
removes the task from its milestone); 0 leaves it unchanged.
Keep task state honest — this is what makes the project a trustworthy record:
- When you begin working a task, set it to in_progress (update_task
status=in_progress).
- Log progress as you go with add_task_log — at meaningful steps, not saved up
for the end.
- The moment a task's work is complete, set it done. Never leave finished work
at todo/in_progress — an out-of-date status makes Scribe misrepresent what's
left to do.
- At a significant landing (a merge, a shipped feature, a finished plan), write
a short dated dev-log note on the project (create_note) summarizing what
landed, and mark the plan/task done.
Scribe maintains a Rulebook system (Rulebook -> Topic -> Rule). Rules carry
an actionable statement plus optional Why and How-to-apply context. At the
@@ -56,6 +69,19 @@ creating a rule, call list_always_on_rules and list_rules(project_id=...) to
avoid duplicates. Coordinate with the operator on whether a new rule belongs
in a project, an existing rulebook+topic, or a new rulebook.
That boundary cuts the other way too. Because rules are pull-only, a fresh
session won't reach for them unless its always-loaded context says to. So
when the current project subscribes to a rulebook (enter_project /
get_project reports subscribed_rulebooks), make sure the host's persistent
memory — the CLAUDE.md / AGENTS.md / ~/.claude memory the client loads at
startup — carries a short pointer: that the engineering and workflow rules
for this work live in Scribe and must be loaded via list_always_on_rules (or
enter_project when a project is in scope), plus a one-line note of what the
current project is and what is in flight. Add or refresh that pointer when
it's missing or stale; never copy the rules' content into memory — the
pointer plus project context is the whole job. This is what lets the next
session reach for Scribe instead of trusting a stale local copy.
When you are working on a specific project, call enter_project(project_id)
ONCE at session start (or whenever the active project changes). It returns the
project, its applicable_rules + project_rules + subscribed_rulebooks, milestone
@@ -63,6 +89,17 @@ summary, open tasks, and recent notes — everything you need to know the lay of
the land before mutating. Don't call get_project + get_applicable_rules + a
search separately when enter_project already composes them.
Don't wait to be told which project you're in. At the start of a session that
touches Scribe — or the moment work clearly belongs to a project but none is in
scope — bootstrap project context proactively: search for a related existing
project (search / list_projects, matching on the work's subject, the repo or
directory name, and recent activity). If you find a confident match, propose it
and call enter_project once the operator confirms. If nothing matches, offer to
create a project, confirming its name and goal first. Always confirm before
adopting or creating — never do either silently, and never guess a project into
existence. Once a project is in scope, the enter_project handshake and the
host-memory pointer step above both apply.
Plans are tasks with kind=plan, and Scribe is the canonical home for them.
When you begin non-trivial work, call start_planning(project_id, title) FIRST —
before any brainstorming, design, or plan-writing skill runs. start_planning
+16 -6
View File
@@ -14,7 +14,7 @@ Sentinels (preserved from existing fable-mcp):
what makes a Note a Task)
- priority="none" sets explicit no-priority; priority="" is "leave unchanged"
- project_id=0 / milestone_id=0 / parent_id=0 → "no association" on create,
"leave unchanged" on update
"leave unchanged" on update; on update, -1 clears the FK (sets it NULL)
"""
from __future__ import annotations
@@ -143,10 +143,14 @@ async def update_task(
task_id: ID of the task to update.
title: New title, or omit to leave unchanged.
body: New markdown body, or omit to leave unchanged.
status: New status — one of: todo, in_progress, done, cancelled.
status: New status — one of: todo, in_progress, done, cancelled. Drive
the lifecycle: set in_progress when you start, done when complete —
don't leave finished work at todo.
priority: New priority — one of: none, low, medium, high.
project_id: New project. Omit (0) to leave unchanged.
milestone_id: New milestone. Omit (0) to leave unchanged.
project_id: New project. 0 = leave unchanged, -1 = clear (remove from
its project; also clears the milestone), positive = set.
milestone_id: New milestone. 0 = leave unchanged, -1 = clear (remove
from its milestone), positive = set.
"""
uid = current_user_id()
fields: dict = {}
@@ -158,9 +162,15 @@ async def update_task(
fields["status"] = status
if priority:
fields["priority"] = priority
if project_id:
# Optional FKs: 0 = leave unchanged, -1 = clear (set NULL), positive = set.
if project_id == -1:
fields["project_id"] = None
fields["milestone_id"] = None # a milestone can't outlive its project
elif project_id:
fields["project_id"] = project_id
if milestone_id:
if milestone_id == -1:
fields["milestone_id"] = None
elif milestone_id:
fields["milestone_id"] = milestone_id
note = await notes_svc.update_note(uid, task_id, **fields)
if note is None:
+40
View File
@@ -164,6 +164,46 @@ async def test_update_task_raises_when_not_found():
await update_task(task_id=999, status="done")
@pytest.mark.asyncio
async def test_update_task_milestone_zero_is_omitted():
"""milestone_id=0 is 'leave unchanged' — must not reach the service."""
fake = _fake_task()
mock = AsyncMock(return_value=fake)
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
await update_task(task_id=1, milestone_id=0)
assert "milestone_id" not in mock.call_args.kwargs
@pytest.mark.asyncio
async def test_update_task_milestone_positive_is_set():
fake = _fake_task()
mock = AsyncMock(return_value=fake)
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
await update_task(task_id=1, milestone_id=42)
assert mock.call_args.kwargs["milestone_id"] == 42
@pytest.mark.asyncio
async def test_update_task_milestone_negative_one_clears():
"""milestone_id=-1 clears the milestone (sets the column NULL)."""
fake = _fake_task()
mock = AsyncMock(return_value=fake)
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
await update_task(task_id=1, milestone_id=-1)
assert mock.call_args.kwargs["milestone_id"] is None
@pytest.mark.asyncio
async def test_update_task_clearing_project_also_clears_milestone():
"""project_id=-1 clears the project and, with it, the milestone."""
fake = _fake_task()
mock = AsyncMock(return_value=fake)
with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock):
await update_task(task_id=1, project_id=-1)
assert mock.call_args.kwargs["project_id"] is None
assert mock.call_args.kwargs["milestone_id"] is None
@pytest.mark.asyncio
async def test_add_task_log_returns_log_dict():
log = MagicMock()