Skip to content

Commit b12a9ab

Browse files
jgravelleclaude
andcommitted
release: v1.108.162 — canonical handoff contract: finalize_handoff + munch://handoff/<id> (#374)
Server-owned canonical final handoff (jcodemunch.handoff/v1). The assistant authors the analysis; the server deterministically assembles caller-supplied sections + appendices, validates every evidence_ref against the session's real retrieval record (yield-tracker served ids; unknown refs fail closed with isError), persists session-scoped, and serves the immutable body via the munch://handoff/<id> resource with byte-identical repeated reads. canonical:true is advisory metadata only. Standard tier, core_compact unchanged; tool count 90 -> 91. Counter: STATE_CHANGING_ACTIONS + EXAMPLES entry. Requested by @mightydanp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c2dd386 commit b12a9ab

11 files changed

Lines changed: 639 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.162] - 2026-07-23 - Canonical handoff contract: finalize_handoff + munch://handoff/<id> (#374)
6+
7+
### Added
8+
- **New tool `finalize_handoff`** (`jcodemunch.handoff/v1`, requested by
9+
@mightydanp in #374). Ends a multi-step repository audit with one
10+
authoritative, server-owned Markdown handoff — no client-specific Stop hook
11+
required. The assistant authors the analysis; the server owns everything
12+
downstream: deterministic assembly of caller-supplied `sections` (+ optional
13+
named `appendices`, each included exactly once, duplicates rejected),
14+
session-scoped persistence, identity, SHA-256 hashing, and immutable
15+
serving. Same inputs produce a byte-identical body, the same `handoff_id`,
16+
and the same hash. No character limit; never writes to the repository.
17+
- **Evidence attestation — the part a Stop hook can't do.** `evidence_refs`
18+
are validated against the session's actual retrieval record (the yield
19+
tracker's served-symbol ids from `search_symbols` / `get_ranked_context`;
20+
a ref may be a served symbol id or its file path). A finalized handoff
21+
therefore attests that every reference it cites corresponds to something
22+
this server really served this session. Unknown or contradicted refs fail
23+
closed with `CallToolResult(isError=True)` and an `unknown_refs` list.
24+
- **New resource `munch://handoff/<id>`** serves the exact canonical body
25+
(`text/markdown`); repeated reads are byte-identical, unknown ids error.
26+
Finalized handoffs are advertised via `list_resources()` alongside the
27+
#371 runtime-identity resource.
28+
- The success receipt (`{schema, handoff_id, resource_uri, sha256, length,
29+
canonical: true, evidence_count, appendices}`) carries `canonical: true`
30+
as advisory metadata only — supporting clients can render the resource
31+
directly; nothing is forced on hosts without that capability.
32+
- Counter coverage: `finalize_handoff` joins `STATE_CHANGING_ACTIONS` (order
33+
requires `allow_state_change=true`; annotated `readOnlyHint: false`) and
34+
gets a curated `EXAMPLES` entry for `menu` / `route` discovery.
35+
- Standard tier — `core_compact` schema budget unchanged. Tool count 90 → 91
36+
(full surface). Tests `tests/test_v1_108_162.py` (22).
37+
538
## [1.108.161] - 2026-07-23 - BM25 tokenizer: Unicode word splitting + CJK character bigrams
639

740
### Fixed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@ The server exposes one MCP resource, `munch://runtime/identity` — a read-only
393393

394394
---
395395

396+
## Canonical handoff (`finalize_handoff` + `munch://handoff/<id>`)
397+
398+
A multi-step repository audit can end with one authoritative, server-attested result instead of a client-specific Stop hook. The assistant authors the analysis; `finalize_handoff` takes those sections plus `evidence_refs`, validates every reference against what this session **actually retrieved** (symbol ids or file paths served by `search_symbols` / `get_ranked_context` — unknown refs fail closed with `isError`), deterministically assembles one canonical Markdown handoff (`jcodemunch.handoff/v1`), and returns a compact receipt: `{handoff_id, resource_uri, sha256, length, canonical: true}`. The immutable body is served by the `munch://handoff/<id>` resource — repeated reads are byte-identical. Session-scoped, in-memory, never writes to your repository; appendices appear exactly once; no character limit. `canonical: true` is advisory metadata for clients that support rendering an authoritative MCP resource directly. The server assembles and attests — it never authors conclusions.
399+
400+
---
401+
396402
## Start fast
397403

398404
> **Ubuntu 24.04+ / Debian 12+:** System Python is externally managed (PEP 668).

benchmarks/schema_baseline.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"core_compact": 3969,
3-
"core_full": 5331,
4-
"standard_compact": 18310,
5-
"standard_full": 19909,
6-
"full_compact": 19620,
7-
"full_full": 21219
2+
"core_compact": 3996,
3+
"core_full": 5419,
4+
"standard_compact": 18752,
5+
"standard_full": 20412,
6+
"full_compact": 20062,
7+
"full_full": 21722
88
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.161"
3+
version = "1.108.162"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/jcodemunch_mcp/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ def apply_adaptive_languages(source_root: str, detected: set[str]) -> bool:
384384
"get_architecture_metrics", "render_diagram",
385385
"get_project_intel", "list_workspaces", "invalidate_cache", "get_watch_status",
386386
"analyze_perf", "tune_weights", "check_embedding_drift", "suggest_corrections",
387-
"digest", "diff_health_radar", "get_file_risk",
387+
"digest", "finalize_handoff", "diff_health_radar", "get_file_risk",
388388
"import_runtime_signal", "get_runtime_coverage",
389389
"find_hot_paths", "find_unused_paths", "get_redaction_log",
390390
],
@@ -1805,6 +1805,7 @@ def generate_template() -> str:
18051805
"diff_health_radar",
18061806
"digest",
18071807
"embed_repo",
1808+
"finalize_handoff",
18081809
"find_dead_code",
18091810
"find_implementations",
18101811
"find_hot_paths",

src/jcodemunch_mcp/counter.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"invalidate_cache", "register_edit", "tune_weights",
4848
"set_tool_tier", "announce_model", "embed_repo",
4949
"import_runtime_signal", "summarize_repo",
50+
"finalize_handoff", # persists a session handoff record (#374)
5051
})
5152

5253
# Forward-looking tripwire. ``order`` refuses to dispatch any action whose name
@@ -163,6 +164,12 @@ def _required_args(schema: dict) -> list[str]:
163164
"get_repo_map": {"repo": "owner/name", "token_budget": 4000},
164165
"get_file_tree": {"repo": "owner/name", "path_prefix": "src/"},
165166
"digest": {"repo": "owner/name"},
167+
"finalize_handoff": {
168+
"repo": "owner/name",
169+
"task": "Audit the authentication surface",
170+
"sections": [{"heading": "Findings", "content": "…markdown authored by the assistant…"}],
171+
"evidence_refs": ["src/auth.py::login#function"],
172+
},
166173
# relationships / impact
167174
"find_importers": {"repo": "owner/name", "file_path": "src/app.py"},
168175
"find_references": {"repo": "owner/name", "identifier": "parse_config"},

