Skip to content

Instantly share code, notes, and snippets.

@sungchun12
Created January 9, 2026 21:22
Show Gist options
  • Select an option

  • Save sungchun12/e251dd943847da9220a0416dcbf96101 to your computer and use it in GitHub Desktop.

Select an option

Save sungchun12/e251dd943847da9220a0416dcbf96101 to your computer and use it in GitHub Desktop.

CLAUDE.md

Read and follow AGENTS.md for all beads workflows, especially the "Landing the Plane" section.

Project Overview

Python CLI application built with Click.

Beads Workflow (Parallel Terminals with Worktrees)

This repo uses beads with:

  • sync-branch: beads-sync (daemon commits beads changes here)
  • daemon.auto_commit: true and daemon.auto_push: true
  • Git hooks installed (bd hooks install)

Starting Work

# 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

During Work

  • Stay in the worktree (pwd should 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

Completing Work

# 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

Forbidden Actions

  • ❌ Working directly in main repo (always use worktree)
  • ❌ Running git checkout to switch branches
  • ❌ Closing issues you didn't claim
  • ❌ Modifying .beads/ files directly
  • ❌ Running bd daemon commands (daemon is managed globally)
  • ❌ Spawning subagents or background tasks for issue work
  • ❌ Working on multiple issues in a single session

Parallelization Model

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.


Click CLI Standards

Project Structure

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 Entry Point Pattern

# 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()

Command Pattern

# 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)

Click Conventions

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)

Testing Commands

# 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

pyproject.toml Entry Point

[project.scripts]
mycli = "mypackage.cli:cli"

Code Standards

  • 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

Running Quality Gates

ruff format .
ruff check --fix .
pytest -v

File Conventions

  • 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/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment