AI coding assistants are powerful. But running multiple agents across different machines? That’s where things get interesting. And chaotic.

I’ve built a workflow that lets multiple AI agents work on the same codebase without stepping on each other’s toes. This post breaks down the complete system using open-source tools.

The problem

Imagine this: You have Claude running in your IDE, another instance in your terminal, and maybe a third on your laptop. All working on the same repo. What could go wrong?

  • Race conditions: Two agents pick up the same task
  • Conflicting changes: Agent A refactors a function while Agent B adds to it
  • Build artifact conflicts: Agent A’s .venv gets corrupted by Agent B’s uv sync
  • Lost context: Agent crashes mid-task, work is abandoned
  • Configuration drift: Each agent follows different rules
  • No visibility: You have no idea what each agent is doing

The solution isn’t to use fewer agents; it’s to build coordination infrastructure.

Architecture overview

┌──────────────────────────────────────────────────────────────┐
│                   Agent Configuration Layer                  │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────────────┐  │
│  │ Ruler        │  │ Skillz       │  │ MCP Servers        │  │
│  │ (Rules)      │  │ (Validation) │  │ (Tools)            │  │
│  └──────────────┘  └──────────────┘  └────────────────────┘  │
├──────────────────────────────────────────────────────────────┤
│                    Issue Tracking Layer                      │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Beads - Git-native issue tracking                      │  │
│  │ - JSONL format, one issue per line                     │  │
│  │ - Sync via git pull/push                               │  │
│  │ - CLI for agents, not web UI                           │  │
│  └────────────────────────────────────────────────────────┘  │
├──────────────────────────────────────────────────────────────┤
│                    Coordination Layer                        │
│  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐  │
│  │ Advisory Locks │  │ Git Worktrees  │  │ Claim Protocol │  │
│  │ (Who works)    │  │ (Isolation)    │  │ (Atomic ops)   │  │
│  └────────────────┘  └────────────────┘  └────────────────┘  │
├──────────────────────────────────────────────────────────────┤
│                    Context & Learning Layer                  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Agent Learning - Institutional knowledge               │  │
│  │ - PostgreSQL (project_dev) + Git JSONL sync            │  │
│  │ - Automatic extraction from commits                    │  │
│  │ - Context-aware retrieval with token budgets           │  │
│  │ - Staleness detection & confidence decay               │  │
│  └────────────────────────────────────────────────────────┘  │
├──────────────────────────────────────────────────────────────┤
│                    Git Integration Layer                     │
│  ┌────────────┐  ┌─────────────┐  ┌──────────────────────┐   │
│  │ pre-commit │  │ pre-push    │  │ post-merge           │   │
│  │ (lint)     │  │ (validate)  │  │ (auto-sync)          │   │
│  └────────────┘  └─────────────┘  └──────────────────────┘   │
└──────────────────────────────────────────────────────────────┘

Let’s build each layer.

Layer 1: Ruler, unified agent configuration

Every agent needs the same rules. Without consistency, you get chaos. Ruler solves this.

The problem with multiple agents

Different AI tools expect configuration in different places:

Tool Config Location
Claude Code CLAUDE.md
Cursor .cursor/rules/
GitHub Copilot .github/copilot-instructions.md
Windsurf .windsurfrules

Maintaining 5 copies of the same rules? Nightmare.

Ruler: single source of truth

Ruler generates agent-specific configs from a single source:

.ruler/
├── ruler.toml          # Which agents to generate for
├── AGENTS.md           # Main rules (the source of truth)
├── python-patterns.md  # Python-specific rules
├── testing-rules.md    # Testing requirements
└── security-rules.md   # Security constraints

ruler.toml:

[agents.claude]
output = "CLAUDE.md"
mcp_output = ".mcp.json"

[agents.cursor]
output = ".cursor/rules/AGENTS.md"
mcp_output = ".cursor/mcp.json"

[agents.copilot]
output = ".github/copilot-instructions.md"

AGENTS.md (your rules):

