docs(6204): shipped instruction text describes the product, not this instance's records
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m10s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 39s

MCP tool docstrings and comments, the server's orientation notes, the plugin
skills, every hook script and the plugin README/PACKAGING drop record ids,
milestone numbers, dates and incident stories, and state each non-obvious why
as a present-tense fact. History stays in the task logs.

Corrections found on the way:
- plugin README/PACKAGING said every hook is a GET; moment, rule-opened and
  reply-rules POST, so the plugin's key needs write scope. Both now say so,
  name Settings -> MCP Access, and list every wired hook and endpoint.
- the contract-change hint printed "(lesson #4207)", this instance's record,
  to every install. It no longer cites a record.
- tasks.py claimed there is no delete_task; there is.
- rulebooks.py cited delete_event and a design spec that no longer exist.
- scribe_after_write.sh carried an always-empty etag_q; removed.
- test_write_path_trigger pinned "| tac | scribe_defs | head -1", text that
  existed only in a comment about the old code; it now pins the real tail -1
  pipeline.
- docs/development.md: CI runs on Gitea Actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 19:45:09 -04:00
co-authored by Claude Opus 5.5
parent fc8f554cee
commit 17777e9bb1
43 changed files with 641 additions and 777 deletions
+16 -23
View File
@@ -4,15 +4,9 @@ Tasks are notes with a non-null `status` — same model, different filter.
Wrappers call services/notes.py for CRUD with is_task=True and add the
task-specific fields (status, priority, due_date, parent_id), plus
services/task_logs.py for add_task_log AND for reading those logs back —
`get_task` returns them and `list_tasks` counts them. For most of this
module's life `add_task_log` wrote to a surface no agent could read: the
entries reached the web UI and nothing else, so a session opening a task
saw only the body — a claim written before the work — with the record
written during it invisible beside it. A stale body then had nothing to
contradict it, and shipped work got rebuilt (#4241).
There is no delete_task — matches the existing fable-mcp surface.
Cancel by updating status to "cancelled".
`get_task` returns them and `list_tasks` counts them. The log is the record
written during the work; a body is a claim written before it, so a session
opening a task needs both.
Sentinels (preserved from existing fable-mcp):
- status="" / priority="" / title="" / body="" → "leave unchanged" on update
@@ -181,8 +175,8 @@ async def get_task(
written once, at the start, when the least is known; the log is written
during the work and after it. A task whose body says "not started" and
whose log records a partial ship is not a contradiction to resolve — the
log is simply later. This block exists because its absence cost a session
a day of rebuilding work that had already shipped (#4241).
log is simply later; trusting the body alone is how finished work gets
rebuilt.
The NEWEST entry arrives whole (to 4000 characters); older ones are
shortened to a headline. Anything shortened is cut from the MIDDLE, so
@@ -228,10 +222,9 @@ async def get_task(
# Same reasoning as get_note's record_pulled, and this is the tool where it
# matters MOST: auto-inject ranks kind-blind over a corpus that is
# overwhelmingly tasks and issues, so tasks dominate what it surfaces. Without
# this the surfaced→pulled loop was open exactly where the volume is — every
# surfaced task counted as never-pulled because the tool that opens one didn't
# say so, driving auto-inject's measured pull-through toward zero for its own
# dominant kind. #1038 and #2085 are explicitly gated on that number (#2245).
# this every surfaced task would count as never-pulled, driving
# auto-inject's measured pull-through toward zero for its own dominant
# kind.
await systems_tools.attach_systems(
uid, getattr(note, "user_id", uid) or uid, data, note.id, note.project_id
)
@@ -365,7 +358,7 @@ async def create_task(
await systems_tools.attach_systems(uid, uid, data, note.id, project_id or None)
data.update(dedup_svc.note_overlap_response(overlaps, "task"))
# A task that says it is "matching" another project's work names the
# source of a pattern — family canon's citation trigger (milestone 463).
# source of a pattern — family canon's citation trigger.
await family_svc.attach_family_hint(uid, data, note, created=True)
return await placement_svc.attach_placement(uid, data, note)
@@ -467,8 +460,8 @@ async def update_task(
await family_svc.attach_family_hint(uid, data, note, created=False)
if status in _CLOSING_STATUSES:
data["report_back"] = REPORT_BACK_CUE
# Owed family adoptions filed while the task was open (milestone 463
# step 6) — work now waiting in some project, which the report names.
# Owed family adoptions filed while the task was open — work now
# waiting in some project, which the report names.
await family_adoption_svc.attach_owed_adoptions(uid, data, note)
return await moment_delivery.attach_moment_rules(
uid, "update_task", {"status": status, "project_id": project_id}, data,
@@ -490,7 +483,7 @@ async def add_task_log(task_id: int, content: str) -> dict:
out, and where it actually stands.
A log on an open task also marks it as being worked by this session — its
`claim` (milestone 381). That is what brings the task and its newest
`claim`. That is what brings the task and its newest
entries back to you after a compaction; it lapses by itself once the
session stops writing, and a status of done, cancelled or todo clears it.
@@ -520,8 +513,8 @@ async def add_task_log(task_id: int, content: str) -> dict:
return data
# The in-band half of milestone 409 step 3. The completion shape itself rides
# the closing response as `reply_shape` (milestone 500); this is the one line
# The in-band completion cue. The completion shape itself rides the closing
# response as `reply_shape`; this is the one line
# that says what the reply covers, for every MCP client, at the moment a piece
# of work closes. One line on purpose: a template here would be read as the
# reply itself.
@@ -537,7 +530,7 @@ _ITEM_KEYS = {"title", "body", "type", "status", "priority", "kind", "tags", "sy
def _batch_items(records: list[dict], *, what: str = "record") -> list[batch_svc.BatchItem]:
"""Parse the door's plain dicts into BatchItems, refusing unknown keys.
Strict for the same reason StrictArgsMCPServer is (#2709): a misspelt key
Strict for the same reason StrictArgsMCPServer is: a misspelt key
silently dropped — `milestone` for a per-record milestone, `desc` for body —
creates a record that looks right and is missing what the caller sent.
"""
@@ -574,7 +567,7 @@ async def _first_duplicate(
"""The duplicate gate over a whole batch — the first hit blocks all of it.
`overlaps`, when given, collects each record's near matches below the copy
band by its 1-based position (#4306), for the reply once the batch is
band by its 1-based position, for the reply once the batch is
created."""
for i, item in enumerate(items, start=1):
found: list = []