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
- Very specific: line 33
If a pre-release exists that is newer than the last actual release, ask: "There's a pre-release (vX.Y.Z) ready.
- Very specific: line 48
Only doctoc writes files. Every other hook reports and leaves the tree alone.
- Distinctive: line 171
Hook failure: Run the command the hook names, stage the result, retry commit.
- Very specific: line 181
Skills: Create skills/<name>/SKILL.md with name/description frontmatter.
- Very specific: line 183
Commands: Create commands/<name>.md with description frontmatter.
- Very specific: line 256
Skill invokes commands: Use /command-name syntax to delegate.
- Distinctive: line 300
When you add a new persistent key, decide up front:
- Very specific: line 304
If the distinction is unclear, pick config and document the choice.
- Distinctive: line 310
Every user-facing configuration option MUST follow the three-point rule:
- Very specific: line 312
Prompted on fresh install: If the user has never answered, offer a prompt.
- Very specific: line 354
Subprocesses: Use the @pytest.mark.allow("subprocess") marker on your test function.
- Very specific: line 376
Docker tests: only run in CI via integration-test.yml in a dedicated container.
- Very specific: line 444
If a value is genuinely incidental (wall-clock timestamp, tmp path, object identity), use the INSTANCE AnyThing() — never the bare class, …
- Very specific: line 449
When harvesting real values from tripwire's failure hints, READ them before pasting: hint reprs can contain os.environ, credentials, and …
- Very specific: line 474
Use tripwire's domain-specific plugins when applicable instead of generic mocks.
- Distinctive: line 518
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.
- Distinctive: line 526
When designing a new module pair, draw the dependency graph BEFORE writing code.
- Distinctive: line 527
If a cycle is unavoidable, design the shared surface FIRST.
- Distinctive: line 536
Before any commit, verify:
- Very specific: line 543
Every library change ships to users across four platforms.
≈ “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.
Files
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.pySession 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/spellbookIf 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/*.mdare 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/*.mdfile 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: runuv run install.pyto create its symlink (or regenerate the flat-platform concatenation) before the new module reaches any installed platform.MANIFEST.mddoes not trackrules/*.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.
- Editing an existing
- Skills go in
skills/<name>/SKILL.mdwith YAML frontmatter - Commands go in
commands/<name>.mdwith YAML frontmatter - Hooks go in
hooks/and must be registered ininstaller/components/hooks.py - MCP tools are defined in
spellbook/mcp/server.pyandspellbook/mcp/tools/modules
Pre-commit Hooks
Only doctoc writes files. Every other hook reports and leaves the tree alone. If a hook fails:
doctocfailures: Table of contents in markdown files needs regeneration. Usually fixes itself on re-commit.Check generated documentation is current: Thedocs/mirror is stale. The hook runsscripts/generate_docs.py --check, which names each stale page and writes nothing. Runuv run scripts/generate_docs.py, stage the regenerated files, and re-commit.Check documentation completeness: A skill/command has no generated doc page. Runuv run scripts/generate_docs.pyand 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.
| Variable | Effect |
SPELLBOOK_GATE_CMD | Shell 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_AUTONOMOUS | Operator 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_CAP | Harness 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.
| Platform | Support Level | GitHub | Config Location | MCP Transport |
| Claude Code | Primary, full | anthropics/claude-code | ~/.claude/ | HTTP daemon |
| Antigravity | Basic | google-deepmind/antigravity | ~/.gemini/antigravity | HTTP daemon |
| OpenCode | Basic | anomalyco/opencode | ~/.config/opencode/ | HTTP daemon |
| Codex | Basic | openai/codex | ~/.codex/ | HTTP daemon |
| Gemini CLI | Basic | google/gemini-cli | ~/.gemini/ | HTTP daemon |
Invariant Principles
- Library vs Repo distinction: Library items (
skills/,commands/) ship to users and require docs. Repo items (.claude/skills/) are internal only. - Documentation follows code: Library changes require CHANGELOG, README, and docs updates. The
check-docs-completenesspre-commit hook enforces the README anddocs/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. - Test before commit:
uv run pytest tests/+uv run install.py --dry-runbefore any commit.
Glossary
| Term | Location | Ships to Users | Docs Required |
| Library skill | skills/*/SKILL.md | Yes | CHANGELOG, README, docs |
| Library command | commands/*.md | Yes | CHANGELOG, README, docs |
| Repo skill | .claude/skills/*/SKILL.md | No | None |
| Rule module | rules/*.md | Yes | CHANGELOG, 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 manifestsKey Files
| File | Purpose |
rules/ | User-facing rule modules (installed per-module) |
extensions/gemini/ | Gemini extension (linked via gemini extensions link) |
install.py | Installer entry |
spellbook/mcp/server.py | MCP 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 installerPre-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
- NEW users are presented with the option during initial installation.
- 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.
- 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
| Where | What to add |
spellbook/core/config.py | CONFIG_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 wizard | Default 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 viapython3 install.py. - CLI wrapper
spellbook/cli/commands/install.py— invoked byspellbook installafter 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 maxDo NOT trim content to fit. Split instead.
</CRITICAL>
When a Skill Exceeds Limits
- Skill becomes orchestrator: SKILL.md is a thin wrapper defining workflow phases, delegating to commands.
- Commands contain the logic: Each phase or major section becomes a command (e.g.,
advanced-code-review-plan.md). - Skill invokes commands: Use
/command-namesyntax 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 logicPlatform Installers
| Platform | File | Output |
| Claude Code | installer/platforms/claude_code.py | CLAUDE.md + MCP + skills/commands |
| Gemini CLI | installer/platforms/gemini.py | Native extension via link |
| OpenCode | installer/platforms/opencode.py | AGENTS.md + MCP |
| Codex | installer/platforms/codex.py | AGENTS.md + MCP |
Config vs State
Spellbook stores persistent key-value data in two places. Pick the right one:
| File | Module | Purpose |
~/.config/spellbook/spellbook.json | spellbook.core.config | User intent (what the user configured) |
~/.local/spellbook/state.json | spellbook.core.state | Runtime 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:- 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. - 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. - 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(thespellbook installCLI command, including the--reconfigurebranch).
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.mockin any form —patch(),patch.object(),MagicMock,AsyncMock,Mock,mock_open,PropertyMock,create_autospec, etc.pytest-mock/mockerfixturemonkeypatch.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.fixturethat returns a fake replacement for a real dependency
Allowed uses of monkeypatch (pytest-builtin, not a mocking framework):
monkeypatch.setenv()/monkeypatch.delenv()for environment variablesmonkeypatch.chdir()for working directorymonkeypatch.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:
- Every call must be pre-authorized - unmocked calls raise
UnmockedInteractionError - Every interaction must be asserted - unasserted calls raise
UnassertedInteractionsErrorat teardown - 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
| Need | Pattern |
| Mock a module attribute | tripwire.mock("module.path:attribute") |
| Mock an object method | tripwire.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 asserts | with tripwire.in_any_order(): ... |
| Optional mock (may not fire) | .required(False) |
| Async function mock | Same API; use async with tripwire: for sandbox |
| Environment variables | Use 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 sentinelstripwire.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-reviewon a PR, or manually viaworkflow_dispatchof 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 XError: undeclared field: ''cint' for type system.string— typed literal parsed as field access on string (cascading parse error)Error: type mismatchat the call site of a function defined in the other module of the cycleError: invalid indentationorattempting to call undeclared routine: '<Error>'after attempting to break the cycle with forward declarations
Standard Fixes (in order of preference)
- 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.
- Function pointer. A declares a global
proc varslot. B sets it at module load time. A calls through the pointer. No cycle. - Forward declaration. In Nim:
type Foo* = ref objectdeclares the type without fields. Sufficient if B only passes the type around and doesn't access fields. - Mid-file import. In Nim, putting
import Bafter A's type definitions can work IF the cycle is truly resolvable. Often it isn't. - Duplicated type declarations. Last resort. Define the same
ref objectin 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:
uv run pytest tests/passes (fast tests only, heavy markers auto-skipped)uv run install.py --dry-runsucceedsuv run scripts/generate_docs.pyif you touchedskills/,commands/,agents/, orrules/(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>
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository65 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- documentation-updatesSkill
- spellbook-auditingSkill
- advanced-code-reviewSkill
- adversarial-reviewSkill
- agent2agentSkill
- analyzing-domainsSkill
- analyzing-skill-usageSkill
- assembling-contextSkill
- async-await-patternsSkill
- auditing-green-mirageSkill
- autonomous-modeSkill
- branch-contextSkill
- code-reviewSkill
- creating-issues-and-pull-requestsSkill
- debuggingSkill
- dedupeSkill
- deep-researchSkill
- dehallucinationSkill
- design-explorationSkill
- designing-workflowsSkill
- developSkill
- devils-advocateSkill
- dispatching-parallel-agentsSkill
- distilling-prsSkill
- documenting-projectsSkill
- documenting-toolsSkill
- emotional-stakesSkill
- enforcing-code-qualitySkill
- estimating-ticketsSkill
- executing-plansSkill
- fact-checkingSkill
- finding-dead-codeSkill
- finishing-a-development-branchSkill
- fixing-testsSkill
- fractal-thinkingSkill
- gathering-requirementsSkill
- generating-diagramsSkill
- instruction-engineeringSkill
- isolated-testingSkill
- managing-artifactsSkill
- merging-worktreesSkill
- opportunity-awarenessSkill
- optimizing-instructionsSkill
- polish-repoSkill
- reflexionSkill
- requesting-code-reviewSkill
- resolving-merge-conflictsSkill
- reviewing-design-docsSkill
- reviewing-impl-plansSkill
- reviewing-prsSkill
- rounding-up-worktree-sessionsSkill
- sharpening-promptsSkill
- smart-readingSkill
- test-driven-developmentSkill
- testing-strategySkill
- tooling-discoverySkill
- using-git-worktreesSkill
- using-lsp-toolsSkill
- using-skillsSkill
- The first 60 shown.
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.

