feat(plugin): the seam that erases the evidence is where the unresolved rules get named (#4216)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m46s
CI & Build / Build & push image (push) Successful in 14s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m46s
CI & Build / Build & push image (push) Successful in 14s
Step 5 of milestone 419. The milestone's subject is that a rule read and ignored is arithmetically identical to a rule read and followed, and the compaction is where that identity becomes permanent — the turns holding the evidence are summarised away, and the unjudged thing survives as nothing. WHY THIS IS ASSEMBLED IN THE HOOK. `rule_usage_events` has no session column; it is per user over a window. A session-scoped answer therefore cannot be asked of the server, and has to be built where a session is a thing that exists. Four ledgers four hooks already write: .rules.ids an arm NAMED the rule .opened.ids the session called get_rule (#4100) .acted.ids the session called rule_outcome (new here) .checkpoint.ids the rule HELD an act (#4214) Every one is an observed tool call. Nothing asks the model what it followed — milestone 386 ruled that out, because a model asked "did you apply rule 156?" says yes. Two subtractions: named-minus-opened is the arm talking to nobody, opened-minus-acted is the milestone's whole subject. PreCompact stdout is the compaction's custom instructions (#3680), not a message to the model, so the readout does not say "you slipped" — it says which ids must be carried through, which is the one thing a summary can do about an unjudged finding. SILENT WHEN NOTHING HAPPENED, and the accusations are conditional on having members. "0 rules unresolved" on every compaction is how a readout teaches its reader to skip it. Traffic is still reported, because the static instructions already ask for it in prose; these lines are the measured version. scribe_record_outcome.sh is the third ledger's writer, matched on mcp__.*__rule_outcome and mirroring scribe_record_opened.sh: TMPDIR only, silent, exit 0 on every path. A PostToolUse hook that spoke would put a line after every rule_outcome call and give recording an outcome a cost. Also: check_plugin.py skipped the new hook for want of a smoke event, which would have left the newest of the three ledgers as the only one the plugin lane never runs. Added, mirroring its sibling. tests/test_precompact_hook.py now isolates TMPDIR — the hook reads session ledgers from there, so without isolation a test would see whatever this real session had accumulated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -29,6 +29,8 @@ correct beside every other hook in this directory and would inject nothing.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
@@ -44,11 +46,22 @@ EVENT = {"session_id": "s1", "transcript_path": "/tmp/t.jsonl", "cwd": "/repo",
|
||||
"custom_instructions": None}
|
||||
|
||||
|
||||
def _run(event: dict) -> subprocess.CompletedProcess:
|
||||
def _run(event: dict, tmpdir: str | None = None) -> subprocess.CompletedProcess:
|
||||
"""Run the hook with its ledger directory ISOLATED.
|
||||
|
||||
The hook reads this session's rule ledgers since #4216, and they live under
|
||||
`$TMPDIR/scribe-priorart`. Without an override these tests would read the
|
||||
machine's real /tmp: on a developer box mid-session that is not empty, and
|
||||
the output would depend on what some other session happened to leave
|
||||
behind. None of the assertions below would fail on it today, which is
|
||||
exactly why it is worth closing now rather than after it starts flaking.
|
||||
"""
|
||||
if shutil.which("bash") is None:
|
||||
pytest.skip("bash not installed")
|
||||
env = dict(os.environ)
|
||||
env["TMPDIR"] = tmpdir or tempfile.mkdtemp()
|
||||
return subprocess.run(["bash", str(HOOK)], input=json.dumps(event),
|
||||
capture_output=True, text=True, timeout=30)
|
||||
capture_output=True, text=True, timeout=30, env=env)
|
||||
|
||||
|
||||
def _code() -> str:
|
||||
@@ -115,9 +128,11 @@ def test_it_never_blocks_the_compaction():
|
||||
assert "decision" not in code, "a block decision would skip the compaction"
|
||||
assert "exit 2" not in code
|
||||
# A truncated or absent event must not turn into a non-zero exit either.
|
||||
env = dict(os.environ)
|
||||
env["TMPDIR"] = tempfile.mkdtemp()
|
||||
for event in ("", "not json", "{}"):
|
||||
out = subprocess.run(["bash", str(HOOK)], input=event,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
capture_output=True, text=True, timeout=30, env=env)
|
||||
assert out.returncode == 0, f"{event!r} → {out.returncode}: {out.stderr}"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user