Contributing¶
Prerequisites¶
- Python 3.12+ — required; summon-claude uses modern Python features
- uv — dependency management and toolchain
- git — standard version control
- git-branchless (optional) — commit history cleanup tools (
git sl,git reword,git branchless move)
Install git-branchless:
brew install git-branchless && git branchless init
Getting Started¶
# Clone and install
git clone git@github.com:summon-claude/summon-claude.git
cd summon-claude
uv sync
# Verify setup
make all # install → lint → test
Common Commands¶
make install # Install all dependencies (uv sync + git hooks)
make lint # Run ruff check + format (auto-fix, fails if files changed)
make test # Run full pytest suite
make all # Complete workflow: install → lint → test
make build # Build sdist and wheel
make py-lint # Python lint only
make py-typecheck # Run pyright type checking
make py-test # Run full Python test suite
make py-test-quick # Quick tests only (skips Slack + LLM integration, fail-fast)
make py-test-slack # Slack integration tests (requires real credentials)
make docs-serve # Serve docs locally with live reload
make docs-build # Build docs in strict mode
make docs-screenshots # Regenerate all documentation screenshots
Full reference:
make help # Lists all targets with descriptions
Commit Message Guidelines¶
summon-claude follows Conventional Commits.
Format¶
<type>(<scope>): <description>
[optional body]
[optional footer]
Types¶
| Type | Purpose |
|---|---|
feat |
Adds new feature |
fix |
Fixes a bug |
docs |
Updates documentation |
chore |
Maintenance (dependencies, configs) |
refactor |
Refactors code (no behavior change) |
test |
Adds or modifies tests |
perf |
Improves performance |
style |
Formatting, whitespace (no code change) |
Scopes¶
Scope rules:
- One scope per commit — no commas (e.g., fix(auth): not fix(auth,session):)
- Lowercase letters, numbers, hyphens, underscores only
- If a change spans multiple components, use the primary one or omit scope
Component scopes:
| Scope | Area |
|---|---|
sessions |
Session lifecycle and management (sessions/ package) |
slack |
Slack integration (slack/ package) |
daemon |
Daemon process and IPC (daemon.py, event_dispatcher.py) |
cli |
CLI entry point and subcommands (cli/ package) |
config |
Configuration and settings (config.py) |
auth |
Authentication tokens and short codes (sessions/auth.py) |
permissions |
Permission handling and approval flow (sessions/permissions.py) |
mcp |
MCP server and tools (slack/mcp.py) |
registry |
Session storage and SQLite operations (sessions/registry.py) |
db |
Database maintenance CLI (summon db group: status, vacuum, purge) |
reset |
Reset commands for data and config clearing (cli/reset.py) |
hooks |
Lifecycle hooks and Claude Code hook bridge (sessions/hooks.py, cli/hooks.py) |
project |
Project lifecycle and management (cli/project.py) |
scribe |
Scribe monitoring agent |
canvas |
Canvas storage and MCP tools (canvas_mcp.py, slack/canvas_store.py) |
diagnostics |
Diagnostic checks and doctor command (diagnostics.py, cli/doctor.py) |
plugin |
Claude Code plugin skill and manifest (.claude-plugin/) |
Infrastructure scopes:
| Scope | Area |
|---|---|
ci |
CI/CD pipelines and GitHub Actions |
deps |
Dependency updates |
build |
Build system, pyproject.toml, Makefile |
repo |
Repository structure, gitignore, misc files |
Examples¶
Good:
feat(mcp): adds slack_create_thread tool
Enables Claude to start new threads for organizing long conversations.
fix(permissions): respects settings.json auto-approve rules
Permission handler was ignoring ToolPermissionContext.suggestions
from the Claude SDK, falling back to hardcoded allowlist only.
chore(deps): updates slack-bolt to v2.0
Bad:
updated stuff
fix(permissions,session): updates handlers and tokens
feat(cli): adds new commands
Added three new commands: init, build, and deploy. Each command
has its own handler and validation logic. Updated help text to
include all new commands and their options.
Writing commit messages¶
DO:
- Use present indicative tense ("adds" not "add" or "added")
- Keep subject line ≤50 characters
- Explain WHY in body, not WHAT (the diff shows what)
- Keep body to 2-3 lines maximum, each ≤72 characters
- Reference issues: Closes #123
Body structure: 1. First line: Why this change was needed (problem/motivation) 2. Second line (optional): Essential technical context if non-obvious 3. That's it.
No body needed if the change is obvious from the subject line.
DON'T: - Use emojis or ALL CAPS - List changed files (git shows this) - Include statistics (lines changed) - Add meta-commentary ("Generated with...", "Co-Authored-By...") - Write verbose explanations or "benefits" - Describe what changed (the diff shows that)
Breaking Changes¶
Add ! after type/scope:
feat(sessions)!: changes SessionManager API
BREAKING CHANGE: create_session now requires EventDispatcher reference.
Git Workflow¶
Use feature branches for all work. Don't commit directly to main.
One-time setup:
git config --global push.autoSetupRemote true
Feature workflow:
# Create feature branch from latest upstream
git fetch upstream
git switch -c feature/your-feature upstream/main
# Work and commit
git commit -am "feat(scope): description"
git push # Auto-sets up tracking
# Rebase before PR
git fetch upstream
git rebase upstream/main
# Create PR
gh pr create
After PR merges, delete the branch and sync main.
Commit Workflow¶
During development: - Commit freely and often — don't worry about perfection - WIP commits, debugging attempts, and iterations are all fine - Focus on making progress, not perfect history
Before PR/merge: - Review your commits — look at the full diff and commit history - Group related changes — combine commits that belong together logically - Use interactive rebase to reorganize, squash, and reword
Decision criteria for squashing: - Do these commits represent one logical change? - Would a reviewer want to see these as separate steps? - Does each commit add value independently?
If commits are just iterations toward a solution, squash them. If commits represent distinct logical changes, keep them separate.
Commit cleanup with git-branchless¶
# Review your commits
git log --oneline -10
git sl # Visual commit graph
# Reword a commit message
git reword -m "fix(ci): configures node 20 and disables cache" jkl3456
# Squash commits together
git branchless move --fixup -x def5678 -d abc1234
git branchless move --fixup -x ghi9012 -d abc1234
# View the result
git sl
You decide what makes sense for your change — there's no formula.
Pull Request Guidelines¶
Keep PR descriptions brief: - State what changed and why - Use bullet points for multiple changes - Reference related issues/PRs if applicable - No verification sections, file lists, or test result summaries
The diff shows what changed. CI shows test results. Don't repeat information that's already visible.
Good example:
## Summary
- Migrates CLI from argparse to click for proper output handling and shell completion
- Resolves print() usage by replacing with click.echo()
Bad example:
## Summary
Migrated CLI framework
## Changes Made
- Updated cli.py to use click decorators
- Modified cli_config.py for click groups
- Added click to dependencies
## Test Results
✅ 332 tests passing
## Files Changed
- src/summon_claude/cli.py
- src/summon_claude/cli_config.py
- pyproject.toml
Code Style¶
Linting and formatting¶
uv run ruff check . --fix # Auto-fix lint issues
uv run ruff format . # Auto-format
uv run pyright # Type checking
Or via make:
make lint # ruff check + format
make py-typecheck # pyright only
Ruff configuration (from pyproject.toml):
- Line length: 100 characters
- Target: Python 3.12
- Enabled rule sets: pycodestyle, pyflakes, isort, pep8-naming, flake8-bugbear, comprehensions, pyupgrade, bandit, datetimez, pathlib, pylint, and more
- All relative imports are banned — use absolute imports (from summon_claude.sessions.auth import ...)
Pyright configuration:
- Mode: standard
- Checks src/ only; excludes tests/
- reportMissingImports = false (claude-agent-sdk lacks py.typed marker)
- reportMissingTypeStubs = false (slack-sdk, slack-bolt don't ship stubs)
Import organization¶
isort is configured via ruff with known-first-party = ["summon_claude", "helpers"]. Imports are grouped: stdlib → third-party → first-party. All imports must be absolute.
Database Migrations¶
Schema migrations run automatically when SessionRegistry connects — users never run a manual step. The migration system lives in sessions/migrations.py.
Adding a migration¶
- Bump
CURRENT_SCHEMA_VERSIONinsessions/migrations.py - Write an async migration function that takes
db: aiosqlite.Connection - Add it to
_MIGRATIONSkeyed by the version it migrates from
CURRENT_SCHEMA_VERSION = 15 # was 14
async def _migrate_14_to_15(db: aiosqlite.Connection) -> None:
await db.execute("ALTER TABLE sessions ADD COLUMN tags TEXT")
_MIGRATIONS: dict[int, Any] = {
...
14: _migrate_14_to_15,
}
Migration rules¶
- Idempotent — if the process crashes mid-migration, the same migration reruns on next connect; it must not fail or corrupt data
- All pending migrations run within a single
BEGIN IMMEDIATE/COMMITtransaction, withROLLBACKon any error Nonemeans no-op (used for the 0→1 baseline where DDL already matches)- Migrations are the single source of truth — fresh DBs create v1 baseline tables and run all migrations from v1 to current; do not add schema changes to the DDL constants in
registry.py - SQLite lacks
IF NOT EXISTSforALTER TABLE— wrapALTER TABLEstatements intry/exceptto handle the "duplicate column name" error on re-run PRAGMA foreign_keys = ONis set in_connect()but cannot be changed inside a transaction — set it before anyBEGIN; future migrations needing temporary FK violations must use a separate connection
Checking schema version¶
summon db status # Shows current schema version and migration state
Diagnostic Checks¶
The summon doctor command uses a registry pattern in diagnostics.py. Each check is a class that implements the DiagnosticCheck protocol and is registered in DIAGNOSTIC_REGISTRY.
Adding a check¶
-
Create a class implementing
DiagnosticCheckindiagnostics.py:class MySubsystemCheck: name = "my_subsystem" description = "Checks something important" async def run(self, config: SummonConfig | None) -> CheckResult: # Return early with "skip" if prerequisites are missing if config is None: return CheckResult( status="skip", subsystem="my_subsystem", message="Config not available", ) details: list[str] = [] # ... perform checks, append to details ... return CheckResult( status="pass", subsystem="my_subsystem", message="Everything looks good", details=details, ) -
Register it and add the subsystem name:
DIAGNOSTIC_REGISTRY["my_subsystem"] = MySubsystemCheck()Add
"my_subsystem"to theKNOWN_SUBSYSTEMSfrozenset at the top of the file. -
Add guard test mappings in
tests/test_diagnostics_guard.py:uv run pytest tests/test_diagnostics_guard.py -vThe guard tests verify that every registered check is in
KNOWN_SUBSYSTEMSand vice versa. Add MCP server names, binary paths, or credential references to the appropriate mapping dicts if your check validates external integrations.
CheckResult fields¶
| Field | Type | Purpose |
|---|---|---|
status |
"pass" / "fail" / "warn" / "info" / "skip" |
Overall result |
subsystem |
str |
Identifier (must match KNOWN_SUBSYSTEMS entry) |
message |
str |
One-line summary shown in default output |
details |
list[str] |
Itemized findings (shown with -v) |
suggestion |
str \| None |
Actionable next step (shown with -v) |
collected_logs |
dict[str, list[str]] |
Log tails keyed by filename (shown with -v, included in exports) |
Guidelines¶
- All checks run in parallel — do not depend on results from other checks
- Use
config: SummonConfig | None— config may beNoneif it failed to load; returnskipin that case - Return
skipwhen prerequisites are missing — notfail(e.g., scribe not enabled, no GitHub token) - Keep checks fast — use timeouts for network calls (10s for API calls, 5s for CLI version checks)
- Never log secrets — use the
redactorsingleton to sanitize any user-specific data in details or messages - Match SEC-003 — don't include workspace names, usernames, or Slack team names in results
Documentation Screenshots¶
Screenshots in docs/assets/screenshots/ are generated automatically — never edit them manually. When you change UI-facing behavior (permission buttons, channel messages, canvas layout), regenerate them:
make docs-screenshots
Prerequisites¶
The screenshot script starts a real summon session, authenticates via Slack, and captures Playwright screenshots. It requires:
- summon configured —
summon initcompleted with valid Slack credentials - Slack browser auth — run
summon auth slack login <workspace-url>once to save Playwright browser state - Playwright installed —
uv sync --extra slack-browser .envfile — the script loadsSUMMON_TEST_SLACK_BOT_TOKENfrom.envviapython-dotenv
Worktree considerations¶
In a worktree (.claude/worktrees/*/), the local-install detection may resolve config paths to the worktree's .summon/ directory instead of the global config. Two options:
- Copy config:
cp ~/.config/summon/config.env .summon/config.envandcp ~/.local/share/summon/slack_workspace.json .summon/ - Force global:
SUMMON_LOCAL=0 make docs-screenshotsbypasses local-install detection
If the worktree path is too long for Unix sockets (>104 chars), use SUMMON_LOCAL=0.
Sections¶
# All sections (recommended)
make docs-screenshots
# Individual sections
uv run python scripts/docs-screenshots.py --section session-ux # Real session screenshots
uv run python scripts/docs-screenshots.py --section terminal # CLI terminal captures
uv run python scripts/docs-screenshots.py --section slack-setup # Validates setup screenshots
uv run python scripts/docs-screenshots.py --dry-run # List planned screenshots
Project Architecture¶
src/summon_claude/
├── canvas_mcp.py # Canvas MCP server (read, write, update_section tools)
├── config.py # SummonConfig (pydantic-settings) with validation
├── daemon.py # Unix daemon with PID/lock, IPC framing
├── diagnostics.py # DiagnosticCheck protocol, CheckResult, Redactor, all check implementations
├── event_dispatcher.py # Routes Slack events to sessions by channel
├── github_auth.py # GitHub OAuth App device flow authentication
├── mcp_untrusted_proxy.py # MCP stdio proxy that marks tool results as untrusted
├── security.py # Prompt injection defense utilities
├── slack_browser.py # Playwright-based Slack WebSocket monitor for external workspaces
├── summon_cli_mcp.py # MCP tools for session lifecycle (session_list, _info, _start, _stop)
├── cli/
│ ├── __init__.py # Click wiring, root group, setup_logging
│ ├── auth.py # Auth group: unified auth commands for GitHub, Google, Slack
│ ├── config.py # Config subcommands (show, set, path, edit, check)
│ ├── daemon_client.py # Typed async client for daemon Unix socket API
│ ├── db.py # DB subcommand implementations (status, vacuum, purge)
│ ├── doctor.py # Doctor command logic (check runner, output formatting, export, submit)
│ ├── formatting.py # Output formatting (echo, format_json, print_session_table)
│ ├── helpers.py # Session resolution (resolve_session, pick_session)
│ ├── hooks.py # Lifecycle hooks CLI (install/uninstall bridge, show/set/clear)
│ ├── interactive.py # Interactive terminal selection with TTY-aware fallback
│ ├── preflight.py # Claude CLI preflight check (version, path)
│ ├── project.py # Project lifecycle (add, remove, up, down, workflow)
│ ├── reset.py # Reset commands (data, config)
│ ├── session.py # Session subcommand implementations (list, info, logs, cleanup)
│ ├── slack_auth.py # Slack browser auth CLI helpers
│ ├── start.py # async_start() implementation
│ ├── stop.py # async_stop() implementation
│ └── update_check.py # PyPI update checker with 24h cache
├── sessions/
│ ├── auth.py # Session auth tokens and short codes
│ ├── commands.py # !-prefixed command dispatch, aliasing, plugin skill registration
│ ├── context.py # Context window usage tracking via JSONL transcript parsing
│ ├── hook_types.py # Hook constants (VALID_HOOK_TYPES, INCLUDE_GLOBAL_TOKEN)
│ ├── hooks.py # Lifecycle hooks runner (worktree_create, project_up, project_down)
│ ├── manager.py # Session lifecycle, IPC control plane
│ ├── migrations.py # Schema versioning and migration functions
│ ├── permissions.py # Tool permission handling + Slack buttons
│ ├── registry.py # SQLite session storage (WAL mode)
│ ├── response.py # Response streaming, turn threads, emoji lifecycle, turn summaries
│ ├── scheduler.py # Session scheduling (cron tasks, timer injection)
│ ├── session.py # Session orchestrator (Claude SDK + Slack + pre-send architecture)
│ └── types.py # Shared session types and dataclasses
└── slack/
├── bolt.py # Bolt app, rate limiter, health monitor
├── canvas_store.py # SQLite-backed canvas markdown state with background Slack sync
├── canvas_templates.py # Canvas markdown templates for different agent profiles
├── client.py # Channel-bound Slack output client (post, update, react, canvas)
├── formatting.py # Markdown-to-Slack-mrkdwn conversion
├── markdown_split.py # Markdown-aware message splitting for Slack's length limits
├── mcp.py # MCP tools for Claude to read and interact with Slack
└── router.py # Thread-aware message routing (main, turn threads, subagent threads)