Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Please choose versions by [Semantic Versioning](http://semver.org/).
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.

## Unreleased

- fix: ambient operator memory is now part of the configuration identity. The reviewer resolves `$HOME/.claude/CLAUDE.md` and obeys it β€” proven by an `opus`/`xhigh`/`full` review of `quant#109` that ended with the operator's personal state-closer panel (`πŸ“Œ`, `πŸ‘€ You:`, `⏰ Next:`), a convention defined only in that file. The config hash therefore claimed to identify a configuration it did not determine. `ambient_memory_hash()` is now a hash component and is recorded on its own row field so a reader can see *which* input differed. **Every pre-existing ledger row carries a stale config hash** β€” they were measured under an unpinned condition
- docs: `review_env` records why isolation was attempted and abandoned. Memory resolves through `HOME`, but so does auth: redirecting `HOME` to the config dir, to a scratch dir with `.claude.json` symlinked, and to a scratch dir mirroring all 49 entries of `~/.claude` minus `CLAUDE.md` each produced `Not logged in`. `claude --bare` drops CLAUDE.md discovery but drops OAuth and plugin sync with it. Containerising the review (the `claude-yolo` pattern) remains the route to cross-machine portability; pinning restores reproducibility on one machine and nothing more

## v0.38.0

- fix: one issue, one golden entry. The `apt-key` adjudication in `v0.37.0` added a line-free entry for a defect the Opus baseline **already carried**, pinned to `ci.yml:32` β€” so the same issue was represented twice, three findings matched both, and recall inflated to a spurious `1.000`. The original entry is relaxed to a line-free signature instead and the duplicate removed (43 entries: 42 accepted, 1 rejected). The `v0.37.0` note below claims the finding was "absent from the Opus baseline"; that was wrong β€” it was present, pinned to a line the later runs did not cite
Expand Down
47 changes: 44 additions & 3 deletions bench/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,49 @@ def content_hash(root: pathlib.Path) -> str:
return h.hexdigest()


AMBIENT_MEMORY_PATH = pathlib.Path.home() / ".claude" / "CLAUDE.md"


def ambient_memory_hash(path: pathlib.Path = None) -> str:
"""Digest of the operator memory the reviewer actually reads, or "none".

The reviewer resolves `$HOME/.claude/CLAUDE.md` and obeys it. Proven on
2026-08-09: an opus/xhigh/full review of `quant#109` ended with the
operator's personal state-closer panel (`πŸ“Œ`, `πŸ‘€ You:`, `⏰ Next:`), a
convention defined only in that file and nowhere in this plugin.

Isolating it was attempted and abandoned. Memory resolves through HOME, but
so does authentication β€” every HOME redirect tried (config dir, a scratch
dir with `.claude.json` symlinked, a scratch dir mirroring all 49 entries of
`~/.claude` minus CLAUDE.md) produced `Not logged in Β· Please run /login`.
`claude --bare` drops CLAUDE.md discovery but drops OAuth and plugin sync
with it. Containerising the review (the `claude-yolo` approach) would work
and remains the route to cross-machine portability.

So it is pinned instead of removed. Hashing it keeps the promise the config
identity actually makes β€” that a digest determines the measurement β€” and
makes a change to operator memory invalidate cached rows instead of silently
altering results. It does NOT make a score portable to another machine;
nothing here claims it does.
"""
p = AMBIENT_MEMORY_PATH if path is None else path
try:
return hashlib.sha256(p.read_bytes()).hexdigest()
except OSError:
return "none"


def config_hash(rules_commands_hash: str, model: str, effort: str,
mode: str, prs_version: str) -> str:
"""SHA-256 over the five configuration-identity components.
mode: str, prs_version: str, ambient_hash: str = None) -> str:
"""SHA-256 over the configuration-identity components.

Mode is a first-class component: changing only mode must produce a different digest.
Ambient operator memory is a component for the reason given in
ambient_memory_hash β€” it demonstrably steers the reviewer, so leaving it out
made the digest a promise the runner could not keep.
"""
payload = "\0".join([rules_commands_hash, model, effort, mode, prs_version])
amb = ambient_memory_hash() if ambient_hash is None else ambient_hash
payload = "\0".join([rules_commands_hash, model, effort, mode, prs_version, amb])
return hashlib.sha256(payload.encode("utf-8")).hexdigest()


Expand Down Expand Up @@ -1523,6 +1559,11 @@ def build_row(*, checkout: PrCheckout, cfg_hash: str, rc_hash: str,
return {
"config_hash": cfg_hash,
"rules_commands_hash": rc_hash,
# Recorded on its own line, not just folded into config_hash: a reader
# comparing two rows must be able to see WHICH input differed, and
# ambient operator memory is the one input that changes without any
# commit to this repo.
"ambient_memory_hash": ambient_memory_hash(),
"model": model,
"effort": effort,
"mode": mode,
Expand Down
31 changes: 31 additions & 0 deletions bench/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,37 @@ def test_config_hash_identical_inputs(self):
args = ("a" * 64, "claude-opus-5", "high", "short", "dev-1")
self.assertEqual(run.config_hash(*args), run.config_hash(*args))

def test_config_hash_distinguishes_ambient_memory(self):
"""Same five components but different operator memory β†’ different digest.

Ambient `~/.claude/CLAUDE.md` demonstrably steers the reviewer (an
opus/xhigh/full review ended with the operator's personal state-closer
panel), so a digest that ignored it would claim to identify a
configuration it does not determine. Without this test the component
could be dropped from the payload and every other test would still pass.
"""
args = ("a" * 64, "claude-opus-5", "high", "short", "dev-1")
self.assertNotEqual(
run.config_hash(*args, ambient_hash="memory-A"),
run.config_hash(*args, ambient_hash="memory-B"),
"config hash must change when operator memory changes",
)

def test_ambient_memory_hash_reports_none_when_absent(self):
"""A machine with no operator memory hashes to the literal "none".

Not an empty string and not a crash: an absent file is a real, nameable
configuration state, and it must be distinguishable from a present one.
"""
import pathlib
import tempfile
with tempfile.TemporaryDirectory() as td:
missing = pathlib.Path(td) / "CLAUDE.md"
self.assertEqual(run.ambient_memory_hash(missing), "none")
present = pathlib.Path(td) / "present.md"
present.write_text("some operator rule\n", encoding="utf-8")
self.assertNotEqual(run.ambient_memory_hash(present), "none")


class TestLoadManifest(unittest.TestCase):
"""Manifest loading and validation."""
Expand Down
Loading