Compare commits

...
18 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
bvandeusen e3d7007417 Merge pull request 'Drift-audit remediation + Stored Processes + Dashboard' (#55) from dev into main 2026-06-03 08:11:14 -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
4 changed files with 94 additions and 27 deletions
+28 -21
View File
@@ -1,12 +1,19 @@
# CI runs first; build only proceeds if all checks pass. # CI runs first; build only proceeds if all checks pass.
# #
# Push to dev: typecheck + lint + test + build :dev + :<sha> # Push to dev: typecheck + lint + test + build :dev + :<sha>
# Tag v* (release): typecheck + lint + test + build :latest + :<sha> + :<version> # 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 # Both dev and main are gated AND built. dev pushes move the :dev tag; main
# dev has already passed CI, and the release tag is the sole trigger # pushes publish only the immutable :<sha> image — no :main tag, because
# for a production image. Re-running CI on the merge commit just burns # :latest (release-only) is the single production pointer and a :main alias
# runner time without changing the outcome. # 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: # To cut a release:
# Create a release via the Forgejo UI on main with a v* tag name. # 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. # gating on branch push is already enough.
# #
# NOTE on the `if:` guards below: Forgejo Actions does not consistently # NOTE on the `if:` guards below: Forgejo Actions does not consistently
# honor `on.push.branches` as a filter — merge commits landing on main # honor `on.push.branches` as a filter, so every job repeats the ref check
# still trigger the workflow, producing redundant runs on the same SHA # explicitly — permitting dev, main, and v* tags, rejecting anything else.
# 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).
# #
# Required secrets (repo → Settings → Secrets → Actions): # Required secrets (repo → Settings → Secrets → Actions):
# REGISTRY_USER — your Forgejo username # REGISTRY_USER — your Forgejo username
@@ -29,7 +33,7 @@ name: CI & Build
on: on:
push: push:
branches: [dev] branches: [dev, main]
tags: ["v*"] tags: ["v*"]
paths: paths:
- "src/**" - "src/**"
@@ -67,8 +71,8 @@ env:
jobs: jobs:
typecheck: typecheck:
name: TypeScript typecheck name: TypeScript typecheck
# Skip on main merge-commit pushes — see workflow header comment. # Gate dev, main, and v* tags; reject any other ref (see header note).
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 runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -92,7 +96,7 @@ jobs:
lint: lint:
name: Python 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 runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -106,7 +110,7 @@ jobs:
test: test:
name: Python tests 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 runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -138,11 +142,9 @@ jobs:
build: build:
name: Build & push image name: Build & push image
needs: [typecheck, lint, test] needs: [typecheck, lint, test]
# Build on dev branch pushes and version tag pushes only. # Build on dev, main, and v* tag pushes. dev → :dev, main → (sha only),
# Mirrors the ref guard on the gate jobs above — main merge-commit # tag → :latest + :<version>; every build also gets an immutable :<sha>.
# pushes skip here too, so no production image is ever built from a if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
# raw main push (only from the v* tag the release creates).
if: github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci runs-on: python-ci
container: container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14 image: git.fabledsword.com/bvandeusen/ci-python:3.14
@@ -168,6 +170,11 @@ jobs:
refs/heads/dev) refs/heads/dev)
TAGS="$TAGS,${{ env.IMAGE }}: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/*) refs/tags/*)
TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}" TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}"
BUILD_VERSION="${{ github.ref_name }}" BUILD_VERSION="${{ github.ref_name }}"
+13 -1
View File
@@ -36,7 +36,8 @@ Mechanics:
- Tags are plain strings (no `#` prefix). Empty list clears tags; omit to leave - Tags are plain strings (no `#` prefix). Empty list clears tags; omit to leave
unchanged on updates. unchanged on updates.
- For optional integer FKs (project_id, milestone_id, parent_id), use 0 to mean - 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: 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 - When you begin working a task, set it to in_progress (update_task
@@ -88,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 the land before mutating. Don't call get_project + get_applicable_rules + a
search separately when enter_project already composes them. 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. 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 — When you begin non-trivial work, call start_planning(project_id, title) FIRST —
before any brainstorming, design, or plan-writing skill runs. start_planning before any brainstorming, design, or plan-writing skill runs. start_planning
+13 -5
View File
@@ -14,7 +14,7 @@ Sentinels (preserved from existing fable-mcp):
what makes a Note a Task) what makes a Note a Task)
- priority="none" sets explicit no-priority; priority="" is "leave unchanged" - priority="none" sets explicit no-priority; priority="" is "leave unchanged"
- project_id=0 / milestone_id=0 / parent_id=0 → "no association" on create, - 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 from __future__ import annotations
@@ -147,8 +147,10 @@ async def update_task(
the lifecycle: set in_progress when you start, done when complete — the lifecycle: set in_progress when you start, done when complete —
don't leave finished work at todo. don't leave finished work at todo.
priority: New priority — one of: none, low, medium, high. priority: New priority — one of: none, low, medium, high.
project_id: New project. Omit (0) to leave unchanged. project_id: New project. 0 = leave unchanged, -1 = clear (remove from
milestone_id: New milestone. Omit (0) to leave unchanged. 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() uid = current_user_id()
fields: dict = {} fields: dict = {}
@@ -160,9 +162,15 @@ async def update_task(
fields["status"] = status fields["status"] = status
if priority: if priority:
fields["priority"] = 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 fields["project_id"] = project_id
if milestone_id: if milestone_id == -1:
fields["milestone_id"] = None
elif milestone_id:
fields["milestone_id"] = milestone_id fields["milestone_id"] = milestone_id
note = await notes_svc.update_note(uid, task_id, **fields) note = await notes_svc.update_note(uid, task_id, **fields)
if note is None: 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") 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 @pytest.mark.asyncio
async def test_add_task_log_returns_log_dict(): async def test_add_task_log_returns_log_dict():
log = MagicMock() log = MagicMock()