feat(409): the reporting reflex reaches every surface a session reads (#4012)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 53s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m28s
CI & Build / Build & push image (push) Successful in 31s

Step 3 of milestone 409 "Response shapes". Step 2's reporting-back skill
only helps if it fires, and only exists in the Claude Code plugin.

- scribe_static_context.md and using-scribe (new reflex 11) say: report back
  in a shape the operator can read, placed from the `placement` block rather
  than memory, and point at the reporting-back skill. using-scribe also lists
  it among the sibling process-skills.
- update_task returns a one-line `report_back` cue when a task is closed
  (done or cancelled). A tool response is the only surface every MCP client
  sees, at the moment the report is about to be written.
- _INSTRUCTIONS takes no line: there is no budget without trading out a
  session-start reflex. The decision is recorded in server.py's comment
  block so it is not re-litigated blind.
- Guards: test_instruction_surfaces_agree pins the reflex and the placement
  pointer on both plugin surfaces; a tool test pins the cue on closing
  statuses and its absence on every other update.

Plugin version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-14 10:11:58 -04:00
co-authored by Claude Opus 5
parent b4dbc495fe
commit 7239e3c479
7 changed files with 112 additions and 4 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "scribe", "name": "scribe",
"description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your active-project context, process-skills (writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.", "description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your active-project context, process-skills (writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.",
"version": "2026.09.14.1406", "version": "2026.09.14.1411",
"author": { "author": {
"name": "Bryan Van Deusen" "name": "Bryan Van Deusen"
}, },
+6
View File
@@ -55,6 +55,12 @@ for the operator's work, and as your own working memory across sessions.
moment it's complete. When you **fix** something — even in passing — record it moment it's complete. When you **fix** something — even in passing — record it
as its own issue (`create_task(kind="issue")`), not as a work-log line on an as its own issue (`create_task(kind="issue")`), not as a work-log line on an
unrelated open task. unrelated open task.
- **Report back in a shape the operator can read** — they were not there while
you worked, so organise each reply around where the work stands rather than
the order you did things in: which task or milestone it belongs to, what now
works, what needs them, what comes next. Take the placement from the
`placement` block task writes return, not from memory. The `reporting-back`
skill holds the shape for each kind of reply.
- **Tag to Systems as you write** — `enter_project` lists the project's - **Tag to Systems as you write** — `enter_project` lists the project's
Systems (its named subsystems/areas). When you create or meaningfully update Systems (its named subsystems/areas). When you create or meaningfully update
a record, ask which areas it is about and pass `system_ids`; if an area has a record, ask which areas it is about and pass `system_ids`; if an area has
+11 -2
View File
@@ -220,6 +220,15 @@ Two constraints on *how* that's achieved:
either: `verify_snippet` compares the recorded location and code against the either: `verify_snippet` compares the recorded location and code against the
repo, which is richer and already wired to drift detection. repo, which is richer and already wired to drift detection.
11. **Report back in a shape the operator can read.** They were not there while
you worked, so organise the reply around **where the work stands**, not the
order you did things in: which task or milestone it belongs to, what now
works, what needs them, and what comes next. Take the placement from the
`placement` block that `create_task` / `update_task` return — the milestone,
step N of M, the next open step — rather than from memory. The
`reporting-back` skill holds the shape for each kind of reply: completions,
findings, decisions, handoffs, "where are we".
## Stay inside the active project's scope ## Stay inside the active project's scope
Once a project is in scope — you called `enter_project`, or the working repo is Once a project is in scope — you called `enter_project`, or the working repo is
@@ -300,6 +309,6 @@ nothing will tell you it drifted.
## Other Scribe process-skills ## Other Scribe process-skills
This plugin also ships focused process-skills — writing-plans, systematic This plugin also ships focused process-skills — writing-plans, reporting-back,
debugging, verification, and brainstorming. Reach for the matching one when its systematic debugging, verification, and brainstorming. Reach for the matching one when its
situation arises, the same way you reach for this skill. situation arises, the same way you reach for this skill.
+7
View File
@@ -34,6 +34,13 @@ from quart import Quart
# competing for the last ~68 characters, so an addition here is a trade, never # competing for the last ~68 characters, so an addition here is a trade, never
# an append. # an append.
# #
# Milestone 409 step 3 (reporting back to the operator in a readable shape)
# took NO line here, deliberately: there is no room without trading out a
# session-start reflex, and the moment it applies is when a task closes. So it
# rides in-band instead — update_task returns `placement` and a one-line
# `report_back` cue on done/cancelled, which every MCP client sees — with the
# full shapes in the reporting-back skill and the static context.
#
# Milestone 317 (a note's own verify_with / expires_when, and the sweep over # Milestone 317 (a note's own verify_with / expires_when, and the sweep over
# them) was DECLINED a line, deliberately, by the operator — not overlooked. # them) was DECLINED a line, deliberately, by the operator — not overlooked.
# The reasoning, so it is not re-litigated blind: this is a map, and its own # The reasoning, so it is not re-litigated blind: this is a map, and its own
+18 -1
View File
@@ -271,6 +271,9 @@ async def update_task(
work sits and what comes next — read them from here rather than work sits and what comes next — read them from here rather than
reconstructing them, because a remembered milestone title or "next step" reconstructing them, because a remembered milestone title or "next step"
reads exactly like a real one when it is wrong. reads exactly like a real one when it is wrong.
Closing a task (done or cancelled) also returns `report_back`: a one-line
reminder of what the reply to the operator should cover.
""" """
uid = current_user_id() uid = current_user_id()
fields: dict = {} fields: dict = {}
@@ -308,7 +311,10 @@ async def update_task(
await systems_tools.attach_systems( await systems_tools.attach_systems(
uid, getattr(note, "user_id", uid) or uid, data, task_id, note.project_id uid, getattr(note, "user_id", uid) or uid, data, task_id, note.project_id
) )
return await placement_svc.attach_placement(uid, data, note) await placement_svc.attach_placement(uid, data, note)
if status in _CLOSING_STATUSES:
data["report_back"] = REPORT_BACK_CUE
return data
async def add_task_log(task_id: int, content: str) -> dict: async def add_task_log(task_id: int, content: str) -> dict:
@@ -344,6 +350,17 @@ async def add_task_log(task_id: int, content: str) -> dict:
return data return data
# The in-band half of milestone 409 step 3. The reporting-back skill and the
# static context carry the full shapes, but both live only in the Claude Code
# plugin; a tool response reaches every MCP client, at the moment a piece of
# work closes, which is exactly when the report is about to be written. One
# line on purpose: a template here would be read as the reply itself.
_CLOSING_STATUSES = ("done", "cancelled")
REPORT_BACK_CUE = (
"Reporting this to the operator? Say where it sits (from `placement`), "
"what now works, what needs them, and what comes next."
)
_ITEM_KEYS = {"title", "body", "type", "status", "priority", "kind", "tags", "system_ids"} _ITEM_KEYS = {"title", "body", "type", "status", "priority", "kind", "tags", "system_ids"}
+36
View File
@@ -322,3 +322,39 @@ def test_a_surface_claiming_rules_bind_also_names_what_does_not():
f"'how the operator likes this done' as something it may not proceed " f"'how the operator likes this done' as something it may not proceed "
f"past. Name the other kind, however briefly." f"past. Name the other kind, however briefly."
) )
# ── Reporting back (milestone 409 step 3) ──────────────────────────────
#
# The reporting-back skill carries the shapes, but a skill only helps if it
# fires. The reflex that sends a session to it lives on the two plugin
# surfaces a session always reads; the in-band cue on update_task is the half
# that reaches clients with no plugin at all. _INSTRUCTIONS took no line, on
# purpose — server.py's comment block records why.
REPORT_REFLEX = "report back in a shape the operator can read"
REPORT_SURFACES = (
ROOT / "plugin" / "hooks" / "scribe_static_context.md",
ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md",
)
def test_the_reporting_reflex_reaches_every_plugin_surface():
missing = []
for path in REPORT_SURFACES:
text = " ".join(path.read_text().split()).lower()
if REPORT_REFLEX not in text or "reporting-back" not in text:
missing.append(str(path.relative_to(ROOT)))
assert not missing, (
f"these surfaces no longer send a session to the reporting-back skill "
f"({REPORT_REFLEX!r} plus the skill's name): {missing}. Without the "
f"reflex the skill loads only when its description happens to match."
)
def test_the_reporting_reflex_takes_placement_from_the_record():
"""The failure milestone 409 began from: a placement reconstructed from
memory reads exactly like a real one when it is wrong."""
for path in REPORT_SURFACES:
text = " ".join(path.read_text().split())
assert "`placement`" in text, f"{path.relative_to(ROOT)} no longer points at the placement block"
+33
View File
@@ -0,0 +1,33 @@
"""update_task closes a task with a one-line reminder of what the report needs.
The in-band half of milestone 409 step 3: the skill and static context only
exist in the Claude Code plugin, and a tool response reaches every MCP client
at the moment a piece of work closes. Pinned on the response, not the wording.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
pytestmark = pytest.mark.usefixtures("_bind_user")
async def _update(**kwargs):
from scribe.mcp.tools.tasks import update_task
note = MagicMock(id=5, user_id=7, project_id=None)
note.to_dict.return_value = {"id": 5}
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", AsyncMock(return_value=note)), \
patch("scribe.mcp.tools.tasks.systems_tools.attach_systems", AsyncMock()), \
patch("scribe.mcp.tools.tasks.placement_svc.attach_placement", AsyncMock()):
return await update_task(task_id=5, **kwargs)
@pytest.mark.parametrize("status", ["done", "cancelled"])
async def test_closing_a_task_carries_the_cue(status):
out = await _update(status=status)
assert "placement" in out["report_back"] and "needs them" in out["report_back"]
@pytest.mark.parametrize("kwargs", [{"status": "in_progress"}, {"status": "todo"}, {"body": "more notes"}])
async def test_other_updates_do_not(kwargs):
assert "report_back" not in await _update(**kwargs)