# Project Rules

## Critical Constraints

1. **Never decrease test coverage** - 90%+ required
2. **TODOs must reference issues** - Format: `# TODO(#123): description`
3. **No secrets in code** - Use environment variables

## Before Every Commit

pytest && pylint --recursive=y .

## Code Style

- Type hints on ALL functions
- Async/await everywhere
- 80 char lines

Generate all configs:

npx @intellectronica/ruler apply

Now Claude, Cursor, and Copilot all follow the same rules.

Modular rules

Split rules into focused files:

<!-- .ruler/AGENTS.md -->
# Project Rules

{{include:python-patterns.md}}
{{include:testing-rules.md}}
{{include:security-rules.md}}

Each file handles one concern. Ruler concatenates them.

Layer 2: Beads, Git-native issue tracking

GitHub Issues are great for humans. Terrible for agents. Why?

  • API rate limits
  • Network latency
  • No offline access
  • Can’t atomically claim work

Beads solves this with git-native issue tracking.

Installation

# Go (the bd CLI is written in Go)
go install github.com/steveyegge/beads/cmd/bd@latest

Initialize in your repo

cd your-project
bd init

This creates:

.beads/
├── config.yaml      # Configuration
├── issues.jsonl     # Issues (git-tracked)
└── metadata.json    # Local state

Basic commands

# Create issues
bd create --title="Add user authentication" --type=feature --priority=2

# List open issues
bd list --status=open

# Show ready work (no blockers)
bd ready

# View issue details
bd show proj-abc1

# Claim work
bd update proj-abc1 --status=in_progress

# Complete work
bd close proj-abc1 --reason="Implemented in commit abc123"

# Sync with remote
bd sync

Why JSONL?

Issues are stored as JSON Lines; one issue per line:

{"id":"proj-001","title":"Add auth","status":"open","priority":2,"created_at":"2026-01-08T10:00:00Z"}
{"id":"proj-002","title":"Fix login","status":"in_progress","priority":1,"created_at":"2026-01-08T11:00:00Z"}
{"id":"proj-003","title":"Update docs","status":"closed","priority":3,"created_at":"2026-01-08T12:00:00Z"}

Benefits:

  • Git-native: Changes tracked like code
  • Merge-friendly: Conflicts are per-line, easy to resolve
  • Offline-capable: No network needed
  • Fast: Local SQLite for queries, JSONL for sync

Dependencies

Beads tracks issue dependencies:

# Create related issues
bd create --title="Design auth API" --type=task
# Created: proj-xyz1

bd create --title="Implement auth API" --type=task
# Created: proj-xyz2

# Add dependency (implement depends on design)
bd dep add proj-xyz2 proj-xyz1

# See what's blocked
bd blocked

# proj-xyz2 won't show in `bd ready` until proj-xyz1 is closed

Conflict resolution

When two agents edit issues.jsonl simultaneously:

<<<<<<< HEAD
{"id":"proj-001","status":"in_progress","assignee":"agent-a"}
=======
{"id":"proj-001","status":"in_progress","assignee":"agent-b"}
>>>>>>> origin/main

Resolution rules (built into bd sync):

  • closed beats in_progress beats open
  • Later timestamp wins for same status
  • Both versions kept for different issues

After resolving: bd sync rebuilds the local database.

Layer 3: PostgreSQL advisory locks, atomic claims

The beads claim protocol has a race window:

# Agent A                           # Agent B
bd sync                             bd sync
# Both see issue proj-abc1 as open
bd update proj-abc1 --status=in_progress
                                    bd update proj-abc1 --status=in_progress
# Both agents now working on same issue!

The solution: PostgreSQL advisory locks.

Why advisory locks?

Feature Benefit
Session-level Auto-release on agent crash (no orphaned locks)
Non-blocking Immediate failure if already claimed (no waiting)
Fast Pure in-memory, no table writes (~1ms overhead)
Cluster-aware Works across all PostgreSQL connections
Built-in No external dependencies (Redis, ZooKeeper, etc.)