src/jcodemunch_mcp/handoff.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
"""Server-owned canonical handoff contract (``jcodemunch.handoff/v1``, #374).
2+
3+
A multi-step repository audit ends with one authoritative Markdown result.
4+
The assistant authors the analysis (the server never writes conclusions);
5+
this module owns everything downstream of authorship: deterministic assembly,
6+
evidence attestation, persistence, identity, hashing, and immutable serving
7+
via the ``munch://handoff/<id>`` resource.
8+
9+
The differentiator no client Stop hook can provide: ``evidence_refs`` are
10+
validated against the session's actual retrieval record (the yield tracker's
11+
served-symbol ids) at finalization time, so a finalized handoff attests that
12+
every evidence reference it cites corresponds to something actually served
13+
by this server in this session. Unknown refs fail closed.
14+
15+
Charter constraints (accepted scope, issue #374):
16+
- Deterministic: same repo/task/profile/sections/evidence/appendices ->
17+
byte-identical body, same id, same sha256.
18+
- Each appendix exactly once; duplicate names rejected.
19+
- No arbitrary character limit.
20+
- Session-scoped (process == session, the _steer_state precedent), local-first,
21+
in-memory only — never writes to the user's repository or index store.
22+
- ``canonical: true`` in the receipt is advisory metadata for direct-render
23+
clients; it forces nothing.
24+
"""
25+
26+
from __future__ import annotations
27+
28+
import hashlib
29+
import threading
30+
from typing import Iterable, Optional
31+
32+
HANDOFF_SCHEMA = "jcodemunch.handoff/v1"
33+
HANDOFF_URI_PREFIX = "munch://handoff/"
34+
HANDOFF_CONTENT_TYPE = "text/markdown"
35+
36+
# Session-scoped store: handoff_id -> {"body": str, "receipt": dict}.
37+
# Process lifetime == MCP session lifetime (same precedent as server._steer_state);
38+
# process exit is the cleanup.
39+
_lock = threading.Lock()
40+
_handoffs: dict[str, dict] = {}
41+
42+
43+
def _norm_file(path: str) -> str:
44+
return path.replace("\\", "/").lstrip("./")
45+
46+
47+
def _validate_evidence(refs, served_ids: Iterable[str]):
48+
"""Attest each ref against the session retrieval record.
49+
50+
A ref is attested when it is exactly a served symbol id, or the file
51+
component (before ``::``) of a served id — a file-level citation of
52+
retrieved code. Returns (ordered_unique_refs, unknown_refs).
53+
"""
54+
served = set(served_ids or ())
55+
served_files = {_norm_file(sid.split("::", 1)[0]) for sid in served}
56+
seen: set[str] = set()
57+
ordered: list[str] = []
58+
unknown: list[str] = []
59+
for ref in refs:
60+
if not isinstance(ref, str) or not ref.strip():
61+
unknown.append(repr(ref))
62+
continue
63+
ref = ref.strip()
64+
if ref in seen:
65+
continue
66+
seen.add(ref)
67+
ordered.append(ref)
68+
if ref not in served and _norm_file(ref) not in served_files:
69+
unknown.append(ref)
70+
return ordered, unknown
71+
72+
73+
def _validate_sections(sections):
74+
if not isinstance(sections, list) or not sections:
75+
return None, "sections must be a non-empty list of {heading, content} objects"
76+
out = []
77+
for i, sec in enumerate(sections):
78+
if not isinstance(sec, dict):
79+
return None, f"sections[{i}] must be an object with 'heading' and 'content'"
80+
heading = sec.get("heading")
81+
content = sec.get("content")
82+
if not isinstance(heading, str) or not heading.strip():
83+
return None, f"sections[{i}].heading must be a non-empty string"
84+
if not isinstance(content, str) or not content.strip():
85+
return None, f"sections[{i}].content must be a non-empty string"
86+
out.append((heading.strip(), content.rstrip()))
87+
return out, None
88+
89+
90+
def _validate_appendices(appendices):
91+
if appendices is None:
92+
return [], None
93+
if not isinstance(appendices, list):
94+
return None, "appendices must be a list of {name, content} objects"
95+
out = []
96+
names: set[str] = set()
97+
for i, app in enumerate(appendices):
98+
if not isinstance(app, dict):
99+
return None, f"appendices[{i}] must be an object with 'name' and 'content'"
100+
name = app.get("name")
101+
content = app.get("content")
102+
if not isinstance(name, str) or not name.strip():
103+
return None, f"appendices[{i}].name must be a non-empty string"
104+
if not isinstance(content, str) or not content.strip():
105+
return None, f"appendices[{i}].content must be a non-empty string"
106+
name = name.strip()
107+
if name in names:
108+
return None, f"duplicate appendix name: {name!r} (each appendix appears exactly once)"
109+
names.add(name)
110+
ctype = app.get("content_type") or "text/markdown"
111+
out.append((name, str(ctype), content.rstrip()))
112+
return out, None
113+
114+
115+
def render_handoff(repo: str, task: str, profile: str, sections, evidence_refs, appendices) -> str:
116+
"""Deterministic canonical Markdown. No timestamps, no randomness."""
117+
lines = [
118+
f"# Handoff: {task}",
119+
"",
120+
f"- Schema: {HANDOFF_SCHEMA}",
121+
f"- Repo: {repo}",
122+
f"- Profile: {profile}",
123+
"",
124+
]
125+
for heading, content in sections:
126+
lines += [f"## {heading}", "", content, ""]
127+
lines += [
128+
"## Evidence",
129+
"",
130+
"Every reference below was validated against this session's retrieval",
131+
"record at finalization time (server-attested).",
132+
"",
133+
]
134+
lines += [f"- `{ref}`" for ref in evidence_refs]
135+
lines.append("")
136+
for name, ctype, content in appendices:
137+
lines += [f"## Appendix: {name}", "", f"_Content type: {ctype}_", "", content, ""]
138+
return "\n".join(lines).rstrip() + "\n"
139+
140+
141+
def finalize_handoff(
142+
*,
143+
repo,
144+
task,
145+
sections,
146+
evidence_refs,
147+
profile: str = "general",
148+
appendices=None,
149+
served_ids: Optional[Iterable[str]] = None,
150+
) -> dict:
151+
"""Assemble, attest, persist, and return the compact receipt.
152+
153+
Any validation failure returns ``{"error": ...}`` — the server dispatcher
154+
maps in-band error dicts to ``CallToolResult(isError=True)`` (v1.108.74
155+
contract). The server never authors content: sections/appendices arrive
156+
verbatim from the caller; only assembly and attestation happen here.
157+
"""
158+
if not isinstance(repo, str) or not repo.strip():
159+
return {"error": "repo must be a non-empty string"}
160+
if not isinstance(task, str) or not task.strip():
161+
return {"error": "task must be a non-empty string"}
162+
if not isinstance(profile, str) or not profile.strip():
163+
return {"error": "profile must be a non-empty string"}
164+
sec, err = _validate_sections(sections)
165+
if err:
166+
return {"error": err}
167+
apps, err = _validate_appendices(appendices)
168+
if err:
169+
return {"error": err}
170+
if not isinstance(evidence_refs, list) or not evidence_refs:
171+
return {
172+
"error": (
173+
"evidence_refs must be a non-empty list of session retrieval "
174+
"references (symbol ids or file paths served this session by "
175+
"search_symbols / get_ranked_context)"
176+
)
177+
}
178+
refs, unknown = _validate_evidence(evidence_refs, served_ids or ())
179+
if unknown:
180+
return {
181+
"error": (
182+
"evidence attestation failed: the following refs do not "
183+
"correspond to anything retrieved in this session"
184+
),
185+
"unknown_refs": unknown,
186+
"hint": (
187+
"Evidence refs must be symbol ids (or their file paths) that "
188+
"this session actually served via search_symbols or "
189+
"get_ranked_context. Retrieve the evidence first, then finalize."
190+
),
191+
}
192+
193+
body = render_handoff(repo.strip(), task.strip(), profile.strip(), sec, refs, apps)
194+
raw = body.encode("utf-8")
195+
sha256 = hashlib.sha256(raw).hexdigest()
196+
handoff_id = sha256[:16]
197+
receipt = {
198+
"schema": HANDOFF_SCHEMA,
199+
"handoff_id": handoff_id,
200+
"repo": repo.strip(),
201+
"profile": profile.strip(),
202+
"content_type": HANDOFF_CONTENT_TYPE,
203+
"resource_uri": f"{HANDOFF_URI_PREFIX}{handoff_id}",
204+
"sha256": sha256,
205+
"length": len(raw),
206+
"canonical": True,
207+
"evidence_attested": True,
208+
"evidence_count": len(refs),
209+
"appendices": [name for name, _, _ in apps],
210+
}
211+
with _lock:
212+
_handoffs[handoff_id] = {"body": body, "receipt": receipt}
213+
return dict(receipt)
214+
215+
216+
def get_handoff(handoff_id: str) -> Optional[dict]:
217+
"""Return {"body", "receipt"} for a stored handoff, or None."""
218+
with _lock:
219+
rec = _handoffs.get(handoff_id)
220+
return {"body": rec["body"], "receipt": dict(rec["receipt"])} if rec else None
221+
222+
223+
def handoff_for_uri(uri: str) -> Optional[dict]:
224+
"""Resolve a munch://handoff/<id> URI to its stored record, or None."""
225+
s = str(uri)
226+
if not s.startswith(HANDOFF_URI_PREFIX):
227+
return None
228+
return get_handoff(s[len(HANDOFF_URI_PREFIX):])
229+
230+
231+
def list_handoff_resources() -> list[dict]:
232+
"""Rows for list_resources(): one per finalized handoff this session."""
233+
with _lock:
234+
return [
235+
{
236+
"uri": rec["receipt"]["resource_uri"],
237+
"name": f"handoff-{hid}",
238+
"description": (
239+
f"Canonical handoff for {rec['receipt']['repo']} "
240+
f"({rec['receipt']['profile']}); immutable, "
241+
f"sha256 {rec['receipt']['sha256'][:12]}…"
242+
),
243+
}
244+
for hid, rec in _handoffs.items()
245+
]
246+
247+
248+
def clear_handoffs() -> None:
249+
"""Test hook: drop all session handoffs."""
250+
with _lock:
251+
_handoffs.clear()

0 commit comments

Comments
 (0)