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
6 changes: 3 additions & 3 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ here so neither side drifts.
### Windows fresh-install test (2026-08-15, first fully-cold install)

- [x] **`setup --dry-run` warns about a missing claude CLI but the real run hard-fails** — fixed in 8.7.1: claude is a soft prerequisite, real run degrades like the dry-run promises ([#258](https://github.com/CryptoJones/omind/issues/258)) — either fail the dry-run too, or degrade the real run gracefully (do vault/seed work, skip only MCP registration)
- [ ] **Windows: POSIX `.sh` hooks + `fleet-sudo` installed unverified** ([#259](https://github.com/CryptoJones/omind/issues/259)) — doctor should probe hook executability (needs an `sh` on PATH); setup should warn or ship PowerShell equivalents; also `�` mojibake in doctor output under PS 5.1
- [x] **Windows: POSIX `.sh` hooks + `fleet-sudo` installed unverified** — fixed in 8.7.2: direct omind hook commands on Windows, sh-gated bash guards, doctor probe, UTF-8 console ([#259](https://github.com/CryptoJones/omind/issues/259)) — doctor should probe hook executability (needs an `sh` on PATH); setup should warn or ship PowerShell equivalents; also `�` mojibake in doctor output under PS 5.1
- [x] **Windows: codex hook verifier false-negatives on its own SessionStart/PostToolUse entries** — fixed in 8.7.1: shared Windows-tolerant command_is_omind_hook predicate ([#261](https://github.com/CryptoJones/omind/issues/261)) — hooks are written correctly but setup/--force/doctor all report them missing (quoting-form mismatch in the matcher); guard selftest passes for all six harnesses on Windows

### From the 2026-08-01 top-to-bottom code review
Expand Down Expand Up @@ -117,14 +117,14 @@ review's headline recommendations are deliberately NOT tracked here: temporal va
already shipped (#169), and adopting an external memory framework as the storage layer is
already rejected below._

- [ ] **Capture the agent's own work without requiring it to remember a tool call** ([#221](https://github.com/CryptoJones/omind/issues/221)) — _enhancement (memory)_ —
- [x] **Capture the agent's own work without requiring it to remember a tool call** ([#221](https://github.com/CryptoJones/omind/issues/221)) — _enhancement (memory)_ —
7 of 10 lanes named this the largest gap. Every write needs the agent to *decide* to call
`create-note`; a forgotten call loses the memory silently, with no error and no warning.
Explicitly NOT the external-document ingest rejected below — this captures the agent's own
work, which is what omind's notes already are. Open question is whether it belongs here or
in the harness; a cheaper middle option is a detector that flags "this session wrote no
notes" without omind ever reading a transcript.
- [ ] **Flat note namespace: every agent on every machine sees every note** ([#222](https://github.com/CryptoJones/omind/issues/222)) — _enhancement (mesh)_ —
- [x] **Flat note namespace: every agent on every machine sees every note** ([#222](https://github.com/CryptoJones/omind/issues/222)) — _enhancement (mesh)_ —
single-lane finding. Two axes: retrieval precision (unrelated notes compete in every
search, where Mem0/LangMem/Letta all partition by user/agent/run) and blast radius (no way
to scope a note to a project, machine, or agent). Distinct from #196, which covered the
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ 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.7.2] - 2026-08-15

### Fixed
- **Native Windows gets working Claude hooks (#259).** Setup registered the
POSIX `.sh` adapters on Windows unverified. Now: the OMI-compliance guard
and gate-reset register direct omind invocations (`guard adapter` /
`guard preflight` — the pure-Python path the Codex harness already uses);
the secret-output + fresh-base git guards run through `sh` when Git for
Windows provides one and are skipped with a clear warning (and a doctor
`tool:sh` check) when it doesn't; `fleet-sudo` and the dead `.sh` copies are
no longer written on Windows; and CLI output is forced to UTF-8 on Windows,
fixing the doctor mojibake under PowerShell 5.1.

## [8.7.1] - 2026-08-15

### Fixed
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.7.1"
version = "8.7.2"
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.7.1"
__version__ = "8.7.2"
8 changes: 8 additions & 0 deletions src/omind/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from __future__ import annotations

import argparse
import contextlib
import os
import shlex
import sys
Expand Down Expand Up @@ -1578,6 +1579,13 @@ def _run_help(args: argparse.Namespace) -> int:


def main(argv: list[str] | None = None) -> int:
if os.name == "nt":
# Windows PowerShell 5.1's default console codepage mangles the em
# dashes and check glyphs doctor/setup print (#259's mojibake finding);
# UTF-8 output fixes the rendering everywhere modern Windows runs.
for stream in (sys.stdout, sys.stderr):
with contextlib.suppress(Exception):
stream.reconfigure(encoding="utf-8") # type: ignore[union-attr]
parser = build_parser()
args = parser.parse_args(argv)
if args.command == "help":
Expand Down
117 changes: 108 additions & 9 deletions src/omind/provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,17 @@ def _hook_exe_path(command_text: str) -> str | None:
#: (each adapter's filename always appears in its command).
OMI_GUARD_MARKER = "omi-guard.sh"
OMI_GATE_RESET_MARKER = "omi-gate-reset.sh"

#: Native-Windows hook-command markers. Windows cannot execute the POSIX .sh
#: adapters, so provisioning there registers direct omind invocations — the
#: same pure-Python guard path the Codex harness uses (#259). The markers let
#: replacement/diagnosis recognize either form on either platform.
WINDOWS_OMI_GUARD_MARKER = "guard adapter --harness claude"
WINDOWS_GATE_RESET_MARKER = "guard preflight"


def _windows() -> bool:
return os.name == "nt"
#: The retired hand-rolled PreToolUse('*') guard, replaced by ``omi-guard.sh``.
#: Provision strips it from settings.json so a prototype machine doesn't run two
#: guards. (Its name is NOT a substring of OMI_GUARD_MARKER, so the two markers
Expand Down Expand Up @@ -743,6 +754,8 @@ def _write_enforce_hook_script(self) -> None:

def _write_guard_hook_script(self) -> None:
"""Write the fresh-base git guard hook from package data to ~/.claude/hooks/."""
if _windows() and shutil.which("sh") is None:
return # POSIX script; no sh on this Windows box — install skipped
dest = _guard_hook_dest()
try:
content = (
Expand All @@ -761,6 +774,8 @@ def _write_secret_output_guard_script(self) -> None:
Unlike ``omi-guard.sh`` the script has no machine-specific paths, so it
ships verbatim with no install-time substitution (like ``git-fresh-base.sh``).
"""
if _windows() and shutil.which("sh") is None:
return # POSIX script; no sh on this Windows box — install skipped
dest = _secret_output_guard_dest()
try:
content = (
Expand All @@ -781,6 +796,8 @@ def _write_fleet_sudo_script(self) -> None:
ever guesses the entry or hands CJ a command to run. The guard blocks raw
`sudo` and points here; see the OMI Playbook.
"""
if _windows():
return # bash + pass + sudo wrapper — meaningless on Windows
dest = _fleet_sudo_dest()
try:
content = (
Expand Down Expand Up @@ -918,22 +935,35 @@ def ensure_guard_hook_installed(self) -> None:
something changed (or ``--force``).
"""
path = claude_settings_path()
if _windows() and shutil.which("sh") is None:
self.log(
" SKIP: the secret-output + fresh-base git guards are POSIX "
"shell and no `sh` is on PATH — install Git for Windows to "
"enable them (doctor flags this gap)."
)
return
data = self._read_settings(path)
hooks_cfg = data.get("hooks")
if not isinstance(hooks_cfg, dict):
hooks_cfg = {}

def _script_command(dest: Path) -> str:
# A bare .sh path is not executable by the Windows shell; run it
# through the sh found on PATH (Git for Windows). The script
# filename stays in the command, so the markers still match.
return f'sh "{dest}"' if _windows() else str(dest)

desired: dict[str, Any] = {
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": str(_secret_output_guard_dest()),
"command": _script_command(_secret_output_guard_dest()),
"timeout": SECRET_OUTPUT_GUARD_TIMEOUT,
},
{
"type": "command",
"command": str(_guard_hook_dest()),
"command": _script_command(_guard_hook_dest()),
"timeout": GUARD_HOOK_TIMEOUT,
},
],
Expand Down Expand Up @@ -988,6 +1018,18 @@ def _write_omi_guard_scripts(self) -> None:
retires the legacy prototype adapter and stamps the provision manifest so
upgrades can detect hook-set drift (#86/#87)."""
self._remove_legacy_omi_guard()
if _windows():
# The adapters are POSIX shell; Windows registers direct omind
# invocations instead (see ensure_omi_guard_installed), so writing
# the scripts would install dead files a reviewer must puzzle over.
self.log(
" skip omi-guard.sh/omi-gate-reset.sh (POSIX adapters; "
"Windows hooks call omind directly)"
)
if not self.config.dry_run:
write_provision_manifest()
policy.write_seed_policy()
return
omind_exe = canonical_omind_exe()
omi_dir = str(self.config.omi_dir)
for resource, dest in (
Expand Down Expand Up @@ -1029,24 +1071,44 @@ def ensure_omi_guard_installed(self) -> None:
if not isinstance(hooks_cfg, dict):
hooks_cfg = {}

if _windows():
# No .sh adapters on native Windows: call omind directly — the same
# pure-Python guard path the Codex harness uses (#259).
omind_exe = canonical_omind_exe()
guard_command = (
f'"{omind_exe}" guard adapter --harness claude '
f'--omi-dir "{self.config.omi_dir}"'
)
reset_command = (
f'"{omind_exe}" guard preflight --omi-dir "{self.config.omi_dir}"'
)
else:
guard_command = str(_omi_guard_dest())
reset_command = str(_omi_gate_reset_dest())
desired: dict[str, dict[str, Any]] = {
"PreToolUse": {
"matcher": "*",
"hooks": [
{
"type": "command",
"command": str(_omi_guard_dest()),
"command": guard_command,
"timeout": OMI_GUARD_TIMEOUT,
}
],
},
"UserPromptSubmit": {
"hooks": [{"type": "command", "command": str(_omi_gate_reset_dest())}]
"hooks": [{"type": "command", "command": reset_command}]
},
}
# Both platform forms are strippable so a checkout moved between
# platforms converges instead of accumulating one entry per form.
strip_markers = {
"PreToolUse": (OMI_GUARD_MARKER, LEGACY_OMI_GUARD_MARKER),
"UserPromptSubmit": (OMI_GATE_RESET_MARKER,),
"PreToolUse": (
OMI_GUARD_MARKER,
LEGACY_OMI_GUARD_MARKER,
WINDOWS_OMI_GUARD_MARKER,
),
"UserPromptSubmit": (OMI_GATE_RESET_MARKER, WINDOWS_GATE_RESET_MARKER),
}

changed = False
Expand Down Expand Up @@ -1249,6 +1311,31 @@ def _diagnose_tools(tools: dict[str, str]) -> list[CheckResult]:
return results


def _diagnose_windows_sh() -> list[CheckResult]:
"""#259: on native Windows the secret-output + fresh-base git guards are
POSIX shell, executed through an `sh` on PATH (Git for Windows). Without
one they are skipped at setup, so surface the gap; on POSIX say nothing."""
if not _windows():
return []
if shutil.which("sh") is not None:
return [
CheckResult(
"tool:sh",
"ok",
"sh found on PATH — the secret-output + fresh-base git guards run through it",
)
]
return [
CheckResult(
"tool:sh",
"warn",
"no `sh` on PATH — the secret-output + fresh-base git guards are "
"skipped on this Windows box (install Git for Windows, then re-run "
"`omind setup`); the OMI-compliance guard itself runs natively",
)
]


def _diagnose_jq() -> CheckResult:
"""#107: `jq` is the guard hook's fast-path parser, but no longer a hard
dependency — the hook falls back to `omind guard adapter` (pure Python) when
Expand Down Expand Up @@ -1311,6 +1398,7 @@ def diagnose(config: SetupConfig) -> list[CheckResult]:
"""
results = _diagnose_tools(Provisioner.REQUIRED_TOOLS)
results.append(_diagnose_jq())
results.extend(_diagnose_windows_sh())
results.extend(_diagnose_omi_folder(config))
omi = config.omi_dir

Expand Down Expand Up @@ -1491,7 +1579,11 @@ def _diagnose_omi_guard(settings_path: Path, config: SetupConfig) -> CheckResult
the auto-memory hooks. A green here must mean the per-turn consult gate and the
hard blocks really run, so a missing/unwired guard is a ``fail``, not a silent
pass."""
missing = [str(p) for p in _managed_guard_hooks().values() if not p.is_file()]
missing = (
[]
if _windows() # Windows registers direct omind commands, no .sh files
else [str(p) for p in _managed_guard_hooks().values() if not p.is_file()]
)
if missing:
return CheckResult(
"omi_guard",
Expand All @@ -1515,12 +1607,19 @@ def _diagnose_omi_guard(settings_path: Path, config: SetupConfig) -> CheckResult
pre_ok = any(
isinstance(e, dict)
and e.get("matcher") == "*"
and OMI_GUARD_MARKER in _entry_command_text(e)
and (
OMI_GUARD_MARKER in _entry_command_text(e)
or WINDOWS_OMI_GUARD_MARKER in _entry_command_text(e)
)
for e in pre_list
)
ups_list = hooks_cfg.get("UserPromptSubmit")
ups_list = ups_list if isinstance(ups_list, list) else []
ups_ok = any(OMI_GATE_RESET_MARKER in _entry_command_text(e) for e in ups_list)
ups_ok = any(
OMI_GATE_RESET_MARKER in _entry_command_text(e)
or WINDOWS_GATE_RESET_MARKER in _entry_command_text(e)
for e in ups_list
)
if not (pre_ok and ups_ok):
unwired: list[str] = []
if not pre_ok:
Expand Down
Loading
Loading