Developer Guide

Everything you need to build, test, and contribute to Automergent — a terminal-native AI coding agent written in Go.

Go 1.25+ MIT License Charm v2 Stack CGO Enabled 43+ Tools 11 Themes

Project Overview

Automergent is an AI-powered coding agent that lives in your terminal. It provides a full-featured TUI built with the Charm v2 stack, supports multiple AI providers, and ships with 43+ built-in tools across 13 categories.

Core Technologies

ComponentTechnologyVersion
LanguageGo1.25+
Modulegithub.com/iSundram/Automergent—
LicenseMIT—
TUI FrameworkBubble Tea v2v2.0.2
StylingLip Gloss v2v2.0.1
Markdown RenderingGlamour v2v2.0.0
CLI FrameworkCobrav1.8.1
ConfigurationViperv1.19.0
AI SDKGoogle GenAI SDKv1.68.0
Tree-sittergo-tree-sitter—
Databasemodernc.org/sqlitev1.57.0
Prerequisites

You need Go 1.25+ with CGO enabled. Some features (tree-sitter parsing, SQLite) require CGO. On macOS, ensure Xcode command line tools are installed. On Linux, you may need gcc and the SQLite development headers.


Repository Structure

The project follows standard Go project layout with a clean separation between the CLI entrypoint, internal packages, and the documentation site.

cmd/automergent/ CLI entrypoint, Cobra command tree main.go rootCmd, provider resolution, tool registry wiring config_commands.go /config slash commands internal/ agent/ Agent loop, orchestration, approval policies agent.go core agent loop (Run, event bus, tool dispatch) init_routing.go init-phase intent classification & routing approval_policy.go when to require user confirmation approvals.go approval UI integration autocompact.go automatic context compaction context.go context isolation & budget management context_isolation.go per-task context boundaries fleet.go sub-agent fleet management hooks.go lifecycle hooks (pre/post tool, pre/post turn) modes.go agent modes (edit, plan, etc.) neural_context_test.go context selection tests orchestration_policy.go parallel/sequential/delegate strategies registry.go agent type registry rules.go behavioral rules & violation handling skills.go skill discovery & invocation subagents.go sub-agent spawning & lifecycle systemprompt.go layered system prompt composition triage.go request triage & complexity assessment agentdef/ agent type definitions builtin/ built-in agent implementations custom/ user-defined custom agents mcpbridge/ MCP ↔ agent bridge ai/ Provider abstraction layer provider.go Provider interface, Message, CompletionRequest/Response registry.go global provider registry stream.go streaming response helpers message.go message validation & sequence checks fallback.go fallback provider chain sequence.go message sequence validation google/ Gemini client (retry, streaming, GenAI SDK) openai/ OpenAI-compatible client anthropic/ Anthropic Claude client cache/ Caching layer (prompt cache, response cache) config/ Viper-based configuration config.go Config struct, defaults, validation loader.go config file loading, env var resolution providers.go provider credential management providers_catalog.go built-in provider catalog profiles.go named configuration profiles secrets.go secret redaction & handling context/ Budget management & dependency tracking debug/ Debug provider for logging requests & responses provider.go DebugProvider wraps ai.Provider logger.go structured JSON logger diagnostics/ Compiler error parsers & recovery editreview/ Edit review system errors/ Structured errors, retry policies, exit codes git/ Git operations (blame, branch, conflict, semantic diff) mcp/ Model Context Protocol client/server planning/ Planning system & task decomposition prompt/ Phased prompt system phases/ per-phase prompt template files recovery/ Error recovery strategies sandbox/ OS-level sandboxing (macOS, Docker, namespaces) session/ Persistent transcripts, history, undo stack shared/ Shared types & interfaces (TaskSpec, Intent, TodoItem) taskstate/ Task state store tools/ Tool system (43+ tools, 13 categories) tool.go Tool interface, Result, BaseTool, ToolCost registry.go tool registry (Register, Get, All, Clone) orchestrator.go tool orchestration & dispatch coerce.go argument type coercion pathscope.go path scope validation violation.go tool-level policy violations filesystem/ read_file, write_file, edit_file, glob, grep, list_dir shell/ async shell sessions (run, read, write, stop, list) web/ web_fetch, web_search security/ secrets_scan, dependency_audit lsp/ lsp_diagnostics agent/ task (sub-agent spawn), read_agent, list_agents interaction/ ask_user, notify skills/ discover_skills, skill ctxinfo/ ctx_inspect (context introspection) planmode/ enter_plan_mode, exit_plan_mode, verify_plan mcpres/ list_resources, read_resources (MCP) database/ sql (SQLite queries) tui/ Bubble Tea v2 terminal UI tui.go Run() entrypoint app/ core event loop, layout model commands/ slash command handlers (/compress, /sessions, etc.) components/ UI widgets (confirm, diff pane, file tree, palette) keys/ keyboard binding definitions render/ ANSI parser, diff engine, Markdown renderer themes/ 11 color palettes & 50+ styles per theme tips/ contextual help tips version/ Version, Commit, BuildDate (injected via ldflags) workflow/ Workflow engine (todo-driven task execution) docs-site/ Documentation website css/style.css shared styles (dark theme, components) pages/ HTML documentation pages examples/ Usage examples & sample configs

Building

Automergent uses a Makefile with standard targets. All build commands require CGO_ENABLED=1.

Make Targets

CommandDescription
make buildBuild the binary to bin/automergent
make installInstall to $GOPATH/bin
make testRun all tests (CGO_ENABLED=1 go test ./...)
make lintRun golangci-lint
make fmtFormat with gofmt + goimports
make tidyRun go mod tidy
make ciFull CI checks locally (test + vet + lint)
make cleanRemove bin/ directory
make releaseRun GoReleaser snapshot build

Build Flags

The build injects version metadata via ldflags:

makefile
LDFLAGS_AUTOMERGENT := -s -w \
  -X 'github.com/iSundram/Automergent/internal/version.Version=$(VERSION)' \
  -X 'github.com/iSundram/Automergent/internal/version.Commit=$(COMMIT)' \
  -X 'github.com/iSundram/Automergent/internal/version.BuildDate=$(BUILD_DATE)'

These values are read at runtime by the internal/version package and displayed via automergent --version.

Quick Start for Contributors

1

Clone and enter the repo:

git clone https://github.com/iSundram/Automergent.git
cd Automergent
2

Build:

make build
3

Run tests:

make test
4

Run full CI checks:

make ci

CI/CD Pipeline

Automergent uses GitHub Actions for continuous integration and GoReleaser for release distribution.

Continuous Integration

CI runs on every pull request and push to main. The pipeline performs:

yaml .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.25"
      - run: CGO_ENABLED=1 go test ./...
      - run: go vet ./...
      - run: golangci-lint run ./...

Release Pipeline

Releases are triggered by pushing a v* tag (e.g., v1.0.0). GoReleaser builds binaries for:

PlatformArchitectureOutput
Linuxamd64automergent_linux_amd64
Linuxarm64automergent_linux_arm64
macOSamd64automergent_darwin_amd64
yaml .github/workflows/release.yml
name: Release
on:
  push:
    tags: ["v*"]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-go@v5
        with:
          go-version: "1.25"
      - uses: goreleaser/goreleaser-action@v6
        with:
          version: latest
          args: release --clean -f dist/goreleaser-automergent.yaml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Testing

All tests require CGO enabled. Run the full suite with:

CGO_ENABLED=1 go test ./...

Test Patterns

The codebase uses several testing strategies:

Running Specific Tests

bash
# Run tests in a specific package
CGO_ENABLED=1 go test ./internal/tools/...

# Run tests matching a pattern
CGO_ENABLED=1 go test -run TestTool ./internal/tools/

# Run tests with verbose output
CGO_ENABLED=1 go test -v ./internal/agent/...

# Run tests with race detection
CGO_ENABLED=1 go test -race ./...

Linting

Install golangci-lint and run:

golangci-lint run ./...

The project uses the default golangci-lint configuration. CI will fail if any lint errors are found.


Key Interfaces

Automergent is built around a few core interfaces. Understanding these is essential for contributing new tools, providers, or themes.

Tool Interface

Every tool must implement the Tool interface defined in internal/tools/tool.go:

go internal/tools/tool.go
type Tool interface {
    Name() string
    Description() string
    Schema() map[string]any
    Execute(ctx context.Context, args map[string]any) (Result, error)
    RequiresConfirmation(mode string) bool
    IsConcurrencySafe(args map[string]any) bool
    IsReadOnly(args map[string]any) bool
    IsDestructive(args map[string]any) bool
    EstimatedCost() ToolCost
}
MethodPurposeDefault
Name()Unique tool identifier used in schemas and logs—
Description()Shown to the AI model in tool schemas—
Schema()JSON Schema for the tool's parameters—
Execute()Run the tool with the given arguments—
RequiresConfirmation()Whether this tool needs user approval before runningfalse
IsConcurrencySafe()Whether the tool can run concurrently with othersfalse
IsReadOnly()Whether the tool only reads (no side effects)false
IsDestructive()Whether the tool can destroy datafalse
EstimatedCost()Token and latency estimates for schedulinglow cost

BaseTool Helper

Embed BaseTool to get safe defaults for the boolean methods and cost estimates:

go
type MyTool struct {
    tools.BaseTool
}

func (t *MyTool) Name() string        { return "my_tool" }
func (t *MyTool) Description() string  { return "Does something useful" }
func (t *MyTool) Schema() map[string]any {
    return map[string]any{
        "type":       "object",
        "properties": map[string]any{...},
        "required":   []string{...},
    }
}
func (t *MyTool) Execute(ctx context.Context, args map[string]any) (tools.Result, error) {
    // implementation
}

Provider Interface

AI providers implement the Provider interface from internal/ai/provider.go:

go internal/ai/provider.go
type Provider interface {
    Name() string
    Complete(ctx context.Context, req CompletionRequest) (CompletionResponse, error)
    Models(ctx context.Context) ([]Model, error)
    TokenCount(messages []Message) (int, error)
    ContextLimit() int
}

RetryObserver Interface

Providers that retry internally can implement RetryObserver to report retry attempts to the UI:

go
type RetryObserver interface {
    SetRetryObserver(func(RetryInfo))
}

Wrapping providers (caching, debug, etc.) must forward this to the wrapped provider so retries remain visible.


Adding a New Tool

New tools are the most common contribution. Follow these steps to add one:

Step 1: Create the tool file

Add a new file in the appropriate internal/tools/<category>/ directory:

go internal/tools/mytool/mytool.go
package mytool

import (
    "context"

    "github.com/iSundram/Automergent/internal/tools"
)

// MyTool implements tools.Tool
type MyTool struct {
    tools.BaseTool
}

func (t *MyTool) Name() string { return "my_tool" }

func (t *MyTool) Description() string {
    return "Description shown to the AI model"
}

func (t *MyTool) Schema() map[string]any {
    return map[string]any{
        "type": "object",
        "properties": map[string]any{
            "input": map[string]any{
                "type":        "string",
                "description": "The input parameter",
            },
        },
        "required": []string{"input"},
    }
}

func (t *MyTool) Execute(ctx context.Context, args map[string]any) (tools.Result, error) {
    input, _ := args["input"].(string)

    // Tool logic here

    return tools.Result{
        Content: "Result text",
        Summary: "Short summary for UI",
    }, nil
}

Step 2: Register the tool

In cmd/automergent/main.go, add the import and register the tool in the tool registry setup block:

go cmd/automergent/main.go (excerpt)
import (
    toolsMyTool "github.com/iSundram/Automergent/internal/tools/mytool"
)

// In the tool registry setup:
reg.Register(&toolsMytool.MyTool{})

Step 3: Add a tool prompt (optional)

Add a prompt file in internal/prompt/phases/ to guide the model on when and how to use your tool.

Step 4: Test

Write tests in mytool_test.go following the table-driven test pattern:

go
func TestMyTool(t *testing.T) {
    tool := &MyTool{}

    if tool.Name() != "my_tool" {
        t.Errorf("expected name my_tool, got %s", tool.Name())
    }

    result, err := tool.Execute(context.Background(), map[string]any{
        "input": "test",
    })

    if err != nil {
        t.Fatal(err)
    }
    if result.IsError {
        t.Error("expected success")
    }
}

Adding a New Provider

New AI providers implement the Provider interface and register themselves in the global provider registry.

Step 1: Create the provider package

go internal/ai/myprovider/myprovider.go
package myprovider

import (
    "context"
    "github.com/iSundram/Automergent/internal/ai"
)

type Provider struct {
    config ai.ProviderConfig
}

func New(cfg ai.ProviderConfig) *Provider {
    return &Provider{config: cfg}
}

func (p *Provider) Name() string { return "myprovider" }

func (p *Provider) Complete(
    ctx context.Context,
    req ai.CompletionRequest,
) (ai.CompletionResponse, error) {
    // Call your provider's API here
    // Return a CompletionResponse with streaming support
}

func (p *Provider) Models(ctx context.Context) ([]ai.Model, error) {
    // Return available models
}

func (p *Provider) TokenCount(messages []ai.Message) (int, error) {
    // Estimate token count
}

func (p *Provider) ContextLimit() int {
    return 128000 // context window size
}

Step 2: Register in the provider catalog

Add your provider to internal/config/providers_catalog.go so it appears in the config schema and auto-discovery:

go
// In the catalog entry:
"myprovider": {
    Name:        "myprovider",
    DisplayName: "My Provider",
    EnvKey:      "MYPROVIDER_API_KEY",
    Models:      []string{"model-1", "model-2"},
    DefaultModel: "model-1",
}

Step 3: Wire into the CLI

In cmd/automergent/main.go, add a case in the provider switch:

go
case "myprovider":
    provider = myprovider.New(aiCfg)

Step 4: Configure

Add to your ~/.automergent/config.yaml:

yaml
provider: custom
model: model-1

providers:
  custom:
    apiKey: "your-api-key-here"
    baseUrl: "https://your-endpoint/v1"
    defaultModel: "model-1"

Adding a Theme

Automergent ships with 11 built-in themes. Each theme defines a 16-color palette and 50+ pre-built Lip Gloss styles.

Color Palette (16 slots)

SlotPurpose
BackgroundTerminal background color
SurfaceCard/panel background
OverlayDropdown/popover background
TextPrimary text color
SubtextSecondary text
MutedDimmed/disabled text
AccentPrimary accent (brand color)
AccentAltSecondary accent
GreenSuccess states
RedError states
YellowWarning states
BlueInformational / links
MagentaSpecial highlights
CyanCode / secondary accent
BorderNormalDefault border color
BorderFocusedFocused/active border

Step 1: Define the theme

Add a new file in internal/tui/themes/:

go internal/tui/themes/mytheme.go
package themes

import "charm.land/lipgloss/v2"

func MyTheme() *Theme {
    return &Theme{
        Name:          "mytheme",
        Background:    lipgloss.Color("#1a1b26"),
        Surface:       lipgloss.Color("#24283b"),
        Overlay:       lipgloss.Color("#414868"),
        Text:          lipgloss.Color("#c0caf5"),
        Subtext:       lipgloss.Color("#a9b1d6"),
        Muted:         lipgloss.Color("#565f89"),
        Accent:        lipgloss.Color("#7aa2f7"),
        AccentAlt:     lipgloss.Color("#bb9af7"),
        Green:         lipgloss.Color("#9ece6a"),
        Red:           lipgloss.Color("#f7768e"),
        Yellow:        lipgloss.Color("#e0af68"),
        Blue:          lipgloss.Color("#7aa2f7"),
        Magenta:       lipgloss.Color("#bb9af7"),
        Cyan:          lipgloss.Color("#7dcfff"),
        BorderNormal:  lipgloss.Color("#414868"),
        BorderFocused: lipgloss.Color("#7aa2f7"),
    }
}

Step 2: Register in the ThemeEngine

In internal/tui/themes/engine.go, add a RegisterTheme call in NewThemeEngine:

go internal/tui/themes/engine.go
func NewThemeEngine() *ThemeEngine {
    te := &ThemeEngine{...}

    // Register themes
    te.RegisterTheme(Catppuccin())
    te.RegisterTheme(Dracula())
    te.RegisterTheme(MyTheme())  // ← add here
    // ...
}

Built-in Themes

The following themes are available out of the box:

NameStyleBackground
modernDefault dark theme#0d1117
catppuccinCatppuccin Mocha#1e1e2e
draculaDracula#282a36
nordNord#2e3440
gruvboxGruvbox Dark#282828
onedarkAtom One Dark#282c34
tokyonightTokyo Night#1a1b26
solarized-darkSolarized Dark#002b36
solarized-lightSolarized Light#fdf6e3
high-contrastAccessibility#000000
monokaiClassic Monokai#272822

Debug Mode

Automergent includes a debug provider that wraps any AI provider to log all requests and responses. This is invaluable for understanding model behavior and debugging prompt issues.

How It Works

When debug mode is enabled, the DebugProvider wraps the primary provider as a transparent decorator:

go internal/debug/provider.go
type DebugProvider struct {
    provider ai.Provider
    logger   *Logger
}

func (d *DebugProvider) Complete(
    ctx context.Context, req ai.CompletionRequest,
) (ai.CompletionResponse, error) {
    start := time.Now()
    d.logger.LogRequest(ctx, d.provider.Name(), req)

    resp, err := d.provider.Complete(ctx, req)

    if err != nil {
        d.logger.LogError(ctx, d.provider.Name(), err, req)
        return nil, err
    }

    d.logger.LogResponse(ctx, d.provider.Name(), resp, time.Since(start))
    return resp, nil
}

Enabling Debug Mode

Add to your config or use the CLI flag:

yaml ~/.automergent/config.yaml
debug:
  enabled: true
  output_dir: "./debug-logs"

Or via environment variable:

export AUTOMERGENT_DEBUG_ENABLED=true

What Gets Logged

Architecture

The debug wiring in cmd/automergent/main.go shows the decorator chain:

go
// Provider resolution chain:
provider := googleProvider.New(aiCfg)    // base provider
provider = cache.NewCachingProvider(...)  // optional caching wrapper
provider = debug.NewDebugProvider(...)    // debug wrapper (outermost)

The debug provider also implements Unwrap() to let callers reach the underlying provider's capabilities.


Additional Notes

Session Persistence

Automergent persists conversation sessions to disk using JSONL format. The session system supports:

MCP Integration

The Model Context Protocol (MCP) allows extending Automergent with external tool servers. Configure MCP servers in your config:

yaml
mcp:
  servers:
    my-server:
      command: "npx"
      args: ["-y", "@my/mcp-server"]
      env:
        API_KEY: "xxx"

Contributing Guidelines

  1. Fork the repository and create a feature branch
  2. Write tests for new functionality
  3. Run make ci before submitting
  4. Follow existing code style and patterns
  5. Keep PRs focused — one feature or fix per PR
  6. Update documentation if adding user-facing features
Ready to contribute?

Start with a good first issue label, or pick a tool category that interests you. The tool system is the most accessible entry point for new contributors.