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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [0.2.6] — 2026-07-21

### Added
- **`mm_prereg_lint` tool** (19 tools total) — surfaces measure-mirror's new
`prereg_lint` (㉗): a seal-*quality* check to run right before spending compute.
Distinct from `mm_falsifiability_check` (presence) and the `mm_preflight`
existence gate — it flags a kill-condition leaked into the `metric` field, a
quantified kill with no structured threshold, a pass bar at/below chance, a
low `min_n`, or no declared pre-seal machine-checks.
- **`mm_preregister(pre_seal_checks=[...])`** passthrough.

### Changed (deps)
- **measure-mirror pin `e2911ca` → `13077df`** (v0.25.0 → v0.26.0, #28): brings the
`prereg_lint` probe this release's tool and gate wiring require.

### Changed
- **Compute gate reports a leaked kill-condition** (`gate.py`) — when a
pre-registration exists but its kill-condition leaked into `metric` (no kill
fields), `mm_preflight`/`mirror-stack-gate compute` still BLOCKs, now with the
accurate reason (pointing to `mm_prereg_lint`) instead of the misleading
"no sealed preregistration".

## [0.2.5] — 2026-07-21

### Changed
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ dependencies — you don't clone four repos. (Apache-2.0, zero-dep cores.)
No `cwd`, no `PYTHONPATH` — it's a proper installed entry point. Works in Claude Code, Cursor,
Windsurf, any MCP client.

## Tools (18)
## Tools (19)

| Tool | Mirror | Does |
|---|---|---|
| `mm_preregister` | 🪞 claims | seal a claim + kill-condition **before** measuring |
| `mm_preregister` | 🪞 claims | seal a claim + kill-condition **before** measuring (response carries an auto seal-quality lint) |
| `mm_verify` | 🪞 | umbrella verify — every probe whose input key is present |
| `mm_audit` | 🪞 | audit a result vs its sealed registration |
| `mm_power_check` | 🪞 | false-negative guard — is n big enough? (design-time) |
| `mm_falsifiability_check` | 🪞 | Popper gate — kill-condition registered & not tripped? |
| `mm_prereg_lint` | 🪞 | seal *quality* lint (㉗) — leaked kill-condition, bar at/below chance, unstructured kill, low n, undeclared pre-seal checks; the compute gate BLOCKs on a lint FAIL |
| `mm_leakage_check` | 🪞 | train∩test contamination |
| `mm_multiseed_check` | 🪞 | unstable signal / lucky seed |
| `mm_retract` | 🪞 | chain-linked retraction (cannot be silently deleted) |
Expand Down
2 changes: 1 addition & 1 deletion docs/STACK_CANONICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ so "which one is authoritative?" has a written answer.
| Surface | Lives in | Owns |
|---|---|---|
| **measure-mirror `stack/`** | [`bhyi4/measure-mirror`](https://github.com/bhyi4/measure-mirror) `stack/` | the stack **conventions + honesty docs** (`MIRROR_STACK.md`, `DISCIPLINE.md`, `PILLARS.md`, the case study); **`verify_self.py`** (one claims ledger: L1 self-chain + L3 anchors, zero external tool); **`verify_all.py`** (the orchestrator — adds L2 cross-witness *between* ledgers via `am`); `tombstone.py` |
| **mirror-stack-mcp** | this repo | the **agent MCP server** (`server.py`, 18 tools); the **`mirror-stack-gate`** enforcer CLI (`gate.py`); the **zero-config outsider** `mirror-stack-verify` CLI (`verify.py`); Bitcoin/OTS anchoring (`ots_anchor.py`) |
| **mirror-stack-mcp** | this repo | the **agent MCP server** (`server.py`, 19 tools); the **`mirror-stack-gate`** enforcer CLI (`gate.py`); the **zero-config outsider** `mirror-stack-verify` CLI (`verify.py`); Bitcoin/OTS anchoring (`ots_anchor.py`) |

Rule of thumb: **measure-mirror `stack/` = the conventions and the self/orchestrated
verification that ships with the library**; **mirror-stack-mcp = how an *agent* (MCP)
Expand Down
2 changes: 1 addition & 1 deletion mirror_stack_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""🪞🔎🪪 Mirror Stack unified MCP server."""
__version__ = "0.2.5"
__version__ = "0.2.6"
36 changes: 32 additions & 4 deletions mirror_stack_mcp/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@


def scan_claim(ledger_path, claim_id):
"""Return (prereg_entry_or_None, retracted_bool) for claim_id in a ledger."""
prereg, retracted = None, False
"""Return (prereg_entry_or_None, retracted_bool, leaked_entry_or_None) for claim_id.

leaked_entry is a preregistration that carries NO kill fields but whose `metric`
reads like a kill-condition — i.e. the criterion leaked into the wrong field from a
malformed call. It is not a valid prereg (compute stays BLOCKed), but naming it lets
the gate report the real reason instead of a misleading 'no preregistration'.
"""
prereg, retracted, leaked = None, False, None
if os.path.exists(ledger_path):
with open(ledger_path, encoding="utf-8") as fh:
for line in fh:
Expand All @@ -42,19 +48,29 @@ def scan_claim(ledger_path, claim_id):
elif e.get("_type") is None and ("kill_threshold" in e or "kill_condition" in e) \
and e.get("metric") != "protocol_amendment":
prereg = e
return prereg, retracted
elif e.get("_type") is None and e.get("metric") not in (None, "protocol_amendment"):
from measure_mirror import mm
if leaked is None and mm._looks_like_kill_prose(e.get("metric", "")):
leaked = e
return prereg, retracted, leaked


def decide(ledger_path, claim_id, gate="compute", am_ledger=None, reported_acc=None):
"""Pure GO/BLOCK decision. Returns {decision, gate, claim_id, reasons, checks}."""
prereg, retracted = scan_claim(ledger_path, claim_id)
prereg, retracted, leaked = scan_claim(ledger_path, claim_id)
checks: list[str] = []

def out(decision, reasons):
return {"decision": decision, "gate": gate, "claim_id": claim_id,
"reasons": reasons, "checks": checks}

if prereg is None:
if leaked is not None:
return out("BLOCK", [
"a preregistration exists but its kill-condition leaked into the `metric` "
f"field ({str(leaked.get('metric'))[:60]!r}...) — the automated checks can't "
"parse it. Re-seal under a NEW claim_id with the criterion in kill_condition= "
"(mm_prereg_lint pinpoints this)."])
return out("BLOCK", ["no sealed preregistration for this claim — seal one "
"(mm_preregister with a kill-condition) before proceeding"])
has_kill = bool(prereg.get("kill_threshold") or prereg.get("kill_condition"))
Expand All @@ -64,6 +80,18 @@ def out(decision, reasons):
if not has_kill:
return out("BLOCK", ["preregistration has no kill-condition (unfalsifiable) — "
"add one before spending compute"])
# Seal-quality lint: a FAIL (e.g. a pass bar at/below chance) means the
# automated checks can't do their job — compute would be spent against a
# meaningless bar. WARN/INFO inform but don't block.
from measure_mirror import mm
lint = mm._preseal_lint(prereg)
fails = [f for f in lint if f.level == "FAIL"]
warns = [f for f in lint if f.level == "WARN"]
if warns:
checks.append("prereg-lint: " + "; ".join(f.msg for f in warns))
if fails:
return out("BLOCK", ["prereg-lint FAIL — " + " | ".join(f.msg for f in fails)])
checks.append("prereg-lint: no FAIL")
return out("GO", ["sealed preregistration with a kill-condition is present"])

if gate == "publish":
Expand Down
61 changes: 49 additions & 12 deletions mirror_stack_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,22 @@

BEFORE spending compute (seal first):
1. Preregister the claim WITH a kill-condition — mm_preregister(kill_threshold=...).
No falsification criterion = unfalsifiable.
No falsification criterion = unfalsifiable. Run the cheap machine-checks first and
declare them (pre_seal_checks=: reachability-smoke / mass-balance-audit /
neutral-control / manipulation-check / positive-control).
2. Power: is n big enough to detect the effect? mm_power_check (design-time).
3. Lint the seal — mm_prereg_lint (auto in the mm_preregister response). A FAIL
(kill-condition leaked into the metric field, bar at/below chance) means the
automated checks can't fire: fix and re-seal under a NEW claim_id.

BEFORE reporting (verify before speaking):
3. Fair baseline, not crippled, same budget/data — mm_baseline_fairness.
4. Gaming line: rewarding the metric is an artifact; only removal/swap is honest — mm_verify(reward_terms).
5. Both directions — false positive AND false negative. Did you test the REAL target or a stand-in?
6. Multi-seed + independent reproduction — mm_multiseed_check; am_witness from another agent.
7. Scope: state what you closed and did NOT close — mm_verify(claimed_scope, tested_scope).
8. Numeric + multiplicity hygiene — mm_verify (grim, multiple-comparisons).
9. Self-catch: "too good" is suspect first.
4. Fair baseline, not crippled, same budget/data — mm_baseline_fairness.
5. Gaming line: rewarding the metric is an artifact; only removal/swap is honest — mm_verify(reward_terms).
6. Both directions — false positive AND false negative. Did you test the REAL target or a stand-in?
7. Multi-seed + independent reproduction — mm_multiseed_check; am_witness from another agent.
8. Scope: state what you closed and did NOT close — mm_verify(claimed_scope, tested_scope).
9. Numeric + multiplicity hygiene — mm_verify (grim, multiple-comparisons).
10. Self-catch: "too good" is suspect first.
If an LLM judge is involved, check it too (consistency / bias / swap / transitivity).

THE RECORD: seal claims and actions (am_record target=<claim_id>); anchor externally
Expand Down Expand Up @@ -132,6 +137,10 @@ def _compact(findings):
"is inconclusive; raise n or narrow scope.",
"mm_falsifiability_check": "🪞 If the kill-condition tripped (FAIL), the claim is falsified by "
'its OWN criterion → mm_retract it. If it did not trip, that\'s "not refuted", not "proven".',
"mm_prereg_lint": "🪞 Lint judges the seal's QUALITY, not just its presence. A FAIL here "
"(kill-condition leaked into `metric`, or a bar at/below chance) means the automated "
"checks can't fire — fix and re-seal under a NEW claim_id (first-write-wins). Run this "
"right before spending compute; it's the cheap machine-check that saves a KILL.",
"mm_preflight": "🪞 This is a primitive — the MCP only judges GO/BLOCK. YOUR launcher / "
"pre-commit hook must do the actual blocking; the MCP can't stop external compute or "
"commits (by design, opt-in).",
Expand Down Expand Up @@ -162,17 +171,23 @@ def mm_preregister(ledger_path: str, claim_id: str, metric: str, min_n: int = 20
baseline: float = 0.5, pass_threshold: float = 0.6,
kill_condition: str | None = None, kill_threshold: dict | None = None,
depends_on: list[str] | None = None,
metric_range: list | str | None = None, chance: float | None = None) -> dict:
metric_range: list | str | None = None, chance: float | None = None,
pre_seal_checks: list[str] | None = None) -> dict:
"""Seal a claim BEFORE measuring (preregistration). kill_condition/threshold = what falsifies it.

For a non-[0,1] metric, declare metric_range (e.g. [0,100] for a %, or "unbounded" for a
delta/span) + chance (the real chance level, e.g. 1/24≈0.042) so audit doesn't false-FAIL
or assume baseline 0.5. Omit for a plain [0,1] accuracy."""
return _remind("mm_preregister", mm.preregister(
or assume baseline 0.5. Omit for a plain [0,1] accuracy.

The response carries an automatic seal-quality lint (`lint` key): a FAIL there means
the compute gate will BLOCK this claim — fix and re-seal under a NEW claim_id."""
entry = mm.preregister(
ledger_path, claim_id, metric=metric, min_n=min_n, baseline=baseline,
pass_threshold=pass_threshold, kill_condition=kill_condition,
kill_threshold=kill_threshold, depends_on=depends_on,
metric_range=metric_range, chance=chance))
metric_range=metric_range, chance=chance, pre_seal_checks=pre_seal_checks)
lint = _compact(_findings(mm._preseal_lint(entry)))
return _remind("mm_preregister", {**entry, "lint": lint})