Implementation

Create a simple lock module:

# db/advisory_locks.py
import hashlib
from contextlib import asynccontextmanager
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

def _issue_id_to_lock_key(issue_id: str) -> int:
    """Convert issue ID to PostgreSQL advisory lock key (int32)."""
    hash_digest = hashlib.md5(issue_id.encode()).hexdigest()
    return int(hash_digest[:8], 16) % (2**31 - 1)

async def acquire_issue_lock(session: AsyncSession, issue_id: str) -> bool:
    """Try to acquire advisory lock. Returns True if acquired."""
    lock_key = _issue_id_to_lock_key(issue_id)
    result = await session.execute(
        text("SELECT pg_try_advisory_lock(:key)"),
        {"key": lock_key}
    )
    return result.scalar()

async def release_issue_lock(session: AsyncSession, issue_id: str) -> bool:
    """Release advisory lock. Returns True if released."""
    lock_key = _issue_id_to_lock_key(issue_id)
    result = await session.execute(
        text("SELECT pg_advisory_unlock(:key)"),
        {"key": lock_key}
    )
    return result.scalar()

@asynccontextmanager
async def issue_lock(session: AsyncSession, issue_id: str):
    """Context manager for issue locks with auto-cleanup."""
    acquired = await acquire_issue_lock(session, issue_id)
    try:
        yield acquired
    finally:
        if acquired:
            await release_issue_lock(session, issue_id)

The claim script

Create a script that atomically claims issues:

# scripts/bd_claim.py
import asyncio
import subprocess
import sys
from db.advisory_locks import acquire_issue_lock
from db.client import get_session

async def claim_issue(issue_id: str) -> bool:
    """Atomically claim an issue via advisory lock + beads update."""
    async with get_session() as session:
        # Step 1: Try to acquire lock
        if not await acquire_issue_lock(session, issue_id):
            print(f"Issue {issue_id} already claimed by another agent")
            return False

        # Step 2: Update beads status (lock held, safe from races)
        result = subprocess.run(
            ["bd", "update", issue_id, "--status=in_progress"],
            capture_output=True, text=True
        )
        if result.returncode != 0:
            print(f"Failed to update beads: {result.stderr}")
            return False

        print(f"Successfully claimed {issue_id}")
        return True

if __name__ == "__main__":
    issue_id = sys.argv[1]
    success = asyncio.run(claim_issue(issue_id))
    sys.exit(0 if success else 1)

Shell wrapper for convenience:

#!/bin/bash
# scripts/bd_claim.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
exec uv run python scripts/bd_claim.py "$@"

Updated claim protocol

# Agent A                           # Agent B
bd_claim.sh proj-abc1               bd_claim.sh proj-abc1
# ✓ Lock acquired                   # ✗ Already claimed
# ✓ Issue claimed                   # Agent B picks different task

No more race conditions.

Layer 4: Git worktrees, filesystem isolation

Advisory locks solve coordination (who works on what). But agents still share the same working directory. This causes:

  • Build artifact conflicts: Agent A’s pytest corrupts Agent B’s __pycache__
  • Virtual environment races: Simultaneous uv sync commands
  • Uncommitted change conflicts: Agent A’s WIP blocks Agent B’s tests

The solution: git worktrees.

What are worktrees?

Git worktrees let you check out multiple branches simultaneously in different directories:

# Main repo
/project/              # main branch

# Worktrees (separate directories, shared .git)
/project-worktrees/
├── proj-abc1/         # work/proj-abc1 branch
├── proj-xyz2/         # work/proj-xyz2 branch
└── proj-def3/         # work/proj-def3 branch

Key insight: All worktrees share the same .git directory, saving disk space while providing complete filesystem isolation.

Worktree management script

Create a script that combines claiming with worktree creation:

#!/bin/bash
# scripts/agent_worktree.sh
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKTREE_BASE="${WORKTREE_BASE:-$REPO_ROOT/../project-worktrees}"

