Compare commits
2
Commits
8c9ca45479
...
f446573c3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f446573c3d | ||
|
|
82d6812c7f |
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user