Skip to content
Closed
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ uv run pytest -m state # State management tests only

## Skills

**Skills:** Explore `config/skills/*/SKILL.md` for available skills (10 total: agents-md, code-reviewer, continue-crash, crossfire, dev-docs, doc-writer, pr-creator, prompt-critique, prompt-engineer, test-writer).
**Skills:** Explore `config/skills/*/SKILL.md` for available skills (11 total: agents-md, code-reviewer, continue-crash, crossfire, dev-docs, doc-writer, kb, pr-creator, prompt-critique, prompt-engineer, test-writer).
- **SHARED across agents** - symlinked to `~/.claude/skills/`, `~/.config/goose/skills/`, `~/.config/agents/skills/` (Amp), `~/.agents/skills/` (Codex)
- Managed by SharedAgent (displays under "Shared:" in status)
- To add a skill: Create subdir in `config/skills/` with `SKILL.md`
Expand Down
2 changes: 2 additions & 0 deletions src/ai_rules/bootstrap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .installer import (
UV_NOT_FOUND_ERROR,
ToolSource,
ensure_basic_memory_installed,
ensure_statusline_installed,
get_effective_install_source,
get_tool_config_dir,
Expand All @@ -35,6 +36,7 @@
"parse_version",
"UV_NOT_FOUND_ERROR",
"ToolSource",
"ensure_basic_memory_installed",
"ensure_statusline_installed",
"get_effective_install_source",
"get_tool_config_dir",
Expand Down
142 changes: 142 additions & 0 deletions src/ai_rules/bootstrap/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def make_github_install_url(repo: str) -> str:


UV_NOT_FOUND_ERROR = "uv not found in PATH. Install from https://docs.astral.sh/uv/"
BASIC_MEMORY_GITHUB_REPO = "basicmachines-co/basic-memory"


def _validate_package_name(package_name: str) -> bool:
Expand Down Expand Up @@ -395,3 +396,144 @@ def ensure_statusline_installed(
return "failed", None
except Exception:
return "failed", None


def _run_basic_memory_setup(verbose: bool = False) -> None:
"""Run the idempotent basic-memory setup script (git init, GitHub remote).

Reads basic_memory config from ~/.ai-rules-config.yaml and passes
as env vars to the setup script.
"""
setup_script = (
Path(__file__).parent.parent
/ "config"
/ "claude"
/ "hooks"
/ "basic-memory-setup.sh"
)
if not setup_script.exists():
return

env = dict(os.environ)
try:
import yaml

user_config_path = Path.home() / ".ai-rules-config.yaml"
if user_config_path.exists():
with open(user_config_path) as f:
user_config = yaml.safe_load(f) or {}
bm_config = user_config.get("basic_memory", {})
if bm_config.get("repo"):
env["BASIC_MEMORY_WIKI_REPO"] = bm_config["repo"]
if bm_config.get("path"):
env["BASIC_MEMORY_HOME"] = str(Path(bm_config["path"]).expanduser())
except Exception:
pass

try:
subprocess.run(
["bash", str(setup_script)],
timeout=60,
capture_output=not verbose,
env=env,
)
except (subprocess.TimeoutExpired, Exception):
pass


def _is_basic_memory_configured(config: object) -> bool:
"""Check if basic-memory is configured in the merged MCP config.

Checks both profile mcp_overrides and the base mcps.json file.
"""
if hasattr(config, "mcp_overrides") and "basic-memory" in config.mcp_overrides:
return True

try:
import importlib.resources

config_pkg = importlib.resources.files("ai_rules") / "config"
for mcps_path in [
config_pkg / "mcps.json",
config_pkg / "claude" / "mcps.json",
]:
traversable = mcps_path
if hasattr(traversable, "is_file") and traversable.is_file():
import json

data = json.loads(traversable.read_text())
if "basic-memory" in data:
return True
except Exception:
pass

return False


def ensure_basic_memory_installed(
dry_run: bool = False,
from_github: bool = False,
config: object | None = None,
) -> tuple[str, str | None]:
"""Install or upgrade basic-memory if needed. Runs setup script after. Fails open.

Args:
dry_run: If True, show what would be done without executing
from_github: Install from GitHub instead of PyPI
config: Config object; if provided and basic-memory is not configured, skip

Returns:
Tuple of (status, message) where status is:
"already_installed", "installed", "upgraded", "upgrade_available", "failed", or "skipped"
"""
if config is not None and not _is_basic_memory_configured(config):
return "skipped", None
if is_command_available("basic-memory"):
try:
from ai_rules.bootstrap.updater import (
check_tool_updates,
get_tool_by_id,
perform_tool_upgrade,
)

bm_tool = get_tool_by_id("basic-memory")
if bm_tool:
update_info = check_tool_updates(bm_tool, timeout=10)
if update_info and update_info.has_update:
if dry_run:
return (
"upgrade_available",
f"Would upgrade basic-memory {update_info.current_version} → {update_info.latest_version}",
)
success, msg, _ = perform_tool_upgrade(bm_tool)
if success:
if not dry_run:
_run_basic_memory_setup()
return (
"upgraded",
f"{update_info.current_version} → {update_info.latest_version}",
)
except Exception:
pass
if not dry_run:
_run_basic_memory_setup()
return "already_installed", None

try:
success, message = install_tool(
"basic-memory",
from_github=from_github,
github_url=make_github_install_url(BASIC_MEMORY_GITHUB_REPO)
if from_github
else None,
force=False,
dry_run=dry_run,
)
if success:
if not dry_run:
_run_basic_memory_setup(verbose=True)
return "installed", message if dry_run else None
else:
return "failed", None
except Exception:
return "failed", None
27 changes: 27 additions & 0 deletions src/ai_rules/bootstrap/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from dataclasses import dataclass

from .installer import (
BASIC_MEMORY_GITHUB_REPO,
UV_NOT_FOUND_ERROR,
ToolSource,
_is_basic_memory_configured,
_validate_package_name,
get_tool_source,
get_tool_version,
Expand Down Expand Up @@ -65,6 +67,7 @@ class ToolSpec:
get_version: Callable[[], str | None]
is_installed: Callable[[], bool]
github_repo: str | None = None
is_enabled: Callable[[], bool] | None = None

@property
def github_install_url(self) -> str | None:
Expand Down Expand Up @@ -387,6 +390,19 @@ def perform_tool_upgrade(tool: ToolSpec) -> tuple[bool, str, bool]:
return False, f"Unexpected error: {e}", False


def _is_basic_memory_configured_for_active_profile() -> bool:
"""Check if basic-memory is configured for the currently active profile."""
try:
from ai_rules.config import Config
from ai_rules.state import get_active_profile

profile = get_active_profile() or "default"
config = Config.load(profile=profile)
return _is_basic_memory_configured(config)
except Exception:
return False


_SELF_SPEC = ToolSpec(
tool_id="ai-agent-rules",
package_name="ai-agent-rules",
Expand All @@ -396,6 +412,16 @@ def perform_tool_upgrade(tool: ToolSpec) -> tuple[bool, str, bool]:
github_repo=_SELF_GITHUB_REPO,
)

_BASIC_MEMORY_SPEC = ToolSpec(
tool_id="basic-memory",
package_name="basic-memory",
display_name="basic-memory",
get_version=lambda: get_tool_version("basic-memory"),
is_installed=lambda: is_command_available("basic-memory"),
github_repo=BASIC_MEMORY_GITHUB_REPO,
is_enabled=lambda: _is_basic_memory_configured_for_active_profile(),
)


def get_updatable_tools() -> list[ToolSpec]:
"""Get all updatable tool specs, deriving from registered Tool classes."""
Expand All @@ -404,6 +430,7 @@ def get_updatable_tools() -> list[ToolSpec]:
tools: list[ToolSpec] = [_SELF_SPEC]
if StatuslineTool.INSTALL_SPEC is not None:
tools.append(StatuslineTool.INSTALL_SPEC)
tools.append(_BASIC_MEMORY_SPEC)
return tools


Expand Down
47 changes: 43 additions & 4 deletions src/ai_rules/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,20 @@ def version_callback(ctx: click.Context, param: click.Parameter, value: bool) ->
except Exception as e:
logger.debug(f"Failed to get statusline version: {e}")

try:
from ai_rules.bootstrap import get_tool_version, is_command_available

if is_command_available("basic-memory"):
bm_version = get_tool_version("basic-memory")
if bm_version:
console.print(f"basic-memory, version {bm_version}")
else:
console.print(
"basic-memory, version [dim](installed, version unknown)[/dim]"
)
except Exception as e:
logger.debug(f"Failed to get basic-memory version: {e}")

try:
from ai_rules.bootstrap import check_tool_updates, get_tool_by_id

Expand Down Expand Up @@ -1046,6 +1060,7 @@ def install(
from rich.prompt import Confirm

from ai_rules.bootstrap import (
ensure_basic_memory_installed,
ensure_statusline_installed,
get_effective_install_source,
)
Expand Down Expand Up @@ -1101,6 +1116,19 @@ def install(
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)

bm_result, bm_message = ensure_basic_memory_installed(
dry_run=dry_run, config=config
)
if bm_result == "installed":
if dry_run and bm_message:
console.print(f"[dim]{bm_message}[/dim]\n")
else:
console.print("[green]✓[/green] Installed basic-memory\n")
elif bm_result == "failed":
console.print(
"[yellow]⚠[/yellow] Failed to install basic-memory (continuing anyway)\n"
)

if not dry_run:
set_active_profile(profile)

Expand Down Expand Up @@ -1660,12 +1688,21 @@ def status(agents: str | None) -> None:
console.print("[bold cyan]Optional Tools[/bold cyan]\n")
from ai_rules.bootstrap import is_command_available

statusline_missing = False
optional_tools_missing = False
if is_command_available("claude-statusline"):
console.print(" [green]✓[/green] claude-statusline installed")
else:
console.print(" [yellow]○[/yellow] claude-statusline not installed")
statusline_missing = True
optional_tools_missing = True

from ai_rules.bootstrap.installer import _is_basic_memory_configured

if _is_basic_memory_configured(config):
if is_command_available("basic-memory"):
console.print(" [green]✓[/green] basic-memory installed")
else:
console.print(" [yellow]○[/yellow] basic-memory not installed")
optional_tools_missing = True

console.print()

Expand Down Expand Up @@ -1707,7 +1744,7 @@ def status(agents: str | None) -> None:
"[yellow]💡 Run 'ai-agent-rules install' to fix issues[/yellow]"
)
sys.exit(1)
elif statusline_missing:
elif optional_tools_missing:
console.print("[green]All symlinks are correct![/green]")
console.print(
"[yellow]💡 Run 'ai-agent-rules install' to install optional tools[/yellow]"
Expand Down Expand Up @@ -1838,7 +1875,7 @@ def list_agents_cmd() -> None:
)
@click.option(
"--only",
type=click.Choice(["ai-agent-rules", "ai-rules", "statusline"]),
type=click.Choice(["ai-agent-rules", "ai-rules", "statusline", "basic-memory"]),
help="Only upgrade specific tool",
)
def upgrade(
Expand Down Expand Up @@ -1875,6 +1912,8 @@ def upgrade(
if resolved_only is None or t.tool_id == resolved_only
]
tools = [t for t in all_tools if t.is_installed()]
if resolved_only is None:
tools = [t for t in tools if t.is_enabled is None or t.is_enabled()]
missing_tools = [t for t in all_tools if not t.is_installed()]

for tool in missing_tools:
Expand Down
10 changes: 10 additions & 0 deletions src/ai_rules/config/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ Three similar lines > premature abstraction | No helpers for one-time ops | Only

**Why:** LLMs confidently generate plausible-sounding but incorrect assumptions. Explicit verification prevents wasted work and builds trust through transparency.

### Persistent Knowledge Base (basic-memory)

A persistent markdown knowledge base exists at `~/basic-memory/`, powered by the basic-memory MCP server with hybrid BM25+vector search. Knowledge persists across sessions, repos, and machines via git sync.

**Write-back loop:** When you synthesize a useful answer, debug a non-obvious issue, or the user corrects a false assumption, consider persisting it via `write_note`. Search first to avoid duplicates. Invoke the `/kb` skill for formatting conventions.

**Quality conventions:** Use `[GAP: ...]` annotations for unverified claims. Use `[misconception]` tags to document what is NOT true (prevents confident false assertions). Use `[promote]` to flag patterns worth promoting to AGENTS.md rules.

**Key tools:** `search_notes(query)` for hybrid search, `build_context(url)` for graph traversal, `read_note(identifier)` for specific notes, `write_note(title, directory, content)` to persist knowledge.

---

## Technical Standards
Expand Down
11 changes: 11 additions & 0 deletions src/ai_rules/config/claude/hooks/basic-memory-post-write.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Commit + push knowledge base changes after a basic-memory write.
# Called by PostToolUse hook on mcp__basic-memory__* tools.
WIKI_DIR="${BASIC_MEMORY_HOME:-$HOME/basic-memory}"
[ -d "$WIKI_DIR/.git" ] || exit 0
cd "$WIKI_DIR"
git add -A
git diff --cached --quiet && exit 0
git commit -m "auto: update knowledge base"
nohup git push >/dev/null 2>&1 &
exit 0
13 changes: 13 additions & 0 deletions src/ai_rules/config/claude/hooks/basic-memory-runner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Wrapper: sync pending changes, pull latest, then launch basic-memory MCP server.
# Used as the MCP command in mcps.json for all agents.
export PATH="$HOME/.local/bin:$PATH"
WIKI_DIR="${BASIC_MEMORY_HOME:-$HOME/basic-memory}"
if [ -d "$WIKI_DIR/.git" ]; then
(
cd "$WIKI_DIR"
git push >/dev/null 2>&1 || true
git pull --rebase --autostash >/dev/null 2>&1 || git rebase --abort >/dev/null 2>&1
) &
fi
exec basic-memory mcp "$@"
Loading
Loading