AI-powered PR lifecycle manager with a Copilot session dashboard.
Working with coding agents is powerful, but the surrounding workflow is full of friction. You submit a PR, walk away, and come back to find a failed pipeline, three review comments, a merge conflict, and a MerlinBot policy violation — each requiring you to context-switch back, diagnose, fix, push, and wait again. Meanwhile, your Copilot CLI sessions are trapped in the terminal where you started them.
Otto solves three problems:
Otto's web dashboard discovers all your Copilot CLI sessions from ~/.copilot/session-state/, shows their live status, and lets you resume and interact with them from any browser. Start a session at your desk, pick it up from your phone on the couch. Protected by a secret access key via Azure DevTunnels.
otto pr review https://dev.azure.com/org/project/_git/repo/pullrequest/123 \
"focus on error handling, concurrency safety, and resource cleanup"Not a generic "find bugs" review — you tell otto what to focus on and it applies that lens across the entire diff. Otto checks out the full repo so the LLM can read surrounding code for context, not just the diff. It then presents review comments in a table and lets you interactively select which to post as inline comments on the PR.
otto pr add https://dev.azure.com/org/project/_git/repo/pullrequest/123Otto watches your PR continuously. When a pipeline fails, it reads the build logs, classifies the failure (infrastructure vs code), and fixes it. When a reviewer leaves comments, it evaluates each one, fixes the code if it agrees, and replies with its reasoning. When merge conflicts appear, it rebases and resolves them. When MerlinBot flags policy issues, it addresses them. All while you're working on something else.
The goal: submit a PR and let otto get it to green without you babysitting it.
- PR autopilot — monitors PRs for pipeline failures, review comments, merge conflicts, and MerlinBot feedback; auto-fixes and re-pushes up to configurable max attempts
- Guided PR review — LLM-powered code review with focus guidance and interactive inline comment posting
- Copilot dashboard — web UI for managing Copilot CLI sessions with real-time streaming, sub-agent tracking, tool progress, session lifecycle events, and live activity monitoring
- Session sharing — generate time-limited read-only links to share a single session's live conversation
- Remote access — Azure DevTunnel integration with Entra ID, org-scoped, or anonymous access control; QR code for quick phone access
- Session discovery — automatically discovers persisted sessions with live activity timestamps
- Notifications — Microsoft Teams notifications for PR events via Power Automate (setup guide)
- Multi-provider — pluggable PR backends for Azure DevOps and GitHub
Requires Go 1.25.6+ and a GitHub Copilot subscription.
git clone https://github.com/alanmeadows/otto.git
cd otto
make build # produces bin/otto
make install # installs to ~/.local/bin- GitHub Copilot CLI —
npm install -g @github/copilot - bgtask (required for tunnel management) —
go install github.com/philsphicas/bgtask/cmd/bgtask@latest - devtunnel (optional, for remote access) —
winget install Microsoft.devtunnel; bothdevtunnel.exeand the Linuxdevtunnelbinary are supported
# Azure DevOps — only org and project needed if you're logged into az CLI
otto config set pr.default_provider "ado"
otto config set pr.providers.ado.organization "myorg"
otto config set pr.providers.ado.project "myproject"
# Otto authenticates via Entra ID (az CLI) automatically — no PAT required.
# Just make sure you're logged in:
az login
# Or GitHub
otto config set pr.default_provider "github"
otto config set pr.providers.github.token "$GITHUB_TOKEN"ADO Authentication: Otto uses
az account get-access-tokento obtain Entra ID bearer tokens automatically. Tokens are cached and refreshed transparently. A PAT is only needed as a fallback ifaz cliis not available — setOTTO_ADO_PATorpr.providers.ado.patin that case.
# Generic review
otto pr review https://github.com/org/repo/pull/42
# Guided review — tell the LLM what to focus on
otto pr review https://github.com/org/repo/pull/42 "focus on error handling and race conditions"
otto pr review https://dev.azure.com/org/proj/_git/repo/pullrequest/123 "check for security issues in input validation"Otto checks out the PR branch locally, analyzes the codebase structure, and creates an LLM session rooted in the repo — so the agent can read any file for context, not just the diff. Your guidance steers its focus. It then presents review comments in a table and lets you interactively select which to post as inline comments on the PR.
# Add a PR for tracking — otto watches it continuously
otto pr add https://dev.azure.com/org/proj/_git/repo/pullrequest/123
# Start the daemon
otto server startOtto will now poll the PR and automatically:
- Fix pipeline failures (classifies as infrastructure vs code, retries or fixes accordingly)
- Respond to review comments (agrees and fixes, or explains why it's by-design)
- Resolve merge conflicts (rebases and resolves via LLM)
- Address MerlinBot policy violations (ADO-specific)
- Send Teams notifications on status changes
# The dashboard and tunnel are enabled by default.
# Just start the server:
otto server start
# To disable specific features:
otto server start --no-dashboard
otto server start --no-tunnel
otto server start --no-pr-monitoringOpen http://localhost:4098 in your browser. The dashboard shows:
- All persisted Copilot CLI sessions with live "last activity" timestamps
- Resume any session and continue the conversation
- Create new sessions with model selection
- Real-time streaming of LLM responses, tool calls, sub-agents, and session events
- Share individual sessions via time-limited read-only links
- QR code for quick tunnel access from your phone
The tunnel is managed via bgtask so it survives otto restarts. Otto monitors the tunnel health every 2 minutes — if the relay connection drops (process alive but disconnected), otto automatically restarts the tunnel. The dashboard shows the tunnel status and URL when active, and links to the setup guide when inactive. Allowed users can be managed live from the dashboard sidebar.
The dashboard is a responsive web UI embedded in the otto binary. It connects to the Copilot SDK to manage sessions and streams events in real time via WebSocket.
The Copilot CLI embeds its own copilot server that Otto cannot connect to — so Otto reads session event logs from disk and runs its own server for interactive sessions. This creates three session modes:
- Watch — tail a live CLI session in real time (read-only)
- Fork — copy a session's history into a new writable session on Otto's server
- Resume — reconnect to a previously idle session with full interaction
See docs/copilot-architecture.md for the full explanation and architecture diagram.
Otto automatically starts and manages a headless Copilot server via bgtask on port 4321. This server handles all LLM interactions for both the dashboard and PR monitoring. It uses --no-auto-update to avoid corrupting ~/.copilot/pkg/ while other CLI sessions are running.
To manage your own server instead (e.g. on a different port or with custom flags):
# Start your own headless server
copilot --headless --port 5000 --no-auto-update --log-level info &
# Point otto at it
otto config set dashboard.copilot_server localhost:5000When dashboard.copilot_server is set, otto skips starting its own server and connects to yours.
When you create a new session from the dashboard, you can choose a working directory from a dropdown. This dropdown is populated from your tracked repositories — so the Copilot agent starts in the right repo with access to all its files.
To register a repository:
# Interactive — prompts for directory, git strategy, etc.
otto repo add
# Or add with a name
otto repo add my-projectA tracked repo configuration looks like this in .otto/otto.jsonc:
If worktree_dir is configured, all git worktrees under that directory appear in the dashboard's working directory picker. This lets you spin up a Copilot session pointed at a specific branch or worktree — useful for working on multiple features in parallel from your phone.
Tracked repos are also used by the PR autopilot to map PR branches to local working directories.
Click 🔗 Share in any active session to generate a share link (configurable expiry and mode):
https://your-tunnel.devtunnels.ms/shared/6548666f0382549d...
Share links support two modes:
- Read-only — the recipient sees the conversation streaming live (tool calls, responses, intent changes) but cannot send messages
- Read-write — the recipient enters a nickname and can send messages into the session alongside the owner
To access the dashboard from your phone, use Azure DevTunnels:
# One-time setup: install devtunnel and login
winget install Microsoft.devtunnel
devtunnel user login -e # Entra ID (Microsoft), or -g for GitHubRecommended configuration:
# Persistent tunnel ID — stable URL across restarts
otto config set dashboard.tunnel_id "yourname-otto"
# Start the server — dashboard and tunnel are on by default
otto server startOn start, otto logs an access URL with an embedded secret key:
tunnel connected — dashboard available remotely url=https://xxxx-4098.usw3.devtunnels.ms?key=a8f3b2c1...
Once connected, the Tunnel section in the dashboard sidebar shows a scannable QR code — scan it with your phone camera to open the dashboard instantly, no copy-pasting required. The QR encodes the full authenticated URL so you're logged in immediately.
If devtunnel or bgtask aren't installed, otto logs a warning with install instructions and continues without the tunnel. Use otto server status to see all active endpoints including the tunnel URL.
See docs/tunnel.md for the full setup guide.
Otto merges configuration from three layers (later layers override earlier):
- Built-in defaults — sensible starting values
- User config —
~/.config/otto/otto.jsonc - Repo config —
.otto/otto.jsonc(in the repository root)
Both config files use JSONC format (JSON with comments).
Use otto config show to inspect the merged result and otto config set <key> <value> to write values to the repo-local file.
User config (~/.config/otto/otto.jsonc) — personal settings shared across all repos:
{
// Server settings
"server": {
"source_dir": "~/clones/otto", // for upgrade --channel main
"upgrade_channel": "main"
},
// PR autopilot
"pr": {
"default_provider": "ado",
"max_fix_attempts": 5,
"providers": {
"ado": {
// Entra ID auth via az cli — no PAT needed
"organization": "myorg",
"project": "MyProject",
"auto_complete": true,
"merlinbot": true,
"create_work_item": true,
"work_item_area_path": "MyProject\\Team\\Area"
}
}
},
// Tracked repositories
"repos": [
{
"name": "my-project",
"primary_dir": "/home/user/repos/my-project",
"worktree_dir": "/home/user/repos/my-project.worktree",
"git_strategy": "worktree",
"branch_template": "users/myalias/{{.Name}}"
}
]
}Repo config (.otto/otto.jsonc) — repo-specific overrides committed with the project:
{
// Dashboard & tunnel settings for this project
"dashboard": {
"tunnel_id": "myname-otto",
"owner_nickname": "myname",
"tunnel_access": "tenant"
},
// Teams notifications for PR events
"notifications": {
"teams_webhook_url": "https://your-power-automate-webhook-url"
}
}| Key | Type | Default | Description |
|---|---|---|---|
models.primary |
string | claude-opus-4.6 |
Primary LLM model |
models.secondary |
string | gpt-5.2-codex |
Secondary model for multi-model review |
pr.default_provider |
string | ado |
Default PR provider (ado or github) |
pr.max_fix_attempts |
int | 5 |
Max auto-fix attempts per PR |
pr.disable_ai_footer |
bool | false |
Omit the "This response was generated by AI" footer from PR comments |
pr.providers.ado.organization |
string | ADO organization name | |
pr.providers.ado.project |
string | ADO project name | |
pr.providers.ado.pat |
string | ADO personal access token (fallback for az cli) |
|
pr.providers.ado.auto_complete |
bool | false |
Auto-complete ADO PRs |
pr.providers.ado.merlinbot |
bool | false |
Enable MerlinBot integration |
pr.providers.ado.create_work_item |
bool | false |
Create ADO work items for PR fixes |
pr.providers.ado.work_item_area_path |
string | ADO area path for created work items | |
pr.providers.github.token |
string | GitHub personal access token | |
server.poll_interval |
string | 10m |
Daemon PR poll interval |
server.port |
int | 4097 |
Daemon HTTP API port |
server.log_dir |
string | ~/.local/share/otto/logs |
Daemon log directory |
server.source_dir |
string | Path to otto source for upgrade --channel main |
|
server.upgrade_channel |
string | release |
Upgrade channel: release (go install @latest) or main (build from source) |
dashboard.port |
int | 4098 |
Dashboard web server port |
dashboard.copilot_server |
string | Override otto's managed copilot server (e.g. localhost:4321). Empty = otto starts one automatically |
|
dashboard.tunnel_id |
string | Persistent tunnel name for stable URL across restarts | |
dashboard.tunnel_access |
string | Access mode: anonymous, tenant, or empty (authenticated) |
|
dashboard.tunnel_allow_org |
string | GitHub org to grant tunnel access | |
dashboard.owner_email |
string | Dashboard owner email (auto-detected from tunnel JWT if empty) | |
dashboard.owner_nickname |
string | owner |
Display name for session owner in chat bubbles |
dashboard.allowed_users |
string[] | Emails allowed full dashboard access | |
dashboard.require_key |
bool | true |
Require passcode for remote dashboard access. Set to false for fully open dashboard (not recommended) |
notifications.teams_webhook_url |
string | Microsoft Teams webhook URL | |
notifications.events |
string[] | Events to notify on |
| Variable | Description |
|---|---|
OTTO_ADO_PAT |
Azure DevOps personal access token |
OTTO_GITHUB_TOKEN |
GitHub personal access token |
otto LLM-powered PR lifecycle manager
├── pr Manage pull requests
│ ├── add <url> Track a PR for auto-monitoring
│ ├── list List tracked PRs
│ ├── status [id] Show PR status
│ ├── remove [id] Stop tracking a PR
│ ├── fix [id] Manually trigger LLM fix
│ ├── log [id] Show PR activity log
│ ├── review <url> [guide] LLM-powered PR review with optional focus guidance
│ └── submit Submit the current branch as a PR
├── server Manage the otto daemon
│ ├── start Start the daemon
│ │ ├── --no-dashboard Disable Copilot session dashboard
│ │ ├── --no-tunnel Disable Azure DevTunnel
│ │ ├── --no-pr-monitoring Disable PR monitoring loop
│ │ ├── --insecure-tunnel Launch tunnel without authentication
│ │ ├── --insecure-dashboard Disable dashboard passcode requirement
│ │ ├── --dashboard-port Dashboard port (default: 4098)
│ │ ├── --port Server port (default: 4097)
│ │ └── --foreground Run in foreground
│ ├── stop Stop the daemon
│ ├── restart Restart the daemon (via bgtask)
│ ├── upgrade Stop, install latest, restart (via bgtask)
│ │ └── --channel "release" (go install @latest) or "main" (source)
│ ├── status Show daemon status, endpoints, and tunnel URL
│ ├── logs Show daemon log file path
│ └── install Install as systemd user service
├── repo Manage repositories
│ ├── add [name] Register a repository
│ ├── remove <name> Remove a tracked repository
│ └── list List tracked repositories
├── worktree Manage git worktrees
├── config Manage configuration
│ ├── show [--json] Show merged configuration
│ └── set <key> <value> Set a config value
└── completion Generate shell completions
All LLM interaction goes through the GitHub Copilot SDK for Go.
make build # Build binary to bin/otto
make test # Run all tests
make lint # Run golangci-lint
make vet # Run go vet
make all # lint + vet + test + build


{ "repos": [{ "name": "my-project", "primary_dir": "/home/user/repos/my-project", "worktree_dir": "/home/user/repos/my-project-worktrees", "git_strategy": "worktree", "branch_template": "otto/{{.Name}}" }] }