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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [8.6.0] - 2026-08-14

### Added
- **Machine-readable note rules compile into deterministic PreToolUse checks**
(#240). Fenced ```` ```omind-rule ```` YAML blocks in vault notes (`id`,
`tool`, `match` glob, `when: {repo_visibility, branch}`, `except_repos`,
`action: deny|warn`, `message`) are parsed (cached per file mtime/size,
invalid blocks skipped with a breadcrumb) and evaluated in
`guard.check_action` before everything else — every rule a hook can decide
never depends on model attention. The cryptojones.github.io exception was
violated three times *while the governing note was force-recalled*; a
ten-line deterministic check makes that class of recurrence impossible.
Repo visibility via `gh repo view` (24h on-disk cache) **fails open** to
unknown; `warn` and unknown-visibility hits log compliance decisions without
blocking; every deny logs like any other hard rule. `omind rules list`
prints the compiled table including skipped blocks. Ships one seed rule
(deny direct `git push` on a public repo's checked-out main/master) that a
vault note with the same `id` replaces to add exceptions; the repo-deletion
incident class was already covered by `policy.SEED_RULES`.

## [8.5.0] - 2026-08-14

### Added
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,16 @@ rule in `omind.policy`. The Playbook is the guard's priming made explicit: *don'
ask a fresh instance to remember — put the rule in front of it, and block the wrong
action.*

Operators can also declare **deterministic rules inside ordinary vault notes**
with fenced ```` ```omind-rule ```` blocks (YAML: `id`, `tool`, `match` glob,
optional `when: {repo_visibility, branch}` and `except_repos`, `action:
deny|warn`, `message`). They compile into PreToolUse checks evaluated before
everything else — every rule a hook can decide never depends on model
attention. Repo visibility comes from `gh` (cached a day) and **fails open**
when unknown; invalid blocks are skipped with a breadcrumb. Inspect the
compiled table with `omind rules list`. A note rule with a seed rule's `id`
replaces the seed, so per-repo exceptions stay operator-editable in the vault.

## Activity checkpoints

You can't reliably *force* a running agent to do something on a wall clock —
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "omind"
version = "8.5.0"
version = "8.6.0"
description = "Reproduce the OMI/Obsidian memory integration for AI agents, plus a local web app to view, edit, and add memory entries."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/omind/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright 2026 Aaron K. Clark
"""omind — OMI/Obsidian memory tooling for AI agents."""

__version__ = "8.5.0"
__version__ = "8.6.0"
12 changes: 12 additions & 0 deletions src/omind/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@ def build_parser() -> argparse.ArgumentParser:
bench.add_argument("--json", action="store_true", help="emit measurements as JSON")
_add_vault_args(bench)

rules = sub.add_parser(
"rules",
help="deterministic note rules compiled into PreToolUse checks (#240)",
)
rules.add_argument("action", choices=["list"], help="list compiled rules")
_add_vault_args(rules)

lint = sub.add_parser(
"lint",
help="check the vault for broken wikilinks, isolated/orphaned notes, "
Expand Down Expand Up @@ -1603,6 +1610,11 @@ def main(argv: list[str] | None = None) -> int:
return _run_bench(args)
if args.command == "lint":
return _run_lint(args)
if args.command == "rules":
from omind import rules as _rules

print(_rules.format_rules((args.vault / args.folder).expanduser()))
return 0
if args.command == "recover":
return _run_recover(args)
if args.command == "graph":
Expand Down
46 changes: 45 additions & 1 deletion src/omind/guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,48 @@ def decide(action: dict[str, Any]) -> Verdict:
return Verdict(allow=False, reason=f"omi-gate: {GATE_MESSAGE}", rule_id="omi-gate")


def _note_rules_verdict(action: dict[str, Any], omi_dir: Path | None) -> Verdict | None:
"""Deterministic operator note rules (#240), evaluated before everything.

Rules a hook can decide must never depend on model attention. A ``deny``
hit blocks with the rule's message (compliance-logged by the caller like
any other hard deny); a ``warn`` or an unknown-visibility miss logs a
decision event and falls through. Never raises — a broken rule table must
never brick the guard (fail-open like every other layer).
"""
if omi_dir is None:
return None
try:
from omind import rules

hit = rules.evaluate(action, omi_dir, _repo_root_for_action(action))
if hit is None:
return None
session = str(action.get("session") or "")
if hit.outcome == rules.ACTION_DENY:
return Verdict(
allow=False,
reason=(
f"omi-guard (hard): note rule '{hit.rule.id}' "
f"[{hit.rule.note}]: {hit.rule.message}"
),
rule_id=f"note-rule:{hit.rule.id}",
)
compliance.log_event(
compliance.KIND_DECISION,
session=session,
tool=str(action.get("tool") or ""),
command=str(action.get("command") or ""),
rule_id=f"note-rule:{hit.rule.id}",
severity="soft",
outcome=hit.outcome,
detail=(hit.detail or hit.rule.message)[:200],
)
return None
except Exception:
return None


#: Hard ceiling for the embedded excerpt so a huge note can't bloat every deny.
_EXCERPT_CAP = 1_600

Expand Down Expand Up @@ -1251,7 +1293,9 @@ def check_action(action: dict[str, Any], omi_dir: Path | None = None) -> Verdict
(:mod:`omind.adapters`), so every harness logs + decides identically. The
routine ``omi-gate`` "you didn't consult" deny is friction, not logged.
"""
verdict = decide(action)
verdict = _note_rules_verdict(action, omi_dir)
if verdict is None:
verdict = decide(action)
if (
not verdict.allow
and verdict.rule_id == "repo-work-read-git-rules"
Expand Down
Loading
Loading