agentleFS
axiomanticaxiomantic/spellbook/AGENTS.md

spellbook

AGENTS.md11 starsChanged 3 months agoMITView on GitHub

On session start in this project, check if there's a GitHub pre-release newer than the latest full release: If a pre-release exists that is newer than the last actual release, ask: "There's a pre-release (vX.Y.Z) ready. Want to promote it to a full release?" Only doctoc writes files. Every other hook reports and leaves the tree alone. If a hook fails: - doctoc failures: Table of contents in markdown files needs regeneration. Usually fixes itself on re-commit. - Check generated…

  • Installs packages

We split this AGENTS.md into the rules it gives an agent

20 rules. About 0 are common, 7 are distinctive, and 13 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 13

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If a pre-release exists that is newer than the last actual release, ask: "There's a pre-release (vX.Y.Z) ready.

    line 33
  2. Very specific:

    Only doctoc writes files. Every other hook reports and leaves the tree alone.

    line 48
  3. Distinctive:

    Hook failure: Run the command the hook names, stage the result, retry commit.

    line 171
  4. Very specific:

    Skills: Create skills/<name>/SKILL.md with name/description frontmatter.

    line 181
  5. Very specific:

    Commands: Create commands/<name>.md with description frontmatter.

    line 183
  6. Very specific:

    Skill invokes commands: Use /command-name syntax to delegate.

    line 256
  7. Distinctive:

    When you add a new persistent key, decide up front:

    line 300
  8. Very specific:

    If the distinction is unclear, pick config and document the choice.

    line 304
  9. Distinctive:

    Every user-facing configuration option MUST follow the three-point rule:

    line 310
  10. Very specific:

    Prompted on fresh install: If the user has never answered, offer a prompt.

    line 312
  11. Very specific:

    Subprocesses: Use the @pytest.mark.allow("subprocess") marker on your test function.

    line 354
  12. Very specific:

    Docker tests: only run in CI via integration-test.yml in a dedicated container.

    line 376
  13. Very specific:

    If a value is genuinely incidental (wall-clock timestamp, tmp path, object identity), use the INSTANCE AnyThing() — never the bare class, …

    line 444
  14. Very specific:

    When harvesting real values from tripwire's failure hints, READ them before pasting: hint reprs can contain os.environ, credentials, and …

    line 449
  15. Very specific:

    Use tripwire's domain-specific plugins when applicable instead of generic mocks.

    line 474
  16. Distinctive:

    Third types module: If module A defines types that B needs, and B defines functions that A calls, put the shared types in a third module C.

    line 518
  17. Distinctive:

    When designing a new module pair, draw the dependency graph BEFORE writing code.

    line 526
  18. Distinctive:

    If a cycle is unavoidable, design the shared surface FIRST.

    line 527
  19. Distinctive:

    Before any commit, verify:

    line 536
  20. Very specific:

    Every library change ships to users across four platforms.

    line 543

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

Spellbook Development

Quick Start

# Install dependencies
uv pip install -e ".[dev,test]"

# Run tests (targeted)
uv run pytest tests/test_specific_file.py -x

# Run fast tests (default - skips docker, integration, slow, external)
uv run pytest tests/ -x

# Run ALL tests including slow/integration (as CI does)
uv run pytest tests/ -x --override-ini="addopts=" -m "not docker"

# Run linting
uv run ruff check .

# Generate documentation (pre-commit only checks freshness; run this yourself)
uv run scripts/generate_docs.py

Session Start: Pre-release Check

On session start in this project, check if there's a GitHub pre-release newer than the latest full release:

gh release list --limit 5 --repo axiomantic/spellbook

If a pre-release exists that is newer than the last actual release, ask: "There's a pre-release (vX.Y.Z) ready. Want to promote it to a full release?"

Key Conventions

  • AGENTS.md (this file) is for working on the spellbook repo itself. Only update it when changing the development workflow for this specific project (build commands, test conventions, architecture notes). It is NOT installed anywhere.
  • rules/*.md are the global user-facing rule modules that the spellbook installer ships. Directory-capable platforms receive one symlink per selected module in the platform's rules directory (~/.claude/rules/ for Claude Code); flat platforms receive a generated concatenation at their real instruction path. Each module carries YAML frontmatter (id, class, default, benefit) and holds global directives, instructions, and behavioral rules that apply to ALL projects. This is where cross-project instructions belong.
    • Editing an existing rules/*.md file takes effect immediately on directory-capable platforms — the installed file is a symlink to the source, so no reinstall step is needed.
    • Adding a NEW file under rules/ does need a step: run uv run install.py to create its symlink (or regenerate the flat-platform concatenation) before the new module reaches any installed platform. MANIFEST.md does not track rules/*.md — it only registers skills, commands, and agents — so there is no manifest entry to add for a new rule module, just the install step.
  • Skills go in skills/<name>/SKILL.md with YAML frontmatter
  • Commands go in commands/<name>.md with YAML frontmatter
  • Hooks go in hooks/ and must be registered in installer/components/hooks.py
  • MCP tools are defined in spellbook/mcp/server.py and spellbook/mcp/tools/ modules

Pre-commit Hooks

Only doctoc writes files. Every other hook reports and leaves the tree alone. If a hook fails:

  • doctoc failures: Table of contents in markdown files needs regeneration. Usually fixes itself on re-commit.
  • Check generated documentation is current: The docs/ mirror is stale. The hook runs scripts/generate_docs.py --check, which names each stale page and writes nothing. Run uv run scripts/generate_docs.py, stage the regenerated files, and re-commit.
  • Check documentation completeness: A skill/command has no generated doc page. Run uv run scripts/generate_docs.py and stage the result; the hook only checks.
  • Validate skill/command/agent/rule schemas: Checks YAML frontmatter in skills, commands, agents, and rule modules. Fix the frontmatter.

Apart from doctoc, a failing hook does not repair anything for you. Run the command it names, stage the result (git add), and commit again.

Switching Between Worktrees

To switch which worktree the installed spellbook runs from, always run uv run install.py from the target worktree. Don't just re-point ~/.local/spellbook/source — the symlink shortcut updates the daemon's code path but misses per-platform MCP registrations (claude mcp add, OpenCode, Codex, Forge), so /mcp ends up missing spellbook or showing a stale tool list. install.py is idempotent; run it.

Architecture Notes

  • The MCP server (spellbook/) runs as a persistent daemon, not inline with the CLI
  • The installer (installer/) handles multi-platform installation (Claude Code, OpenCode, Codex, Gemini CLI)
  • Skills and commands are markdown files with YAML frontmatter, loaded dynamically by the AI assistant
  • Hooks are bash/python scripts installed into the AI assistant's hook system
  • The extensions/ directory contains platform-specific plugins (e.g., OpenCode workflow state)

Environment Variables

Operator-facing behavioral switches. Not an inventory of every SPELLBOOK_* variable the code reads — path resolution (SPELLBOOK_DIR, SPELLBOOK_CONFIG_DIR) and MCP transport settings are covered where they are used.

VariableEffect
SPELLBOOK_GATE_CMDShell command the OpenCode plugin (hooks/opencode-plugin.ts) runs as a command-execution gate. Unset is the default and means there is no gate: every command is allowed, and no subprocess is spawned. Set means the command MUST produce a verdict: it receives the JSON payload on stdin, exits 0 to allow, and exits 2 to block (with {"error": "..."} on stdout for the reason). Any other outcome — command missing, crash, non-0/2 exit, or the 5s timeout — BLOCKS. A check that cannot complete is not a check that passed. Spellbook ships no gate implementation; the command is entirely operator-supplied.
SPELLBOOK_GIT_PUSH_AUTONOMOUSOperator signal that a protected-branch push confirmation may be skipped in high-trust automation. No code reads it — protected-branch protection is behavioural, honored by the agent, not enforced by any hook or config. See rules/50-git-safety.md.
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPHarness variable, not spellbook's, but the installer WRITES it into settings.json's env block as "0". Claude Code stops honoring a Stop hook's block decision after this many consecutive blocks and ends the turn regardless; 0 disables that cap. Autonomous mode's Stop hook blocks by design and bounds itself with its own rolling-window valve (spellbook/core/autonomous.py), so the harness cap would only cut enforcement short at an arbitrary count. A value you set yourself is left alone, and the installer says so in its output for that run — but autonomous mode then stops being enforced after that many blocks. Written by installer/components/hooks.py.

Spellbook Development Guide

<ROLE> Spellbook Contributor. Your reputation depends on shipping changes that work across all supported platforms without breaking user installations. Every careless commit risks corrupting thousands of developer environments. </ROLE>

Development instructions for spellbook codebase. User-facing rule modules: rules/*.md.

Supported Platforms

Claude Code is the primary supported platform with full support. The others have basic support; some MCP tools, hooks, and skills are Claude Code-specific but can usually be implemented for other platforms. Contributions to extend coverage are welcome.

PlatformSupport LevelGitHubConfig LocationMCP Transport
Claude CodePrimary, fullanthropics/claude-code~/.claude/HTTP daemon
AntigravityBasicgoogle-deepmind/antigravity~/.gemini/antigravityHTTP daemon
OpenCodeBasicanomalyco/opencode~/.config/opencode/HTTP daemon
CodexBasicopenai/codex~/.codex/HTTP daemon
Gemini CLIBasicgoogle/gemini-cli~/.gemini/HTTP daemon

Invariant Principles

  1. Library vs Repo distinction: Library items (skills/, commands/) ship to users and require docs. Repo items (.claude/skills/) are internal only.
  2. Documentation follows code: Library changes require CHANGELOG, README, and docs updates. The check-docs-completeness pre-commit hook enforces the README and docs/ halves. Nothing enforces the CHANGELOG entry — no hook and no CI job reads it, so a library change ships with a stale CHANGELOG unless you update it yourself.
  3. Test before commit: uv run pytest tests/ + uv run install.py --dry-run before any commit.

Glossary

TermLocationShips to UsersDocs Required
Library skillskills/*/SKILL.mdYesCHANGELOG, README, docs
Library commandcommands/*.mdYesCHANGELOG, README, docs
Repo skill.claude/skills/*/SKILL.mdNoNone
Rule modulerules/*.mdYesCHANGELOG, docs

Structure

spellbook/
├── .claude/skills/      # Repo skills (internal, NOT shipped)
├── skills/              # Library skills (shipped)
├── commands/            # Library commands (shipped)
├── agents/              # Agent definitions
├── installer/           # Multi-platform installer
│   ├── platforms/       # claude_code, opencode, codex, gemini
│   └── components/      # context_files, symlinks, mcp
├── spellbook/           # Python package (three-layer architecture)
│   ├── core/            # Config, DB, auth, models, compat
│   ├── memory/          # Memory storage and consolidation
│   ├── sessions/        # Session parsing, resume, compaction
│   ├── security/        # Security scanning, canary, trust
│   ├── notifications/   # OS notifications
│   ├── daemon/          # Server daemon management
│   ├── mcp/             # MCP server and tool definitions
│   │   └── tools/       # 13 tool modules (memory, security, etc.)
│   └── cli/             # CLI entry point and command groups
├── scripts/             # Build/maintenance
├── tests/               # Unit, integration
├── docs/                # Generated (skills/commands/agents) + manual
├── lib/                 # Shared JS
└── extensions/          # Platform manifests

Key Files

FilePurpose
rules/User-facing rule modules (installed per-module)
extensions/gemini/Gemini extension (linked via gemini extensions link)
install.pyInstaller entry
spellbook/mcp/server.pyMCP server entry point
skills/dedupe/SKILL.md/dedupe — semantic instruction deduplication via LLM judgment loop (skill-only, no Python module)

Commands

uv run pytest tests/                              # Fast tests (heavy markers auto-skipped)
uv run pytest tests/unit/test_installer.py        # Specific file
uv run pytest --cov=installer --cov=spellbook tests/  # Coverage
uvx pre-commit install                            # Install hooks
python3 scripts/generate_docs.py                  # Gen docs
uv run install.py --dry-run                       # Test installer

Pre-commit Hooks

Updates TOC in README.md. Checks -- does not write -- the docs/ mirror of skills/commands/agents/rules.

Hook failure: Run the command the hook names, stage the result, retry commit.

Shell/PowerShell Parity

<CRITICAL> Any changes to shell scripts (`.sh` files in `hooks/`, `scripts/`, or elsewhere) MUST have corresponding changes to their Python cross-platform equivalents (`.py` wrappers in the same directory). If a shell script is added, modified, or deleted, the Python equivalent must be updated to match. This ensures Windows compatibility. </CRITICAL>

Adding Content

Skills: Create skills/<name>/SKILL.md with name/description frontmatter. Auto-discovered by MCP.

Commands: Create commands/<name>.md with description frontmatter. Hooks generate docs.

Regenerating the docs/ mirror is a required, normal part of any change to skills/, commands/, agents/, or rules/ — not scope creep. After such an edit, run uv run scripts/generate_docs.py and commit the regenerated docs/ pages in the SAME commit as the source. The blocking generate-docs pre-commit hook checks freshness, so a commit that omits the regenerated pages fails; the extra generated files are the deterministic product of your source change, never an out-of-scope addition.

Adding Config Options

<CRITICAL> Any new user-facing config key MUST satisfy all three points below. Missing any point means a silently-unseen feature, a spammy re-prompt, or a config the user can only set by hand-editing `~/.config/spellbook/spellbook.json`. </CRITICAL>

A "user-facing config" is anything that governs behavior the user cares about (feature flags, endpoints, modes, thresholds). Internal state and cached values are not user-facing.

The three-point rule

  1. NEW users are presented with the option during initial installation.
  2. EXISTING users re-running install are presented with the option if (and only if) they have never answered it before — i.e., the key is unset in their config.
  3. Users who have already answered (any explicit value, including False, "", 0, null) are NOT re-prompted during subsequent installs.

Required changes per new config key

WhereWhat to add
spellbook/core/config.pyCONFIG_DEFAULTS entry (matching key) — or, for a lazily-resolved key family, a config_default_for() resolver instead; see "Sanctioned exception" below
Installer (BOTH entry points — see below)Prompt gated by config_is_explicitly_set(key) or equivalent config_get(key) is None check
Relevant wizardDefault surfaced to the user so they see what "don't change" means

Divergent install entry points

Spellbook has two install entry paths that must be kept in sync:

  • Root install.py (the curl-pipe target, documented entry) — handles fresh installs and upgrades via python3 install.py.
  • CLI wrapper spellbook/cli/commands/install.py — invoked by spellbook install after spellbook is on PATH.

Any new config prompt MUST be wired into both. A prompt that lives only in the CLI wrapper is invisible to users following the documented curl-pipe install, and vice versa. When touching either file, verify parity with the other.

Idempotency pattern

from spellbook.core.config import config_get, config_set

# Skip if user has already answered (any explicit value counts as "answered")
if config_get("my_feature_enabled") is not None:
    return

# Prompt, then persist exactly once
answer = input("Enable my-feature? [y/N]: ").strip().lower()
config_set("my_feature_enabled", answer in ("y", "yes"))

For keys using dotted names or stored in profile_store, use config_is_explicitly_set(key) from spellbook.core.config — it distinguishes "unset" from "set to default-valued literal".

Non-tty fallbacks

Interactive prompts must be skipped cleanly when sys.stdin.isatty() is False (CI, piped installs). Default behavior in that path must match the "user said no / accept default" branch — NEVER silently write an opt-in flag to True.

Re-configure flag

The installer accepts --reconfigure. When set, idempotency checks are bypassed so users can re-answer. Support this by gating your skip-check on not getattr(args, "reconfigure", False).

Size Limits and Splitting

<CRITICAL> Pre-commit hooks enforce size limits to prevent truncation on platforms like OpenCode: - **Skills**: 1900 lines max, 49KB max - **Commands**: 1900 lines max, 49KB max

Do NOT trim content to fit. Split instead. </CRITICAL>

When a Skill Exceeds Limits

  1. Skill becomes orchestrator: SKILL.md is a thin wrapper defining workflow phases, delegating to commands.
  2. Commands contain the logic: Each phase or major section becomes a command (e.g., advanced-code-review-plan.md).
  3. Skill invokes commands: Use /command-name syntax to delegate.

Example structure:

skills/advanced-code-review/
  SKILL.md              # ~200 lines - orchestrator only
commands/
  advanced-code-review-plan.md      # Phase 1 logic
  advanced-code-review-context.md   # Phase 2 logic
  advanced-code-review-review.md    # Phase 3 logic
  advanced-code-review-verify.md    # Phase 4 logic
  advanced-code-review-report.md    # Phase 5 logic

Platform Installers

PlatformFileOutput
Claude Codeinstaller/platforms/claude_code.pyCLAUDE.md + MCP + skills/commands
Gemini CLIinstaller/platforms/gemini.pyNative extension via link
OpenCodeinstaller/platforms/opencode.pyAGENTS.md + MCP
Codexinstaller/platforms/codex.pyAGENTS.md + MCP

Config vs State

Spellbook stores persistent key-value data in two places. Pick the right one:

FileModulePurpose
~/.config/spellbook/spellbook.jsonspellbook.core.configUser intent (what the user configured)
~/.local/spellbook/state.jsonspellbook.core.stateRuntime state (what the code wrote for itself)

Config keys are user-facing preferences and feature flags: auto_update, session_mode, notify_enabled, worker_llm_*. They are surfaced by the install wizards. Reading uses config_get(key), writing uses config_set(key, value).

State keys are facts the code discovered or counters the code maintains: auto_update_branch (auto-detected from git), update_check_failures (watcher failure counter). They must never appear in wizards and must never be presented to the user as a configurable option. Reading uses get_state(key), writing uses set_state(key, value).

When you add a new persistent key, decide up front:

  • Did the user choose this? -> config
  • Did the code derive it? -> state

If the distinction is unclear, pick config and document the choice. Moving later is a migration (see spellbook.core.state.migrate_config_to_state).

Adding Config Options

<CRITICAL> Every user-facing configuration option MUST follow the three-point rule:
  1. Prompted on fresh install. If the user has never answered, offer a prompt. Either add the prompt to an existing wizard in installer/wizards/ (worker_llm.py, defaults.py) or create a new shared wizard module there.
  2. Prompted on re-install IF the key is still unset. The idempotency gate uses spellbook.core.config.config_is_explicitly_set(key). If it returns False, ask again. If it returns True, skip.
  3. NOT re-prompted once the user has answered. Any explicit value counts, including empty strings and False. Declining at the opener still writes a sentinel so the next run does not re-ask.

Wire every wizard into BOTH install entry paths:

  • Root install.py (the curl-pipe entry point used by new users).
  • spellbook/cli/commands/install.py (the spellbook install CLI command, including the --reconfigure branch).

A wizard that lives in only one entry path is a bug. --reconfigure must bypass the idempotency gate so users can revisit earlier answers.

Register every new key in spellbook/core/config.py::CONFIG_DEFAULTS (runtime default).

Sanctioned exception — lazily-resolved default families. A key whose default cannot be computed without doing real work at import time MUST NOT go in CONFIG_DEFAULTS. spellbook/core/config.py is imported by the MCP server and the hooks, so an import-time computation is paid by every consumer whether or not it ever reads that key. Register such families through config_default_for() instead, backed by a memoized resolver.

The one family using this today is rules.module.* (rule_module_config_defaults()): resolving it means globbing and parsing every file in rules/. config_default_for() is the single read path, so callers see no difference; only the timing moves. A test pins the keys OUT of CONFIG_DEFAULTS so the import-time cost cannot come back by accident.

Tests must cover each new key: fresh-install prompt fires, re-install skip, --reconfigure bypass, non-tty noop, and presence in both install entry paths. The idempotency-gate helpers are covered in tests/test_installer_config_helpers.py; the wizard behaviours (prompting, non-tty safety, --reconfigure, and the CONFIG_DEFAULTS exclusion for the lazily-resolved family) are covered in tests/installer/test_rule_modules.py. </CRITICAL>

Testing

Tests marked docker, integration, slow, and external are skipped by default via addopts in pyproject.toml. CI overrides this with --override-ini="addopts=" to run them.

Sandbox & Security (Tripwire)

This project uses pytest-tripwire (imported as tripwire) to strictly enforce a testing sandbox. By default, any attempt to spawn a subprocess or access the network will result in an error (guard = "error" in pyproject.toml).

How to permit specific actions:

  • Subprocesses: Use the @pytest.mark.allow("subprocess") marker on your test function.
  • Network: Use @pytest.mark.allow("network").

Example:

@pytest.mark.allow("subprocess")
def test_cli_invocation():
    # This test is now allowed to use subprocess.run/Popen
    ...

Local development: just run uv run pytest tests/ -- heavy tests are excluded automatically.

To opt in to specific markers locally:

# Run integration tests too
uv run pytest tests/ -m "not docker and not slow and not external" --override-ini="addopts="

# Run everything except docker
uv run pytest tests/ -m "not docker" --override-ini="addopts="

Docker tests only run in CI via integration-test.yml in a dedicated container. Never run locally.

Testing with Tripwire

<CRITICAL> **Tripwire is the ONLY acceptable mocking framework in this project.** This rule is absolute.

Forbidden:

  • unittest.mock in any form — patch(), patch.object(), MagicMock, AsyncMock, Mock, mock_open, PropertyMock, create_autospec, etc.
  • pytest-mock / mocker fixture
  • monkeypatch.setattr(), monkeypatch.setitem(), monkeypatch.delattr(), monkeypatch.delitem() for mocking module attributes, class methods, or functions
  • Hand-rolled mock objects or stub classes that exist only to stand in for real dependencies
  • @pytest.fixture that returns a fake replacement for a real dependency

Allowed uses of monkeypatch (pytest-builtin, not a mocking framework):

  • monkeypatch.setenv() / monkeypatch.delenv() for environment variables
  • monkeypatch.chdir() for working directory
  • monkeypatch.syspath_prepend() for sys.path

That's it. Any other mocking need — function replacement, method stubbing, object patching, HTTP mocking, subprocess mocking, database mocking — MUST use tripwire. No exceptions. If tripwire can't express what you need, that is a signal to refactor the code under test, not to reach for unittest.mock.

PR reviewers (including automated ones) that suggest "use tripwire OR monkeypatch" are wrong. The correct phrasing is "use tripwire; monkeypatch is restricted to environment / cwd / sys.path only." </CRITICAL>

Why Tripwire Instead of unittest.mock

Tripwire enforces three guarantees unittest.mock does not:

  1. Every call must be pre-authorized - unmocked calls raise UnmockedInteractionError
  2. Every interaction must be asserted - unasserted calls raise UnassertedInteractionsError at teardown
  3. Every mock must fire - unused mocks raise UnusedMocksError

Quick Reference

import tripwire

def test_example():
    # Setup mocks
    config = tripwire.mock("myapp.config:get_setting")
    config.returns("value")

    # Execute in sandbox
    with tripwire:
        result = my_function()

    # Assert interactions (REQUIRED - tripwire enforces this)
    config.assert_call(args=("key",), kwargs={})

The assertion must be meaningful

<CRITICAL> Tripwire enforces that you assert; it does not enforce that the assertion says anything. Assert the REAL values. A wildcard matcher (`AnyThing`, `mock.ANY`, or any equivalent) proves nothing and defeats the entire reason this project uses tripwire — an assertion built from wildcards passes against any implementation whatsoever.

This applies to EVERY position, not just the all-wildcard case. One wildcard where a real value was knowable is already a weakened assertion.

# WRONG: passes against any implementation
config.assert_call(args=AnyThing, kwargs=AnyThing, returned=AnyThing)

# CORRECT: real values
config.assert_call(args=("key",), kwargs={}, returned="value")

If a value is genuinely incidental (wall-clock timestamp, tmp path, object identity), use the INSTANCE AnyThing() — never the bare class, which silently evades tripwire's own all-wildcard guard — and say in a comment why the value is incidental. Prefer a type constraint (IsInstance(Type)) over a wildcard where the type is knowable.

When harvesting real values from tripwire's failure hints, READ them before pasting: hint reprs can contain os.environ, credentials, and absolute home paths.

Full rule: patterns/assertion-quality-standard.md. </CRITICAL>

Common Patterns

NeedPattern
Mock a module attributetripwire.mock("module.path:attribute")
Mock an object methodtripwire.mock.object(obj, "method")
Return a value.returns(value)
Raise an exception.raises(ExceptionType(...))
Custom side effect.calls(my_function)
Multiple return values.returns(a).returns(b).returns(c)
Spy (call real + record)tripwire.spy("module:attr")
Assert a call.assert_call(args=(...), kwargs={...})
Order-independent assertswith tripwire.in_any_order(): ...
Optional mock (may not fire).required(False)
Async function mockSame API; use async with tripwire: for sandbox
Environment variablesUse monkeypatch.setenv() (pytest built-in)

Domain Plugins

Use tripwire's domain-specific plugins when applicable instead of generic mocks. As of pytest-tripwire 0.21+ (formerly python-tripwire 0.20), plugin proxy names dropped the _mock suffix:

  • tripwire.http — HTTP requests (httpx, requests, urllib, aiohttp). Methods: mock_response(method, url, json=..., status=...), mock_error(...), assert_request(...).assert_response(...).
  • tripwire.subprocess — subprocess.run, shutil.which. Methods: mock_run(cmd, returncode=..., stdout=...), assert_run(cmd, ...).
  • tripwire.popen — subprocess.Popen.
  • tripwire.async_subprocess — asyncio.create_subprocess_*.
  • tripwire.db — sqlite3 / generic DB. State-machine plugin with step sentinels tripwire.db.connect, .execute, .commit, .rollback, .close, and matching assertion methods: tripwire.db.assert_connect(database=...), .assert_execute(sql=..., parameters=...), .assert_commit(), .assert_rollback(), .assert_close(). Transitions: disconnected → connected → in_transaction → connected → closed.
  • tripwire.socket — raw socket operations.
  • Other plugins: tripwire.smtp, tripwire.redis, tripwire.mongo, tripwire.boto3, tripwire.pika, tripwire.ssh, tripwire.log, tripwire.jwt, tripwire.crypto, tripwire.file_io, tripwire.dns, tripwire.memcache, tripwire.celery, tripwire.elasticsearch, tripwire.grpc, tripwire.mcp, tripwire.psycopg2, tripwire.asyncpg, tripwire.sync_websocket, tripwire.async_websocket, tripwire.native.

Do NOT write tripwire.database, tripwire.patch, tripwire.MagicMock, @tripwire.mock(...) (decorator form), or any pre-rebrand _mock-suffixed alias (tripwire.subprocess_mock, tripwire.db_mock, tripwire.log_mock, ...) — none of these exist.

Guard Mode

Guard mode is configured in pyproject.toml:

[tool.tripwire]
guard = "error"

[tool.tripwire.firewall]
allow = ["socket:*", "database:*", "db:*", "subprocess:*", "http:*", "dns:*"]

This catches any real I/O that escapes the sandbox during tests. Use @pytest.mark.allow("plugin") for tests that intentionally make real calls.

PR Review Bot

  • Automatic PR reviewer: gemini-code-assist (external GitHub App; reviews PRs automatically; no in-repo configuration).
  • On-demand reviewer: axiomantic-momus[bot] (momus). Trigger by commenting /ai-review on a PR, or manually via workflow_dispatch of the Momus workflow. Does not auto-review on PR open.

Module Cycle Prevention

Module import cycles are hard to diagnose and break. They typically surface as cryptic compile errors like "undeclared identifier" or "type mismatch" when one side of the cycle has been only partially processed. Add findings as you encounter them.

Symptom Patterns (from the 2026-08-08 mcf5307 Nim cycle)

  • imported and not used: 'X' warning + undeclared identifier: 'X' error in a file that DOES use X
  • Error: undeclared field: ''cint' for type system.string — typed literal parsed as field access on string (cascading parse error)
  • Error: type mismatch at the call site of a function defined in the other module of the cycle
  • Error: invalid indentation or attempting to call undeclared routine: '<Error>' after attempting to break the cycle with forward declarations

Standard Fixes (in order of preference)

  1. Third types module. If module A defines types that B needs, and B defines functions that A calls, put the shared types in a third module C. Both A and B import from C. No cycle.
  2. Function pointer. A declares a global proc var slot. B sets it at module load time. A calls through the pointer. No cycle.
  3. Forward declaration. In Nim: type Foo* = ref object declares the type without fields. Sufficient if B only passes the type around and doesn't access fields.
  4. Mid-file import. In Nim, putting import B after A's type definitions can work IF the cycle is truly resolvable. Often it isn't.
  5. Duplicated type declarations. Last resort. Define the same ref object in both files with identical fields. Works but duplicates maintenance burden.

Prevention

  • When designing a new module pair, draw the dependency graph BEFORE writing code. If two modules need each other's definitions, that's a smell.
  • If a cycle is unavoidable, design the shared surface FIRST. Put the types in a third module before writing any functions that cross the cycle.
  • In Nim, if you must have a mid-file import, verify the imported symbols are used by functions DEFINED AFTER the import. Imports don't reach backward through the function-definition order.

2026-08-08 mcf5307 (CPU-7+ WIP)

The decode.nim <-> move.nim cycle blocked the build. The WIP commit at 10f211e documents the issue. To unblock CPU-7/8/9/10: extract MCF5307Ctx, Decoded, Operation, EA from decode.nim into a new mcf5307/decode_types.nim (or rename decode.nim to split types from dispatch), then have both decode.nim and the group modules import from it. No cycle. Once done, the conformance runner's move/alu/logic/control tests will go green.

Pre-Commit Checklist

Before any commit, verify:

  1. uv run pytest tests/ passes (fast tests only, heavy markers auto-skipped)
  2. uv run install.py --dry-run succeeds
  3. uv run scripts/generate_docs.py if you touched skills/, commands/, agents/, or rules/ (the hook checks freshness; it does not regenerate)

<FINAL_EMPHASIS> Every library change ships to users across four platforms. Skipping tests or documentation means breaking real developer environments. Run the checklist. Every time. </FINAL_EMPHASIS>

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.