Read and follow AGENTS.md for all beads workflows, especially the "Landing the Plane" section.
Python CLI application built with Click.
This repo uses beads with:
sync-branch: beads-sync(daemon commits beads changes here)daemon.auto_commit: trueanddaemon.auto_push: true- Git hooks installed (
bd hooks install)
# 1. Find available work
bd ready --json
# 2. Claim with unique assignee (prevents race conditions)
ISSUE=<id>
bd update $ISSUE --status in_progress --assignee $(hostname)-$$
# 3. Verify you won the claim (wait for daemon sync)
sleep 2
bd show $ISSUE --json | jq -r '.assignee'
# If not your assignee, pick a different issue
# 4. Create worktree and enter it
bd worktree create .worktrees/$ISSUE
cd .worktrees/$ISSUE- Stay in the worktree (
pwdshould show.worktrees/<id>) - Commit frequently within the worktree
- New discoveries:
bd create "..." -t task --deps discovered-from:$ISSUE - Check other agents' progress:
bd list --status in_progress
# 1. Commit final changes
git add . && git commit -m "feat: description ($ISSUE)"
# 2. Return to main repo
cd ../..
# 3. Close the issue
bd close $ISSUE --reason "Completed: brief summary"
# 4. Remove worktree
bd worktree remove .worktrees/$ISSUE
# 5. Follow "Landing the Plane" in AGENTS.md- ❌ Working directly in main repo (always use worktree)
- ❌ Running
git checkoutto switch branches - ❌ Closing issues you didn't claim
- ❌ Modifying
.beads/files directly - ❌ Running
bd daemoncommands (daemon is managed globally) - ❌ Spawning subagents or background tasks for issue work
- ❌ Working on multiple issues in a single session
Parallelization happens via separate terminal sessions, NOT within a single Claude session.
FORBIDDEN:
- ❌ Spawning subagents or background tasks (
Task agents launched) - ❌ Running multiple issues concurrently in one session
- ❌ Using Claude's built-in parallel task feature for beads work
- ❌ Launching background work with
&or async patterns
CORRECT:
- ✅ Human opens Terminal 1 → Claude works on ONE issue
- ✅ Human opens Terminal 2 → Claude works on ONE different issue
- ✅ Each Claude session = one issue at a time, sequentially
If you see multiple ready issues, pick ONE and work it to completion. Do not attempt to work multiple issues simultaneously. The human controls parallelization by opening additional terminals.
Why: All terminals share one SQLite database (.beads/beads.db). Multiple concurrent writers from subagents cause race conditions, shell failures, and sync corruption.
src/
└── <package_name>/
├── __init__.py
├── cli.py # Main CLI entry point
├── commands/ # Command groups
│ ├── __init__.py
│ └── <feature>.py
└── core/ # Business logic (no Click dependencies)
├── __init__.py
└── <module>.py
# cli.py
import click
@click.group()
@click.version_option()
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
@click.pass_context
def cli(ctx: click.Context, verbose: bool) -> None:
"""Short description of your CLI tool."""
ctx.ensure_object(dict)
ctx.obj['verbose'] = verbose
# Import and register command groups
from .commands import feature
cli.add_command(feature.group)
if __name__ == '__main__':
cli()# commands/feature.py
import click
@click.group('feature')
def group() -> None:
"""Manage features."""
pass
@group.command('create')
@click.argument('name')
@click.option('--description', '-d', default='', help='Feature description')
@click.pass_context
def create(ctx: click.Context, name: str, description: str) -> None:
"""Create a new feature."""
verbose = ctx.obj.get('verbose', False)
# Call core logic, not business logic here
from ..core.features import create_feature
result = create_feature(name, description)
if verbose:
click.echo(f"Created feature: {result}")
else:
click.echo(result.id)Options and Arguments:
# Required argument
@click.argument('name')
# Optional with default
@click.option('--count', '-c', default=1, type=int, help='Number of items')
# Boolean flag
@click.option('--force', '-f', is_flag=True, help='Skip confirmation')
# Choice
@click.option('--format', type=click.Choice(['json', 'table', 'csv']), default='table')
# File handling
@click.option('--output', '-o', type=click.Path(), help='Output file path')
@click.option('--config', type=click.File('r'), help='Config file')Output:
# User-facing output
click.echo("Processing...")
# Errors (to stderr)
click.echo("Error: something failed", err=True)
# JSON output (for scripting)
import json
click.echo(json.dumps(data, indent=2))
# Styled output
click.secho("Success!", fg='green', bold=True)
click.secho("Warning!", fg='yellow')
click.secho("Error!", fg='red', err=True)Error Handling:
@group.command('delete')
@click.argument('id')
@click.option('--force', is_flag=True)
def delete(id: str, force: bool) -> None:
"""Delete an item."""
if not force:
click.confirm(f'Delete {id}?', abort=True)
try:
result = core.delete_item(id)
except ItemNotFoundError:
raise click.ClickException(f"Item '{id}' not found")
except PermissionError:
raise click.ClickException("Permission denied")
click.echo(f"Deleted {id}")Exit Codes:
import sys
# Success (implicit)
click.echo("Done")
# Explicit failure
raise click.ClickException("Something went wrong") # exit code 1
# Custom exit code
ctx.exit(2)# tests/test_cli.py
from click.testing import CliRunner
from mypackage.cli import cli
def test_version():
runner = CliRunner()
result = runner.invoke(cli, ['--version'])
assert result.exit_code == 0
assert '0.1.0' in result.output
def test_create_feature():
runner = CliRunner()
result = runner.invoke(cli, ['feature', 'create', 'my-feature', '-d', 'A description'])
assert result.exit_code == 0
assert 'my-feature' in result.output
def test_create_feature_missing_arg():
runner = CliRunner()
result = runner.invoke(cli, ['feature', 'create'])
assert result.exit_code != 0
assert 'Missing argument' in result.output[project.scripts]
mycli = "mypackage.cli:cli"- Python 3.11+
- Type hints on all functions
- Docstrings on public functions (Google style)
- Format with
ruff format - Lint with
ruff check - Test with
pytest
ruff format .
ruff check --fix .
pytest -v- Commands:
src/<package>/commands/<name>.py - Core logic:
src/<package>/core/<name>.py - Tests mirror source:
tests/test_<name>.py - Keep Click decorators thin — business logic goes in
core/