@mcp.tool()
Expand Down Expand Up @@ -217,6 +232,28 @@ def mm_falsifiability_check(ledger_path: str, claim_id: str,
reported_acc=reported_acc, am_ledger=am_ledger)))


@mcp.tool()
def mm_prereg_lint(ledger_path: str, claim_id: str | None = None) -> list[str]:
"""🔍 Lint a sealed preregistration for QUALITY defects — the cheap machine-check to
run right before spending compute.

falsifiability_check / the compute gate ask 'does a kill-condition exist?'. This asks
'is the seal well-formed enough that the automated checks can actually fire, and is the
bar meaningful?' — the failure classes that leak silent compute:
• kill-condition prose leaked into the `metric` field (a malformed call) — the human
eye sees a criterion, the parser sees none [FAIL]
• a quantified kill written as free text with no structured kill_threshold [WARN]
• a pass bar sitting at or below chance [FAIL]
• min_n below the small-sample floor [WARN]
• no cheap pre-seal machine-checks declared (reachability / accounting / neutral-
control / manipulation / positive-control) [INFO nudge]

claim_id=None lints every preregistration in the ledger. A FAIL means fix and re-seal
under a NEW claim_id (first-write-wins makes the old one uncorrectable)."""
return _remind("mm_prereg_lint",
_compact(_findings(mm.prereg_lint(ledger_path, claim_id))))


