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
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:
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user