diff --git a/docs/mesh.md b/docs/mesh.md index fea6d05..ab0f296 100644 --- a/docs/mesh.md +++ b/docs/mesh.md @@ -69,7 +69,7 @@ Every machine runs an identical **omind node**: | `omind/server.py` | Local **node MCP server** exposing `OmiStore` as MCP tools over stdio. Replaces the provisioned `obsidian-mcp`. (Since 2.27.0 the debounced-sync write signal lives in `OmiStore` itself, so every write surface — MCP, web UI, `omind note`, import — triggers it.) | | `omind/clock.py` | **Logical versioning** — a Lamport counter + stable node-id stamped into each note, the source of ordering truth for merges. | | `omind/merge.py` | The **git merge driver** for OMI notes (the core of the project). Field-level 3-way merge over `NoteFields`. | -| `omind/mesh.py` | **Replication daemon** — `init`, `commit_local`, `sync(peers)`, `daemon`, `clone`, peer membership. | +| `omind/mesh.py` | **Replication daemon** — `init`, `sync(peers)`, `daemon`, `clone`, peer membership. | ### Reused unchanged diff --git a/src/omind/compliance.py b/src/omind/compliance.py index 9e5b79d..5684c58 100644 --- a/src/omind/compliance.py +++ b/src/omind/compliance.py @@ -241,11 +241,6 @@ def read_events(limit: int | None = None) -> list[dict[str, Any]]: return list(events[-limit:]) if limit is not None else list(events) -def recidivism(rule_id: str) -> int: - """How many times ``rule_id`` has been recorded (decision + violation).""" - return sum(1 for e in read_events() if e.get("rule_id") == rule_id) - - def recidivism_counts(events: list[dict[str, Any]] | None = None) -> Counter[str]: """Per-rule occurrence counts across the whole log (drives escalation). diff --git a/src/omind/guard.py b/src/omind/guard.py index 3db8635..76a5f78 100644 --- a/src/omind/guard.py +++ b/src/omind/guard.py @@ -1833,15 +1833,9 @@ def _env_chdir_value(word: str, after: str) -> str | None: return None -def _shell_body(tokens: list[str]) -> str | None: - """The ``-c`` body of ``sh/bash/zsh … -c ''`` (``-lc``, ``-ec`` and - ``-o opt`` before it included), or ``None`` when there is no ``-c``.""" - i = _shell_body_index(tokens) - return None if i is None else tokens[i] - - def _shell_body_index(tokens: list[str]) -> int | None: - """Where :func:`_shell_body` finds the body in ``tokens``.""" + """Where the ``-c`` body of ``sh/bash/zsh … -c ''`` sits in ``tokens`` + (``-lc``, ``-ec`` and ``-o opt`` before it included), or ``None``.""" has_c = False i = 1 while i < len(tokens): diff --git a/src/omind/hooks.py b/src/omind/hooks.py index 27dc0be..06ba55e 100644 --- a/src/omind/hooks.py +++ b/src/omind/hooks.py @@ -96,8 +96,6 @@ def command_is_omind_hook(command: str) -> bool: # current working directory. Auto-journal actions remain searchable but are not # injected at session start. _SESSION_STATE_GLOB = "Session State *.md" -_JOURNAL_GLOB = paths.JOURNAL_GLOB -_JOURNAL_TAIL_BULLETS = 20 # Maximum canonical "full" capsule. Runtime selection lives in ai_usage. _TOTAL_CONTEXT_CHAR_CAP = 24_000 _TRUNCATION_MARKER = "\n…[truncated]" @@ -410,19 +408,6 @@ def _read_priming_note(path: Path) -> str | None: return text -def _latest_by_name(directory: Path, pattern: str) -> Path | None: - """Newest note matching ``pattern``, by filename descending. Never raises. - - Filenames embed ``YYYY-MM-DD``, so a plain lexicographic sort is also a - chronological sort. - """ - try: - matches = sorted(directory.glob(pattern), key=lambda p: p.name, reverse=True) - except OSError: - return None - return matches[0] if matches else None - - def action_bullets(text: str) -> list[str]: """The ``- `` bullets under a journal's ``## Actions`` heading. @@ -441,18 +426,6 @@ def action_bullets(text: str) -> list[str]: return bullets -def _journal_tail(path: Path, limit: int = _JOURNAL_TAIL_BULLETS) -> str | None: - """Last ``limit`` action bullets of a journal note, or ``None``. Never raises.""" - try: - text = path.read_text(encoding="utf-8", errors="replace") - except OSError: - return None - bullets = action_bullets(text) - if not bullets: - return None - return "\n".join(bullets[-limit:]) - - def _update_nudge_line() -> str | None: """The "newer omind available" line for the SessionStart context, or ``None``. diff --git a/src/omind/mesh.py b/src/omind/mesh.py index 248bb72..2f2d9f3 100644 --- a/src/omind/mesh.py +++ b/src/omind/mesh.py @@ -530,14 +530,6 @@ def _commit_locked(omi_dir: Path, message: str) -> bool: return True -def commit_local(omi_dir: Path, node_id: str) -> bool: - """Commit local changes. The lock spans staging, so a concurrent store - write can never be half-staged.""" - store = OmiStore(omi_dir) - with store.write_lock(): - return _commit_locked(omi_dir, f"omind: local changes on {node_id}") - - def _first_line(text: str) -> str: """The most diagnostic line of a git message (CONFLICT/error/fatal first).""" lines = [ln for ln in text.strip().splitlines() if ln.strip()] diff --git a/src/omind/policy.py b/src/omind/policy.py index b9cb65d..22cf16e 100644 --- a/src/omind/policy.py +++ b/src/omind/policy.py @@ -388,16 +388,6 @@ def command_search(compiled: re.Pattern[str], pattern: str, text: str) -> bool: _OWNER_SEP_RE = re.compile(r"[\n;&|(`]") -def _heredoc_owner_is_shell(command: str, start: int) -> bool: - """True when the simple command owning the heredoc at ``start`` is a shell. - - Scans back to the nearest separator and takes the first token that is not a - ``VAR=val`` assignment or a transparent wrapper, comparing its basename. - One heredoc's answer; :class:`_HeredocOwners` answers a run of them. - """ - return _HeredocOwners(command).is_shell(start) - - def _owner_word_skipped(token: str) -> bool: """Whether a word before a heredoc's owner is passed over: a ``VAR=val`` assignment or a transparent wrapper.""" @@ -412,8 +402,9 @@ def _owner_word_skipped(token: str) -> bool: class _HeredocOwners: - """:func:`_heredoc_owner_is_shell` for every heredoc in ``command``, in - linear time overall (#445). Each separator-bounded segment's words are + """Whether the simple command owning each heredoc in ``command`` is a shell + (skipping ``VAR=val`` assignments and transparent wrappers), in linear time + overall (#445). Each separator-bounded segment's words are found once, with the first one that is not skipped; a heredoc's owner is that word when it ends before the heredoc. Walking the words from the separator per heredoc was quadratic in a run of assignments followed by diff --git a/src/omind/searchindex.py b/src/omind/searchindex.py index e3dfd48..9e5b6e2 100644 --- a/src/omind/searchindex.py +++ b/src/omind/searchindex.py @@ -129,18 +129,6 @@ def available() -> bool: return not os.environ.get(DISABLE_ENV) and _fts5_available() -@dataclass -class SearchPage: - """One page of hits, with what the caller needs to decide whether to go on.""" - - hits: list[Hit] - #: Pass back as ``cursor`` to get the next page; ``None`` means this was all. - next_cursor: int | None = None - #: Summed estimate for this page, so context spend is a number the caller - #: can act on rather than something it discovers after the fact. - tokens_returned: int = 0 - - @dataclass class Hit: """One ranked chunk. ``excerpt`` is the matched text, not the whole note.""" @@ -1051,50 +1039,6 @@ def _embed_chunks(self, db: sqlite3.Connection, pending: list[tuple[int, str]]) # -- query -------------------------------------------------------------- - @_locked - def search_page( - self, - query: str, - *, - tag: str | None = None, - limit: int = 25, - include_disabled: bool = False, - max_tokens: int | None = None, - cursor: int = 0, - ) -> SearchPage | None: - """One bounded page of results, plus the cursor needed to ask for more. - - Why a cursor rather than a bigger ``limit``: under load a model tends to - conclude before it has exhausted what it was given — it stops early and - answers from what it has. A page that ends with an explicit - ``next_cursor`` makes continuing a deliberate act rather than something - the model has to remember to keep doing, and makes "there was more" a - fact the caller can see instead of one it has to infer. - - ``max_tokens`` bounds the page by estimated cost and reorders it - edge-first; see :func:`_pack_edge_first`. ``None`` leaves both off. - """ - hits = self.search( - query, - tag=tag, - limit=limit + cursor + 1, # +1 so we can tell "more" from "exactly full" - include_disabled=include_disabled, - ) - if hits is None: - return None - window = hits[cursor : cursor + limit] - has_more = len(hits) > cursor + limit - if max_tokens is not None and window: - window = _pack_edge_first(window, max_tokens) - # Packing can drop the tail, so "more" is now also true when the - # budget — not the page size — is what cut the results short. - has_more = has_more or len(window) < len(hits[cursor : cursor + limit]) - return SearchPage( - hits=window, - next_cursor=(cursor + len(window)) if has_more else None, - tokens_returned=sum(h.tokens for h in window), - ) - @_locked def search( self, @@ -2106,40 +2050,6 @@ def _annotate(hits: list[Hit]) -> None: hit.separation = hit.score - hits[i + 1].score if i + 1 < len(hits) else 0.0 -def _pack_edge_first(hits: list[Hit], max_tokens: int) -> list[Hit]: - """Greedily fit hits into ``max_tokens``, then order them best-outward. - - Two separate jobs, and the second is the interesting one. - - The budget exists because retrieval quality is not the binding constraint: - even with the right passage located, accuracy falls as the surrounding - context grows. Better ranking only decides how large the bill is; it cannot - stop the caller from paying it. So cap what leaves this function. - - The ORDER exists because attention over a long context is U-shaped — the - beginning and end are read well and the middle thins out. Emitting - best-first / second-best-LAST / weakest-in-the-middle puts the strongest - evidence where it is actually read. Note this only pays off once a caller - concatenates enough text to have a "middle" at all; below that it is - harmless reordering, which is why the budget is opt-in rather than default. - """ - kept: list[Hit] = [] - spent = 0 - for hit in hits: - cost = hit.tokens or _estimate_tokens(hit.excerpt) - # Always keep the best hit, even if it alone busts the budget: returning - # nothing is strictly worse than returning the one thing that matched. - if kept and spent + cost > max_tokens: - break - kept.append(hit) - spent += cost - head: list[Hit] = [] - tail: list[Hit] = [] - for i, hit in enumerate(kept): - (head if i % 2 == 0 else tail).append(hit) - return head + tail[::-1] - - def _fuse(legs: list[tuple[float, list[int]]]) -> list[tuple[int, float]]: """Reciprocal Rank Fusion over weighted ranked lists, best first.""" scores: dict[int, float] = {} diff --git a/src/omind/verify.py b/src/omind/verify.py index dba7f6d..01885dc 100644 --- a/src/omind/verify.py +++ b/src/omind/verify.py @@ -31,7 +31,6 @@ import json import os import re -import subprocess import sys from datetime import datetime from pathlib import Path @@ -435,7 +434,7 @@ def _parse_verdict(text: str) -> bool | None: return None -def _ask_model(task: str, text: str, omi_dir: Path | str | None = None) -> bool | None: +def _ask_model(task: str, text: str, omi_dir: Path | str) -> bool | None: """Ask a headless one-shot model whether the consult was relevant. Backend-agnostic: whichever of the known CLIs is on PATH, or a custom one via @@ -443,9 +442,9 @@ def _ask_model(task: str, text: str, omi_dir: Path | str | None = None) -> bool fails open).""" from omind import ai_usage - limits = ai_usage.policy(omi_dir) if omi_dir is not None else None - task_cap = limits.verifier_task_chars if limits else 1_000 - material_cap = limits.verifier_material_chars if limits else 2_000 + limits = ai_usage.policy(omi_dir) + task_cap = limits.verifier_task_chars + material_cap = limits.verifier_material_chars prompt = ( "You are an OMI-compliance relevance checker. An agent was told to consult " "its memory (OMI) before acting on a task, and it consulted the material " @@ -461,35 +460,12 @@ def _ask_model(task: str, text: str, omi_dir: Path | str | None = None) -> bool timeout = int(os.environ.get(_TIMEOUT_ENV) or _DEFAULT_TIMEOUT) except ValueError: timeout = _DEFAULT_TIMEOUT - if omi_dir is None: - # Compatibility path for the public pure ``judge`` helper. Real hook - # calls always supply the vault and therefore use the accounted wrapper. - from omind import ai_usage as _au - - resolved = _au.resolve_model_backend() - if resolved is None: - return None - template, _as_json, _name = resolved - argv = [ - prompt if part == "{prompt}" else part.replace("{prompt}", prompt) - for part in template - ] - stdin_text = prompt if _au.STDIN in template else None - if stdin_text is not None: - argv = [p for p in argv if p != _au.STDIN] - try: - result = subprocess.run( - argv, input=stdin_text, capture_output=True, text=True, timeout=timeout - ) - except (subprocess.TimeoutExpired, OSError): - return None - return _parse_verdict(result.stdout or "") if result.returncode == 0 else None response = ai_usage.run_claude( omi_dir, "verifier", prompt, timeout=timeout, - allowed=limits.verifier_llm if limits else True, + allowed=limits.verifier_llm, ) return _parse_verdict(response or "") @@ -513,12 +489,15 @@ def _judge_scored( task: str, activity: str, text: str, - pending: str = "", - omi_dir: Path | str | None = None, + pending: str, + omi_dir: Path | str, ) -> tuple[bool, float]: - """:func:`judge_with_activity` plus the deterministic score it decided on, - so the off-topic log can carry WHAT the consult was judged against (#148) — - without it, a sampling pass cannot adjudicate false positives after the fact.""" + """Relevance verdict plus the deterministic score it decided on. A consult is + relevant if it overlaps **any** of the turn task, the agent's recent activity + (#95), or the action the gate just blocked (#96), scored by ``max`` so none + dilutes another; the model only breaks the ambiguous middle band, fail-open + everywhere else. The score lets the off-topic log carry WHAT the consult was + judged against (#148).""" if not text or (not task and not activity and not pending): return True, 0.0 # can't judge without a signal -> fail open (relevant) high = _threshold(_HIGH_ENV, _HIGH) @@ -532,37 +511,10 @@ def _judge_scored( return True, score if score <= low: return False, score - verdict = ( - _ask_model(task or activity or pending, text) - if omi_dir is None - else _ask_model(task or activity or pending, text, omi_dir) - ) + verdict = _ask_model(task or activity or pending, text, omi_dir) return (True if verdict is None else verdict), score -def judge_with_activity(task: str, activity: str, text: str, pending: str = "") -> bool: - """Relevance verdict blending the captured task, the agent's recent activity - (issue #95), and the action the consult-gate just BLOCKED (issue #96): a consult - is relevant if it overlaps **any** of the three. Same deterministic-prefilter → - model-fallback ladder as :func:`judge`, scoring against ``max`` of the overlaps so - none dilutes another (concatenating would inflate the recall denominator — the - dilution 2.43.2 fought). The pending (blocked) action is the agent's freshest - intent: at a work *transition* the task and activity are both cold (still the - previous thread), but the blocked action is the new-thread work that tripped the - gate, so the FIRST consult clears instead of burning re-closes. Path noise in a - blocked command is normalized away first (#97). Only the REACTIVE order (attempt → - blocked → consult) records a pending; a proactive consult at a transition has none - to lean on, so prefer attempting the new work first at a transition.""" - return _judge_scored(task, activity, text, pending)[0] - - -def judge(task: str, text: str) -> bool: - """Relevance verdict for a single consult against the turn task alone. - Deterministic prefilter first (with operator-tunable thresholds), the model - only for the ambiguous middle, fail-open everywhere else.""" - return judge_with_activity(task, "", text) - - def _require_mode(require: bool | None) -> bool: if require is not None: return require diff --git a/tests/test_compliance.py b/tests/test_compliance.py index eeff60d..0cc6bd1 100644 --- a/tests/test_compliance.py +++ b/tests/test_compliance.py @@ -88,13 +88,13 @@ def test_the_log_rotates_at_the_cap_without_losing_counts( # The live log restarted, but history spans both generations — escalation # still counts every hit that came before the rotation. assert compliance.compliance_log_path().stat().st_size <= 200 - assert compliance.recidivism("r") == logged + assert compliance.recidivism_counts()["r"] == logged def test_recidivism_counts_exclude_the_gate() -> None: for rid in ("r1", "r1", "r2", "omi-gate"): compliance.log_event(compliance.KIND_DECISION, rule_id=rid, outcome="deny") - assert compliance.recidivism("r1") == 2 + assert compliance.recidivism_counts()["r1"] == 2 counts = compliance.recidivism_counts() assert counts["r1"] == 2 and counts["r2"] == 1 assert "omi-gate" not in counts diff --git a/tests/test_hooks.py b/tests/test_hooks.py index a5d0e20..be8b8d5 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -530,7 +530,7 @@ def test_session_start_journal_tail_is_last_bullets_only(tmp_path: Path) -> None _write_priming_files(tmp_path) old = datetime(2026, 6, 1, 9, 0, 0) hooks.append_entry(tmp_path, "- 09:00 [session old00000] PostToolUse Bash -> OLD-J (ok)", old) - for i in range(hooks._JOURNAL_TAIL_BULLETS + 5): + for i in range(25): bullet = f"- 14:32 [session abcd1234] PostToolUse Bash -> c{i:02d} (ok)" hooks.append_entry(tmp_path, bullet, _NOW) ctx = hooks.build_session_start_context(tmp_path) diff --git a/tests/test_searchindex.py b/tests/test_searchindex.py index 2282fc8..d47be5b 100644 --- a/tests/test_searchindex.py +++ b/tests/test_searchindex.py @@ -617,85 +617,6 @@ def test_annotate_reports_gap_to_next_hit() -> None: assert hits[-1].separation == 0.0 # nothing follows the last hit -def test_pack_edge_first_puts_the_best_at_both_ends() -> None: - """Attention over long context is U-shaped, so rank 2 goes LAST, not second.""" - hits = [ - searchindex.Hit(filename=f"n{i}", heading="", excerpt="x" * 40, score=1.0 - i / 10) - for i in range(5) - ] - searchindex._annotate(hits) - packed = searchindex._pack_edge_first(hits, 10_000) - assert packed[0].filename == "n0" # best first - assert packed[-1].filename == "n1" # second best last - assert packed[2].filename == "n4" # weakest buried in the middle - assert sorted(h.filename for h in packed) == sorted(h.filename for h in hits) - - -def test_pack_respects_the_budget() -> None: - hits = [ - searchindex.Hit(filename=f"n{i}", heading="", excerpt="x" * 400, score=1.0 - i / 10) - for i in range(5) - ] - searchindex._annotate(hits) - assert all(h.tokens == 100 for h in hits) - assert len(searchindex._pack_edge_first(hits, 250)) == 2 - - -def test_pack_always_returns_the_top_hit_even_when_it_busts_the_budget() -> None: - """Returning nothing is strictly worse than returning the one match.""" - hits = [searchindex.Hit(filename="huge", heading="", excerpt="x" * 4000, score=1.0)] - searchindex._annotate(hits) - assert [h.filename for h in searchindex._pack_edge_first(hits, 1)] == ["huge"] - - -def test_search_page_cursor_walks_the_results(omi: Path) -> None: - _note(omi, "Release One", "release checklist alpha", ["release"]) - _note(omi, "Release Two", "release checklist beta", ["release"]) - _note(omi, "Release Three", "release checklist gamma", ["release"]) - idx = searchindex.SearchIndex(omi) - idx.refresh() - - first = idx.search_page("release", limit=1) - assert first is not None and first.hits - assert first.tokens_returned == sum(h.tokens for h in first.hits) - assert first.next_cursor is not None # three notes, one per page - - second = idx.search_page("release", limit=1, cursor=first.next_cursor) - assert second is not None and second.hits - # A cursor must advance. A page that returns the same note forever is worse - # than no paging, because it looks like progress. - assert [h.filename for h in second.hits] != [h.filename for h in first.hits] - - -def test_search_page_without_budget_matches_plain_search(omi: Path) -> None: - """The new surface must not change what the old one returns.""" - _note(omi, "Release One", "release checklist alpha", ["release"]) - _note(omi, "Release Two", "release checklist beta", ["release"]) - idx = searchindex.SearchIndex(omi) - idx.refresh() - - plain = idx.search("release", limit=5) - page = idx.search_page("release", limit=5) - assert plain is not None and page is not None - assert [h.filename for h in page.hits] == [h.filename for h in plain[:5]] - assert page.next_cursor is None # everything fit - - -def test_search_page_budget_signals_there_is_more(omi: Path) -> None: - """A budget that truncates must still report next_cursor, or the caller - silently believes it saw everything.""" - for i in range(4): - _note(omi, f"Release {i}", "release checklist " + ("word " * 200), ["release"]) - idx = searchindex.SearchIndex(omi) - idx.refresh() - - page = idx.search_page("release", limit=4, max_tokens=60) - assert page is not None and page.hits - assert len(page.hits) < 4 # the budget cut it short - assert page.next_cursor is not None # ...and said so - assert page.tokens_returned == sum(h.tokens for h in page.hits) - - def test_separation_reaches_the_search_vault_payload(omi: Path) -> None: """The margin must survive Hit -> NoteSummary, or the MCP tool never sees it. diff --git a/tests/test_verify.py b/tests/test_verify.py index 2d35a1d..0d7441b 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -6,7 +6,6 @@ import io import json -import subprocess from datetime import datetime from pathlib import Path @@ -15,6 +14,10 @@ from omind import compliance, guard, hooks, verify +def _judge(task: str, text: str) -> bool: + return verify._judge_scored(task, "", text, "", ".")[0] + + def _omi(tmp_path: Path) -> Path: omi = tmp_path / "OMI" omi.mkdir(parents=True, exist_ok=True) @@ -98,59 +101,26 @@ def test_verify_consult_index_read_does_not_clear_the_gate( def test_judge_prefilter_high_and_low_skip_the_model(monkeypatch: pytest.MonkeyPatch) -> None: # If the model were called these would blow up; the prefilter must short-circuit. monkeypatch.setattr(verify, "_ask_model", lambda *a, **k: (_ for _ in ()).throw(AssertionError)) - assert verify.judge("codeberg release push", "codeberg release push mirror") is True - assert verify.judge("codeberg release push", "banana mango smoothie") is False + assert _judge("codeberg release push", "codeberg release push mirror") is True + assert _judge("codeberg release push", "banana mango smoothie") is False # No task / no text -> fail open (relevant). - assert verify.judge("", "anything") is True - assert verify.judge("task", "") is True + assert _judge("", "anything") is True + assert _judge("task", "") is True def test_judge_middle_band_consults_the_model(monkeypatch: pytest.MonkeyPatch) -> None: calls: list[tuple[str, str]] = [] - def fake_model(task: str, text: str) -> bool | None: + def fake_model(task: str, text: str, omi_dir: object) -> bool | None: calls.append((task, text)) return False monkeypatch.setattr(verify, "_ask_model", fake_model) # ~one of three task terms overlap -> middle band -> model decides. - assert verify.judge("codeberg release push", "codeberg notes about other things") is False + assert _judge("codeberg release push", "codeberg notes about other things") is False assert calls # the model was actually consulted -def test_ask_model_fails_open_without_any_backend(monkeypatch: pytest.MonkeyPatch) -> None: - """No model CLI at all -> None -> the caller fails open. - - The verifier used to hardcode `claude`; it now resolves whichever supported - CLI is present, so absence means *no backend resolved*, not *no claude*. - """ - from omind import ai_usage - - monkeypatch.setattr(ai_usage, "resolve_model_backend", lambda: None) - assert verify._ask_model("t", "x") is None - - -def test_ask_model_uses_whatever_backend_resolves(monkeypatch: pytest.MonkeyPatch) -> None: - """A non-Claude CLI must be driven correctly — the point of the change.""" - from omind import ai_usage - - seen: dict[str, object] = {} - - def fake_run(argv, **kw): # type: ignore[no-untyped-def] - seen["argv"] = argv - return subprocess.CompletedProcess(argv, 0, stdout="RELEVANT", stderr="") - - monkeypatch.setattr( - ai_usage, - "resolve_model_backend", - lambda: (["/usr/bin/somecli", "exec", "{prompt}"], False, "somecli"), - ) - monkeypatch.setattr(verify.subprocess, "run", fake_run) - assert verify._ask_model("t", "x") is True - assert seen["argv"][:2] == ["/usr/bin/somecli", "exec"] - assert "OMI-compliance relevance checker" in seen["argv"][2] # {prompt} substituted - - def test_verify_consult_relevant_records_no_violation( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -368,10 +338,10 @@ def boom(*a: object, **k: object) -> bool: monkeypatch.setattr(verify, "_ask_model", boom) # zero overlap -> deterministic irrelevant by default - assert verify.judge("codeberg release", "banana mango smoothie") is False + assert _judge("codeberg release", "banana mango smoothie") is False # lower HIGH to 0 -> any score is "high" -> relevant, still no model call monkeypatch.setenv("OMI_VERIFY_HIGH", "0.0") - assert verify.judge("codeberg release", "banana mango smoothie") is True + assert _judge("codeberg release", "banana mango smoothie") is True def test_always_relevant_allowlist(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: @@ -528,7 +498,7 @@ def test_delegated_work_consult_is_relevant_via_activity( "Grep -> matcher merge bsim signature (ok)", ) # Pre-fix: judged against the user line alone, this consult is OFF-topic. - assert verify.judge(task, note.read_text(encoding="utf-8")) is False + assert _judge(task, note.read_text(encoding="utf-8")) is False # Post-fix: blending in what the agent is DOING makes it relevant. verdict = verify.verify_consult( {"tool_name": "Read", "session_id": "build1", "tool_input": {"file_path": str(note)}}, @@ -716,10 +686,10 @@ def test_semantic_blend_rescues_a_keyword_poor_consult(monkeypatch: pytest.Monke text = "the steps to cut a release and ship it to the forge" # keyword-only (no backend): no shared terms -> judged irrelevant, as in 2.x monkeypatch.setattr(verify.embed, "similarity", lambda a, b: None) - assert verify.judge(task, text) is False + assert _judge(task, text) is False # with a semantic backend rating them close, the blend lifts it to relevant monkeypatch.setattr(verify.embed, "similarity", lambda a, b: 0.82) - assert verify.judge(task, text) is True + assert _judge(task, text) is True # -- #239: truncated reads of the DEMANDED note keep the gate armed -----------