@mcp.tool()
def mm_leakage_check(train_items: list, test_items: list) -> str:
"""Detect train∩test contamination via hash intersection."""
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mirror-stack-mcp"
version = "0.2.5"
version = "0.2.6"
description = "Unified MCP server for the Mirror Stack — claims, actions, provenance + verify-all in one server"
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -16,7 +16,7 @@ dependencies = [
# tracks each mirror's default branch, so an install made before a mirror's release
# freezes on whatever was HEAD then and never refreshes — that is how a local install
# ran stale measure-mirror 0.14.3 long after 0.15.1 landed. Bump these on purpose.
"measure-mirror @ git+https://github.com/bhyi4/measure-mirror@e2911cae4fed4f32bbb51d3c748131dace8e2d1e", # v0.25.0 — catalog v1.8 / 45 entries (#26, docs-only); anchor-discipline probes ㉔㉕ + SPEC amendment A2 (anchor_cell/anchor_line_source/known_confounds); v0.24.0 grounding ㉑㉒㉓ + A1; v0.23.0 kill_threshold validation
"measure-mirror @ git+https://github.com/bhyi4/measure-mirror@13077df8c2616e0559742b2d97c37e2459d7906c", # v0.26.0 — ㉗ prereg_lint pre-compute seal-quality probe + pre_seal_checks (#28; required by mm_prereg_lint/gate lint wiring); v0.25.0 catalog v1.8 + anchor probes ㉔㉕ A2; v0.24.0 grounding ㉑㉒㉓ + A1
"action-mirror @ git+https://github.com/bhyi4/action-mirror@fd46e90c3e3f0706792c261316f4699d7a6799d8", # v0.1.0 (ledgers conform to MIRROR-SPEC v1.0)
"provenance-mirror @ git+https://github.com/bhyi4/provenance-mirror@8edbbfd94772d3718de93271c1f80297c0476043", # v0.1.0 (ledgers conform to MIRROR-SPEC v1.0)
]
Expand Down
4 changes: 2 additions & 2 deletions tests/smoke_installed.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

This is the exact check that would have caught our local install going silently stale:
'works in the repo, broken/old once installed'. It asserts the unified server imports
in a clean environment, exposes all 18 tools, and that the three re-exposed mirror
in a clean environment, exposes all 19 tools, and that the three re-exposed mirror
packages are importable (a missing/renamed mirror dep fails here, loudly).
"""
import mirror_stack_mcp.server as s

tools = {t.name for t in s.mcp._tool_manager.list_tools()}
assert len(tools) == 18, f"expected 18 tools, got {len(tools)}: {sorted(tools)}"
assert len(tools) == 19, f"expected 19 tools, got {len(tools)}: {sorted(tools)}"
assert callable(s.main), "entry point mirror_stack_mcp.server:main missing"

import measure_mirror # noqa: F401 re-exposed claim probes
Expand Down
Loading
Loading