feat(shapes): the write-path hook suggests and no longer stamps (milestone 439 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 53s
CI & Build / integration (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m49s
CI & Build / Build & push image (push) Successful in 27s

The hook used to land its evidence as `instance` rows, classified_by=hook —
a permanent verdict nobody read, and the source of the weak stamps that
poisoned two ledgers (#4608). Now the agent that wrote the code judges it at
the end of the turn, and the hook's evidence is what it is shown.

- shape_ledger.suggest_write_path_instances (was stamp_write_path_instances):
  same evidence and form gate, but it writes a PROPOSAL — proposed_snippet_id,
  basis "reference" (named) or "semantic" (resembles), score — and never a
  status. A judged row is left alone; a new shape gets an unclassified
  provisional row so the suggestion reaches the end-of-turn question as
  "looks like #N". By-name uses edges stay: a fact, not a verdict.
- The prior-art line says "looks like #N … when you judge this turn's
  shapes, say whether it is", and the result key is `suggested`.
- write_time_divergence takes `suggested`; _RESEMBLE_MIN re-documented as the
  suggestion bar and the weak-stamp line.
- reusing-code skill no longer says the pull stamps an instance.
- Tests moved to the new contract, plus a guard that nothing the hook does
  writes classified_by="hook".

Plugin version minted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 08:42:10 -04:00
co-authored by Claude Opus 5.5
parent f1fbdf746a
commit 4cf1c6f042
7 changed files with 150 additions and 110 deletions
+14 -14
View File
@@ -1291,9 +1291,9 @@ async def test_stamping_needs_named_shapes_and_a_recent_pull():
patch.object(pc, "semantic_search_notes", AsyncMock(return_value=[])), \
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc.shape_ledger_svc, "recent_pulls", pulls), \
patch.object(pc.shape_ledger_svc, "stamp_write_path_instances", stamp):
patch.object(pc.shape_ledger_svc, "suggest_write_path_instances", stamp):
out = await pc.build_write_path_hint(1, "src/x.py", code=REAL_CODE, project_id=4)
assert out["stamped"] == []
assert out["suggested"] == []
pulls.assert_not_awaited() # no shapes → no read
out = await pc.build_write_path_hint(
1, "src/x.py", code=REAL_CODE, project_id=4,
@@ -1301,7 +1301,7 @@ async def test_stamping_needs_named_shapes_and_a_recent_pull():
)
pulls.assert_awaited_once()
stamp.assert_not_awaited() # shapes, but no pull
assert out["stamped"] == []
assert out["suggested"] == []
@pytest.mark.asyncio
@@ -1330,7 +1330,7 @@ async def test_a_pulled_snippet_already_seen_is_evidence_not_menu():
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \
patch.object(pc.shape_ledger_svc, "recent_pulls", AsyncMock(return_value={7: _ts()})), \
patch.object(pc.shape_ledger_svc, "stamp_write_path_instances", stamp):
patch.object(pc.shape_ledger_svc, "suggest_write_path_instances", stamp):
out = await pc.build_write_path_hint(
1, "src/x.py", code=REAL_CODE, project_id=4, exclude_ids=[7],
stamp_shapes=[("sym", "debounce")], repo_key="git.example.com/a/b",
@@ -1355,11 +1355,11 @@ async def test_a_pulled_snippet_already_seen_is_evidence_not_menu():
assert skw["resembles"] == {7: 0.91}
assert skw["shapes"] == [("sym", "debounce")]
assert skw["repo_key"] == "git.example.com/a/b"
assert out["stamped"][0]["snippet_id"] == 7
# And the session is told what landed, with the way to correct it.
assert out["suggested"][0]["snippet_id"] == 7
# And the session is shown it as evidence for its own end-of-turn judgment.
assert "Shape accounting" in out["context"]
assert "`debounce` → instance of #7" in out["context"]
assert "classify_shapes" in out["context"]
assert "`debounce` → looks like #7" in out["context"]
assert "When you judge this turn's shapes" in out["context"]
@pytest.mark.asyncio
@@ -1376,15 +1376,15 @@ async def test_a_stamp_renders_even_when_the_hint_is_otherwise_silent():
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \
patch.object(pc.shape_ledger_svc, "recent_pulls", AsyncMock(return_value={5: _ts()})), \
patch.object(pc.shape_ledger_svc, "stamp_write_path_instances",
patch.object(pc.shape_ledger_svc, "suggest_write_path_instances",
AsyncMock(return_value=stamped)):
out = await pc.build_write_path_hint(
1, "web/b.css", code=".btn-primary { color: red; }" * 4, project_id=4,
stamp_shapes=[("css", "btn-primary")],
)
assert out["note_ids"] == []
assert out["stamped"] == stamped
assert "`.btn-primary` → instance of #5" in out["context"]
assert out["suggested"] == stamped
assert "`.btn-primary` → looks like #5" in out["context"]
@pytest.mark.asyncio
@@ -1397,13 +1397,13 @@ async def test_a_failing_stamp_does_not_sink_the_hint():
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \
patch.object(pc.shape_ledger_svc, "recent_pulls", AsyncMock(return_value={12: _ts()})), \
patch.object(pc.shape_ledger_svc, "stamp_write_path_instances",
patch.object(pc.shape_ledger_svc, "suggest_write_path_instances",
AsyncMock(side_effect=RuntimeError("ledger down"))):
out = await pc.build_write_path_hint(
1, "src/x.py", code=REAL_CODE, project_id=4, stamp_shapes=[("sym", "f")],
)
assert out["sync_note_ids"] == [12]
assert out["stamped"] == []
assert out["suggested"] == []
def test_route_stamps_only_for_a_caller_allowed_to_write():
@@ -1760,7 +1760,7 @@ async def test_a_record_this_arm_withheld_itself_is_not_a_near_miss():
patch.object(pc, "owner_names_for", AsyncMock(return_value={})), \
patch.object(pc.shape_ledger_svc, "recent_pulls",
AsyncMock(return_value={7: _ts()})), \
patch.object(pc.shape_ledger_svc, "stamp_write_path_instances",
patch.object(pc.shape_ledger_svc, "suggest_write_path_instances",
AsyncMock(return_value=[])), \
patch.object(pc, "semantic_search_notes",
_search_reporting(0.9, fake_note(