create_worktree() {
    local issue_id="$1"
    local worktree_path="$WORKTREE_BASE/$issue_id"
    local branch_name="work/$issue_id"

    # Step 1: Claim issue via advisory lock (atomic)
    echo "Claiming issue $issue_id..."
    if ! "$SCRIPT_DIR/bd_claim.sh" "$issue_id"; then
        echo "Failed to claim (already taken?)"
        exit 1
    fi

    # Step 2: Create worktree directory
    mkdir -p "$WORKTREE_BASE"

    # Step 3: Create worktree on new branch
    echo "Creating worktree at $worktree_path..."
    cd "$REPO_ROOT"
    git worktree add "$worktree_path" -b "$branch_name"

    # Step 4: Copy environment files (not in git)
    if [-f "$REPO_ROOT/.env"](/-f "$REPO_ROOT/.env"/); then
        cp "$REPO_ROOT/.env" "$worktree_path/.env"
    fi

    # Step 5: Install dependencies
    echo "Installing dependencies..."
    cd "$worktree_path"
    uv sync --quiet

    echo "Worktree ready: $worktree_path"
    echo "Branch: $branch_name"
}

remove_worktree() {
    local issue_id="$1"
    local worktree_path="$WORKTREE_BASE/$issue_id"
    local branch_name="work/$issue_id"

    echo "Removing worktree..."
    cd "$REPO_ROOT"
    git worktree remove "$worktree_path" --force 2>/dev/null || rm -rf "$worktree_path"
    git worktree prune

    # Delete branch if merged
    git branch -d "$branch_name" 2>/dev/null || true

    echo "Cleanup complete"
}

