feat(tasks): the hand-off — SessionEnd releases a session's claims; the practice is written down (milestone 381 step 4)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 59s
CI & Build / Python tests (push) Successful in 1m40s
CI & Build / Build & push image (push) Successful in 24s

Release is the mechanical half: scribe_session_end.sh sends the ending
session's id to /api/plugin/release-session, which releases the claims it
held. A tidy-up, not the guarantee (no SessionEnd on a crash; the lease
covers that), and skipped on /clear so SessionStart(clear) can still
hand the claimed work back.

Saying what happened is the half only the model can do. It is stated as a
practice where it is read: the using-scribe skill owns it ("Hand off before
this session's context stops existing", pinned in test_guidance_ownership),
the static context points at it for the wrap-up moment, and add_task_log's
docstring says a log claims the task. _INSTRUCTIONS is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 06:43:14 -04:00
co-authored by Claude Opus 5.5
parent e46eea3b52
commit 952e56ee75
11 changed files with 161 additions and 1 deletions
+5
View File
@@ -155,6 +155,11 @@ TOPICS: tuple[Topic, ...] = (
Topic("a retrieved rule outranks a default habit", U, ("outranks a default habit",),
"a retrieved rule outranks a default habit"),
Topic("log on completion and on a problem", U, ("hit or discover a problem",), "hit or discover a problem"),
# Milestone 381 step 4: the half of a hand-off only the model can do. The
# release is mechanical (SessionEnd hook); saying what happened is not.
Topic("hand off before the context stops existing", U,
("hand off", "claims it for this session"),
"write down what the next session needs"),
Topic("the project's design system binds ui", U, ("resolve_design_system",),
"building ui: the project's design system binds", index=("resolve_design_system",)),
Topic("name the record, never just its number", U, ("name the record",),
+27
View File
@@ -256,3 +256,30 @@ async def test_session_start_after_a_compaction_carries_the_claimed_task(users):
owner, source="compact", session_id="sess-compact"))["context"]
assert "claimed then compacted" in ctx
assert "the migration is written" in ctx
# --- step 4: the hand-off ----------------------------------------------------
def test_session_end_releases_claims_but_not_on_clear():
"""The mechanical half of the hand-off. A /clear keeps the claim, because
SessionStart(source=clear) pushes the claimed work straight back."""
hooks = json.loads((ROOT / "plugin/hooks/hooks.json").read_text())["hooks"]
commands = [h["command"] for b in hooks.get("SessionEnd", []) for h in b["hooks"]]
assert any("scribe_session_end.sh" in c for c in commands)
text = (ROOT / "plugin/hooks/scribe_session_end.sh").read_text()
assert '[ "$reason" = "clear" ] && exit 0' in text
assert "/api/plugin/release-session" in text
@pytest.mark.integration
async def test_ending_a_session_releases_only_that_sessions_claims(users):
owner, _ = users
kept = await notes_svc.create_note(owner, title="other session's", status="in_progress")
gone = await notes_svc.create_note(owner, title="ending session's", status="in_progress")
await tc.bind_session(owner, kept.id, "sess-stays")
await tc.bind_session(owner, gone.id, "sess-ends")
assert await tc.release_session(owner, "sess-ends") >= 1
assert (await notes_svc.get_note(owner, gone.id)).claimed_at is None
assert (await notes_svc.get_note(owner, kept.id)).claim_session == "sess-stays"
assert await tc.release_session(owner, "") == 0