refactor(systems): one seam — the Systems question rides every read and write of a record
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 12s
CI & Build / Python tests (push) Failing after 29s
CI & Build / Build & push image (push) Skipped

Scribe issue #2570, from the operator's challenge: the invariant is
"always be asking whether what you're touching is a System's territory
and whether the work is filed there" — not a nudge in one corner. The
prior shape failed it twice: the hint fired only on creates (with a
special-cased zero-Systems branch), and get_task/get_note returned
records WITHOUT their Systems, so the read-side reflex had nothing to
fire on (same per-kind asymmetry as #2481).

- attach_systems(): single helper used by get/create/update for tasks,
  notes, and snippets, plus add_task_log. Tagged records always show
  `systems`; an untagged project record carries the `systems_hint`
  question instead. Neither field attaches empty (#2483). Hint is
  owner-only; everything fail-open (#2109).
- untagged_systems_hint unified to ONE question — the vocabulary
  listing varies, the question doesn't; the zero-Systems branch stops
  being special text.
- Docstrings state the uniform contract; floor prose now names the
  read-side reflex (systems visible -> list_system_records the pile).
- Plugin 0.1.27 -> 0.1.28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 15:48:41 -04:00
co-authored by Claude Fable 5
parent 56b1952de5
commit 3455f9cb9a
7 changed files with 169 additions and 86 deletions
+52 -24
View File
@@ -17,40 +17,68 @@ from scribe.services import systems as systems_svc
async def untagged_systems_hint(user_id: int, project_id: int) -> str | None:
"""Nudge text for a record created untagged in a project that has Systems.
"""The Systems question, for an untagged project record.
Not a tool. create_task / create_note / create_snippet attach this to
their responses so the tagging question arrives in-band at the exact write
it applies to — instruction prose alone demonstrably doesn't fire at write
time, while in-band behavior (the duplicate gate) does (#2562).
Not a tool — attach_systems() rides this on tool responses so the question
arrives in-band at the exact moment a record is touched. It is ONE
question regardless of vocabulary state ("which area is this about, and is
it modelled?"); only the vocabulary listing varies, because an empty
vocabulary is not an exemption — it is the question at its most urgent
(#2562, #2569). Instruction prose alone demonstrably doesn't fire at write
time; in-band behavior (the duplicate gate) does.
"""
# Fail-open like the dedup gate: a hint must never break a create.
# Fail-open like the dedup gate: a hint must never break the call.
try:
systems = await systems_svc.list_systems(user_id, project_id)
except Exception:
return None
if not systems:
# Zero Systems is the one state nothing else nudges out of: the hint
# below needs a vocabulary to name, so without this branch the FIRST
# create_system depends entirely on prose that demonstrably doesn't
# fire at write time (#2562).
return (
"Created untagged — this project has no Systems yet. If this "
"record is about a code subsystem/area, create_system it (name + "
"a one-paragraph charter) and tag the record. An audit or sweep "
"that names areas is exactly the moment to mint them; the "
"duplicate gate is what guards against sprawl, not holding back."
)
names = ", ".join(f"#{s.id} {s.name}" for s in systems)
if systems:
vocab = "Existing Systems: " + ", ".join(
f"#{s.id} {s.name}" for s in systems
) + "."
else:
vocab = "This project has no Systems yet."
return (
f"Created untagged. This project's Systems: {names}. If this record is "
"about one or more of those areas, tag it (update it with "
"system_ids=[...]) — cross-cutting records like audits take several; "
"if an area it names is missing, create_system it and tag; only leave "
"it untagged if it is about no particular area."
"This record is untagged — which area(s) of the project is it about? "
f"{vocab} Tag it (system_ids=[...]) — cross-cutting records like "
"audits take several; create_system any area it concerns that isn't "
"modelled yet (a sweep that names areas is the moment to mint them, "
"and the duplicate gate guards against sprawl). Leave it untagged "
"only if it is genuinely about no particular area."
)
async def attach_systems(
caller_id: int,
owner_id: int,
data: dict,
note_id: int,
project_id: int | None,
) -> None:
"""Attach a record's Systems to its payload — or the Systems question.
ONE seam for every tool that returns a project record, read or write: a
tagged record shows its areas (the touching-a-System reflex needs the
affiliation visible on read, not just settable on write), an untagged
project record carries the question instead. Neither field is ever
attached empty (same reasoning as notes._attach_supersession / #2483 — a
field that always says nothing trains readers to skip fields). The hint
goes only to the record's owner: tagging someone else's record in someone
else's project is not the caller's call to make. Fail-open — decoration
must never break the call it rides on.
"""
try:
systems = await systems_svc.list_record_systems(owner_id, note_id)
if systems:
data["systems"] = [s.to_dict() for s in systems]
elif project_id and caller_id == owner_id:
hint = await untagged_systems_hint(owner_id, project_id)
if hint:
data["systems_hint"] = hint
except Exception:
pass
async def create_system(
project_id: int,
name: str,