list_worktrees() {
    echo "Active worktrees:"
    git worktree list

    if [-d "$WORKTREE_BASE"](/-d "$WORKTREE_BASE"/); then
        echo ""
        echo "Worktree status:"
        for dir in "$WORKTREE_BASE"/*/; do
            if [-d "$dir"](/-d "$dir"/); then
                local id=$(basename "$dir")
                local branch=$(cd "$dir" && git branch --show-current)
                local uncommitted=$(cd "$dir" && git status --porcelain | wc -l)
                echo "  $id (branch: $branch, uncommitted: $uncommitted files)"
            fi
        done
    fi
}

case "${1:-}" in
    create) create_worktree "$2" ;;
    remove) remove_worktree "$2" ;;
    list)   list_worktrees ;;
    *)      echo "Usage: $0 {create|remove|list} [issue-id]" ;;
esac

Worktree workflow

# 1. Create isolated worktree (claims issue automatically)
scripts/agent_worktree.sh create proj-abc1

# 2. Work in isolated directory
cd ../project-worktrees/proj-abc1

# Make changes with complete isolation
uv run pytest          # Won't affect other agents
uv sync               # Safe, isolated .venv
git add . && git commit -m "Implement feature"
git push -u origin work/proj-abc1

# 3. Create PR, get review, merge to main

# 4. Cleanup
scripts/agent_worktree.sh remove proj-abc1
bd close proj-abc1 && bd sync

Worktree vs advisory locks

Both mechanisms solve different problems:

Mechanism Purpose Scope
Advisory Locks WHO works on WHAT Coordination
Worktrees HOW they work Isolation

They work together:

  1. Lock prevents duplicate claims (coordination)
  2. Worktree prevents file conflicts (isolation)

Disk space considerations

Each worktree duplicates working files but shares .git:

Component Main Repo Per Worktree
.git/ ~200MB 0 (shared)
Source code ~50MB ~50MB
.venv/ ~300MB ~300MB
node_modules/ ~200MB ~200MB
Total ~750MB ~550MB

For 5 concurrent agents: ~750MB + 5×550MB = ~3.5GB total

Worth it for complete isolation.

Layer 5: Skillz, domain-specific validation

Generic linting catches syntax errors. Skills catch domain errors.

Skillz is an MCP server that provides domain-specific validation skills.

What’s a skill?

A skill is a specialized instruction set with tool restrictions:

# .ruler/skills/api-validator/SKILL.md
---
name: api-validator
description: Validate API endpoints follow conventions. Triggers on "api check", "endpoint review".
allowed-tools: Read, Grep, Glob
---

# API Validation Skill

## Checks

1. All endpoints return `{data, meta}` structure
2. Error responses include `error_code` and `message`
3. No breaking changes to existing endpoints
4. Authentication required on non-public routes

## Validation Process

1. Find all router files: `glob("**/routers/**/*.py")`
2. Check response models include required fields
3. Verify error handlers follow pattern
4. Report violations with file:line references

The allowed-tools restriction is crucial. A validation skill shouldn’t edit files; only read them.

Setting up Skillz

Add to your MCP configuration (.mcp.json):

{
  "skillz": {
    "command": "uvx",
    "args": ["skillz@latest", "/path/to/your/project/.ruler/skills"]
  }
}

Ruler can generate this automatically:

# ruler.toml
[mcp.skillz]
command = "uvx"
args = ["skillz@latest", ".ruler/skills"]

Skill categories

Create skills for different validation needs:

.ruler/skills/
├── api-validator/      # API contract compliance
├── security-check/     # OWASP patterns
├── architecture/       # Layer boundaries
├── test-quality/       # Test coverage, meaningful assertions
└── domain-rules/       # Business logic constraints

Example architecture skill:

# .ruler/skills/architecture/SKILL.md
---
name: architecture
description: Validate layer boundaries. Triggers on "architecture check", "layer validation".
allowed-tools: Read, Grep, Glob
---

# Architecture Validation

## Layer Rules

| Layer             | Can Import From     |
|-------------------|---------------------|
| 1. Core           | Nothing             |
| 2. Domain         | Layer 1             |
| 3. Application    | Layers 1, 2         |
| 4. Infrastructure | Layers 1, 2, 3      |
| 5. Presentation   | All layers          |

## Validation

1. Find all Python files
2. Extract imports
3. Check each import against layer rules
4. Report violations: "file.py:15 - Data layer cannot import from Presentation layer"

Invoking skills

In your agent instructions:

## When to Use Skills

- Before committing router changes: invoke `api-validator`
- Before any commit: invoke `architecture`
- When adding auth code: invoke `security-check`

Agents can invoke skills via MCP tool calls or slash commands (depending on the client).

Layer 6: Agent learning (a custom layer)

The hardest problem in a multi-agent setup is context passing. When one agent figures out something non-obvious (“repository classes must use AsyncSession, not the engine directly”), that insight dies with the session. The next agent rediscovers it from scratch, often making a different call along the way.

I built a custom layer to fix this for my own projects. It is not part of the open-source stack above, so treat what follows as a sketch, not a recipe. Each learning is a 1-3 sentence insight plus the files and commits that back it. A git hook extracts these from commits, stores them locally, and writes them to a git-tracked JSONL file that syncs across my machines (the same git-native pattern Beads uses for issues). Before a task, an agent pulls the relevant learnings into its context within a token budget, and each learning carries a confidence score that decays as the files behind it change.

I won’t prescribe an implementation because mine is tied to my stack (PostgreSQL with pgvector for storage and semantic search), and your retrieval and storage choices should follow your own. The reusable idea is the part worth keeping: treat hard-won context like issues, store it git-natively, and feed it back to agents on demand. Layers 1-5, 7, and 8 all work fine without it.

Layer 7: Git hooks

Hooks enforce the workflow automatically. No discipline required.

Beads hooks

Beads installs its own git hooks:

bd hooks install

This creates:

  • pre-commit: Flushes pending issue changes
  • pre-push: Validates issues are synced
  • post-merge: Auto-syncs after pull

Pre-commit framework

Combine with pre-commit for code quality:

# .pre-commit-config.yaml
repos:
  # Code formatting
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format

  # Beads sync
  - repo: local
    hooks:
      - id: bd-sync
        name: bd sync (flush)
        entry: bash -c 'bd sync --flush-only || true'
        language: system
        always_run: true
        pass_filenames: false

  # Type checking
  - repo: local
    hooks:
      - id: mypy
        name: mypy
        entry: mypy
        language: system
        types: [python]

  # Ruler apply (regenerate agent configs if rules changed)
  - repo: local
    hooks:
      - id: ruler-apply
        name: ruler apply
        entry: npx @intellectronica/ruler apply
        language: system
        files: ^\.ruler/

Custom pre-push validation

#!/bin/sh
# .git/hooks/pre-push (or via bd hooks)

# 1. No uncommitted changes
if ! git diff --quiet; then
    echo "Error: Uncommitted changes"
    exit 1
fi

# 2. Issues synced
bd sync --status || {
    echo "Error: Beads not synced. Run 'bd sync'"
    exit 1
}

# 3. Tests pass with coverage
pytest --cov=. --cov-fail-under=80 || exit 1

# 4. Warn about in-progress issues
in_progress=$(bd list --status=in_progress --count)
if [ "$in_progress" -gt 0 ]; then
    echo "Warning: You have $in_progress in-progress issues"
    bd list --status=in_progress
    read -p "Continue push? [y/N] " confirm
    [ "$confirm" = "y" ] || exit 1
fi

Layer 8: Session protocols

The most overlooked piece: how agents start and end work.

Session start protocol

Add this to your AGENTS.md:

## Multi-Agent Coordination

**Session Start Protocol** (MANDATORY):

Option A - Shared directory (simple):

1. Get latest state: `bd sync`
2. Find available work: `bd ready`
3. Claim atomically: `scripts/bd_claim.sh <id>`
4. Push claim: `bd sync`
5. Now read and plan: `bd show <id>`

Option B - Isolated worktree (recommended for parallel work):

1. Get latest state: `bd sync`
2. Find available work: `bd ready`
3. Create worktree: `scripts/agent_worktree.sh create <id>`
   (This claims + creates isolated directory)
4. Work in: `cd ../project-worktrees/<id>`

**Stale Work Detection**:

Check for potentially abandoned work:
- `bd list --status=in_progress`
- If resuming abandoned work: `bd comment <id> "Resuming from previous session"`

Session close protocol

## Session Close Protocol

**CRITICAL**: Before ending any session, complete this checklist:

For shared directory:

1. `git status` - Check what changed
2. `git add <files>` - Stage code changes
3. `bd sync` - Sync beads changes
4. `git commit -m "..."` - Commit code
5. `bd sync` - Sync any new beads changes
6. `git push` - Push to remote

For worktree:

1. `git add . && git commit` - Commit in worktree
2. `git push -u origin work/<id>` - Push branch
3. Create PR if ready
4. `scripts/agent_worktree.sh remove <id>` - Cleanup worktree
5. `bd close <id> && bd sync` - Close issue

**Work is not done until pushed.**

Putting it all together

Here’s the complete workflow with worktrees:

Agent A (Machine 1)                    Agent B (Machine 2)
───────────────────                    ───────────────────
bd sync                                bd sync

bd ready                               bd ready
→ proj-001: Add auth                   → proj-001: Add auth
→ proj-002: Fix bug                    → proj-002: Fix bug

agent_worktree.sh create proj-001      (Agent A claims first)
→ Lock acquired
→ Worktree: ../worktrees/proj-001      bd sync (sees claim)

                                       bd ready
                                       → proj-002: Fix bug

cd ../worktrees/proj-001               agent_worktree.sh create proj-002
                                       → Lock acquired
                                       → Worktree: ../worktrees/proj-002

# Complete isolation                   cd ../worktrees/proj-002
uv run pytest  # Agent A's tests
uv sync        # Agent A's venv        uv run pytest  # Agent B's tests
                                       uv sync        # Agent B's venv

# Retrieve relevant learnings         # Retrieve relevant learnings
get_task_relevant_learnings(...)      get_task_relevant_learnings(...)
# → Sees prior architectural          # → Sees prior patterns
#   patterns discovered               #   from Agent A

git commit && git push                 git commit && git push
# work/proj-001 branch                 # work/proj-002 branch
# → Post-commit hook extracts         # → Post-commit hook extracts
#   learnings automatically            #   learnings automatically

agent_worktree.sh remove proj-001      agent_worktree.sh remove proj-002
bd close proj-001 && bd sync           bd close proj-002 && bd sync
# → Learnings exported to             # → Learnings exported to
#   .learnings/corpus.jsonl            #   .learnings/corpus.jsonl

Key properties:

  • No coordinator needed: Git is the source of truth
  • Atomic claims: PostgreSQL advisory locks prevent races
  • Complete isolation: Each agent has its own directory
  • Context persistence: Agent learning system captures and shares insights
  • Offline-capable: Work locally, sync when connected
  • Self-healing: Session-level locks auto-release on crash

Complete setup checklist

1. Install tools

# Beads
go install github.com/steveyegge/beads/cmd/bd@latest

# Ruler (via npx, no install needed)
npx @intellectronica/ruler --help

# Pre-commit
pip install pre-commit

2. Initialize project

cd your-project

# Initialize beads
bd init

# Create ruler structure
mkdir -p .ruler/skills
touch .ruler/ruler.toml
touch .ruler/AGENTS.md

# Create scripts directory
mkdir -p scripts

3. Configure Ruler

# .ruler/ruler.toml
[agents.claude]
output = "CLAUDE.md"
mcp_output = ".mcp.json"

[agents.cursor]
output = ".cursor/rules/AGENTS.md"

[mcp.skillz]
command = "uvx"
args = ["skillz@latest", ".ruler/skills"]

4. Write your rules

# .ruler/AGENTS.md

## Critical Constraints

1. Never decrease test coverage
2. TODOs must reference issues
3. No secrets in code

## Multi-Agent Coordination

**Session Start**:
- Simple: `bd sync``bd ready``bd_claim.sh <id>``bd sync` → plan
- Isolated: `bd sync``bd ready``agent_worktree.sh create <id>` → work

**Session Close**:
- Simple: `git status``git add``bd sync``git commit``bd sync``git push`
- Isolated: commit → push branch → remove worktree → `bd close <id>``bd sync`

## Before Every Commit

pytest && pylint .

5. Create scripts

# Create advisory lock module
cat > db/advisory_locks.py << 'EOF'
# ... (see implementation above)
EOF

# Create claim script
cat > scripts/bd_claim.py << 'EOF'
# ... (see implementation above)
EOF

# Create worktree script
cat > scripts/agent_worktree.sh << 'EOF'
# ... (see implementation above)
EOF
chmod +x scripts/*.sh

6. Set up agent learning (optional)

This layer is custom to my stack and not part of the open-source setup. See Layer 6 for the idea: extract a short insight plus its backing files from each commit, store it git-natively as a JSONL file synced across machines, and feed it back to agents on demand. Build it to fit your own storage and retrieval choices, or skip it. The rest of the stack works without it.

7. Generate and commit

# Generate agent configs
npx @intellectronica/ruler apply

# Install hooks
bd hooks install
pre-commit install

# Commit everything
git add .
git commit -m "Add multi-agent workflow infrastructure"
git push

Measuring success

How do you know if your multi-agent workflow is working?

Metric Healthy Warning
Race conditions per week 0 >2
Stale in_progress issues 0 >1
Merge conflicts on issues.jsonl <1/week >3/week
Build artifact conflicts 0 >1/week
Agent idle time <5% >20%
Rule violations caught by hooks Decreasing Increasing
Learning corpus size Growing Stagnant
Average learning confidence >0.7 <0.5
Learning invalidation rate <30% >50%

Use bd stats for project health:

bd stats
# Open: 12  In Progress: 2  Closed: 45  Blocked: 3

Check worktree status:

scripts/agent_worktree.sh list
# Active worktrees:
#   proj-abc1 (branch: work/proj-abc1, uncommitted: 0 files)
#   proj-xyz2 (branch: work/proj-xyz2, uncommitted: 3 files)

Check learning corpus health:

from dev_tools.agent_learning.tools import get_learning_statistics

stats = get_learning_statistics()
# {
#   "total_count": 150,
#   "active_count": 120,
#   "invalidated_count": 30,
#   "by_category": {"architecture": 45, "domain": 30, ...},
#   "average_confidence": 0.87
# }

Common pitfalls

1. Reading before claiming

# WRONG
bd show proj-001                         # Read first
# ... 5 minutes planning ...
bd update proj-001 --status=in_progress  # Too late! Agent B claimed it

# RIGHT (with advisory locks)
scripts/bd_claim.sh proj-001             # Atomic claim
bd show proj-001                         # Now read safely

2. Forgetting to push

# Agent A
git commit -m "Done with proj-001"
bd close proj-001
# Forgets to push, closes laptop

# Agent B (next day)
bd sync  # Sees proj-001 still in_progress locally
# Confusion ensues

Solution: Pre-push hooks warn about uncommitted work.

3. Not using worktrees for parallel work

# WRONG: Two agents in same directory
# Agent A                    Agent B
uv sync                      uv sync
# Race condition on .venv!

# RIGHT: Isolated worktrees
agent_worktree.sh create proj-001    agent_worktree.sh create proj-002
cd ../worktrees/proj-001             cd ../worktrees/proj-002
uv sync                              uv sync
# Each has its own .venv - no conflicts

4. Not using dependencies

# WRONG: Create independent issues
bd create --title="Design API"
bd create --title="Implement API"
bd create --title="Test API"
# Agent might start "Test API" before "Implement API" is done

# RIGHT: Create with dependencies
bd create --title="Design API"        # → proj-001
bd create --title="Implement API"     # → proj-002
bd create --title="Test API"          # → proj-003
bd dep add proj-002 proj-001          # Implement depends on Design
bd dep add proj-003 proj-002          # Test depends on Implement

# Now `bd ready` only shows unblocked work

Conclusion

Multi-agent development isn’t about having more agents; it’s about coordination infrastructure. The tradeoff between agent specialization and orchestration complexity is real, and most setups lean too far one way.

The solution is a layered architecture that addresses each coordination challenge:

  1. Ruler: Single source of truth for agent rules
  2. Beads: Git-native issue tracking with dependencies
  3. Advisory Locks: Atomic claim operations (no races)
  4. Worktrees: Complete filesystem isolation
  5. Skillz: Domain-specific validation skills
  6. Agent Learning: Context passing & state consistency
  7. Git Hooks: Automatic enforcement
  8. Protocols: Explicit session start/end procedures

Key insights

Coordination vs. Isolation: These are different problems requiring different solutions.

  • Advisory locks solve coordination (who works on what)
  • Worktrees solve isolation (how they work without conflicts)
  • Agent learning solves context passing (what agents know)

Context Passing & State Consistency: The part that usually trips teams up is context passing between agents and state consistency at scale. By transforming ephemeral task context into durable institutional knowledge, agents inherit prior discoveries and make consistent decisions across sessions and machines.

Observability: The debugging story is rough. A single agent is easy to reason about; multiple agents need solid observability and tracing to find where something went sideways. The system provides observability through:

  • Learning corpus health metrics
  • Staleness detection (automatic confidence decay)
  • Git history for learning evolution
  • Structured evidence linking learnings to files/commits/docs

Getting started

The tools are open source and composable. Start simple, add layers as needed:

  1. Start: Beads for issue tracking
  2. Add: Ruler when you have multiple agents
  3. Layer in: Advisory locks when you hit race conditions
  4. Add: Worktrees when build artifacts conflict
  5. Enable: Agent learning when context passing becomes a bottleneck

The result: agents that work together without stepping on each other, with shared institutional knowledge and consistent decision-making.


Resources: