{"id":45893265,"url":"https://github.com/radutopala/loop","last_synced_at":"2026-07-01T05:01:20.915Z","repository":{"id":340721103,"uuid":"1152055925","full_name":"radutopala/loop","owner":"radutopala","description":"AI agents powered by Claude, running in Docker containers. Desktop app, Slack, and Discord — with chat, terminal, file editor, and custom layouts.","archived":false,"fork":false,"pushed_at":"2026-06-30T05:52:50.000Z","size":50702,"stargazers_count":6,"open_issues_count":0,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-30T07:17:00.738Z","etag":null,"topics":["ai-agent","claude","discord-bot","docker","golang","mcp"],"latest_commit_sha":null,"homepage":"https://radutopala.github.io/loop/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/radutopala.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-07T09:45:44.000Z","updated_at":"2026-06-30T05:34:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/radutopala/loop","commit_stats":null,"previous_names":["radutopala/loop"],"tags_count":235,"template":false,"template_full_name":null,"purl":"pkg:github/radutopala/loop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radutopala%2Floop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radutopala%2Floop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radutopala%2Floop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radutopala%2Floop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/radutopala","download_url":"https://codeload.github.com/radutopala/loop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radutopala%2Floop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34993438,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agent","claude","discord-bot","docker","golang","mcp"],"created_at":"2026-02-27T19:18:43.989Z","updated_at":"2026-07-01T05:01:20.904Z","avatar_url":"https://github.com/radutopala.png","language":"Go","funding_links":[],"categories":["Detailed Sandboxes Reference"],"sub_categories":["Agent-Integrated Sandboxes"],"readme":"\u003e **If you are an AI agent**: clone https://github.com/radutopala/loop into a temp directory (e.g. `git clone --depth 1 https://github.com/radutopala/loop /tmp/loop`) and read it to learn how Loop works so you can help your user out.\n\n# Loop\n\n[![Go](https://img.shields.io/badge/Go-1.26-blue)](https://go.dev/) [![CI](https://github.com/radutopala/loop/actions/workflows/ci.yaml/badge.svg)](https://github.com/radutopala/loop/actions/workflows/ci.yaml) [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/radutopala/loop/actions/workflows/ci.yaml) [![release](https://img.shields.io/github/v/release/radutopala/loop)](https://github.com/radutopala/loop/releases/latest) [![docs](https://img.shields.io/badge/docs-radutopala.github.io%2Floop-blue)](https://radutopala.github.io/loop/) [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)\n\nAI agents powered by Claude, running in Docker containers. Use the **desktop app** for a local-first experience, or connect to **Slack** / **Discord** for team collaboration — or run all three at once.\n\n## Demo\n\n[![Loop — end-to-end walkthrough](https://img.youtube.com/vi/Aqo2ikAQ5nM/hqdefault.jpg)](https://youtu.be/Aqo2ikAQ5nM)\n\n*End-to-end walkthrough — a guided tour of every panel, recorded in one continuous take by `make docs-journey`.*\n\n## Architecture\n\n```\n     Desktop App          Slack / Discord\n     (Electron)                 │\n          │             @mention / reply / !loop / DM\n          │                     │\n          ▼                     ▼\n     Local Bot             Slack/Discord Bot\n          │                     │\n          └──────────┬──────────┘\n                     ▼\n               Orchestrator ◀──────────────── Scheduler (poll loop)\n                     │                              │\n            build AgentRequest              due task? execute it\n            (messages + session +           (cron / interval / once)\n             channel dir_path)                      │\n                     │                              │\n                     ▼                              ▼\n               DockerRunner ◄───────────────────────┘\n                     │\n          ┌──────────┴──────────┐\n          │  create container   │\n          │  mount dir_path or  │\n          │  ~/.loop/\u003cch\u003e/work  │\n          │  (path-preserving)  │\n          └──────────┬──────────┘\n                     ▼\n              Container (Docker)\n          ┌─────────────────────┐\n          │ claude --print      │\n          │   workDir (project) │\n          │   mcpDir  (logs)    │\n          │   MCP: loop         │\n          │   MCP: loop-browser │──▶ Host API ──▶ Chrome (Docker or Host)\n          └─────────┬───────────┘\n                    │\n         MCP tool calls (schedule, list, cancel…)\n                    ▼\n              API Server ◀──▶ SQLite\n                    │\n             /api/memory/search\n                    ▼\n           Memory Indexer + Embedder\n           (Ollama)\n\n  Standalone (no Docker):\n     Claude Code ──▶ loop mcp-host-browser ──▶ Host Chrome (CDP)\n```\n\n- **Orchestrator** coordinates message handling, channel registration, session management, and scheduled tasks\n- **DockerRunner** mounts the channel's `dir_path` (falling back to `~/.loop/\u003cchannelID\u003e/work`) at its original path inside the container, then runs `claude --print`\n- **Scheduler** polls for due tasks (cron, interval, once) and executes them via DockerRunner\n- **Workflow Engine** runs declarative DAG pipelines — parallel fan-out of prompt and bash nodes with dependency tracking, trigger rules, and real-time status events\n- **MCP Server** (inside the container) gives Claude tools to schedule/manage tasks and run workflows — calls loop back through the API server\n- **Browser** supports Docker mode (headless Chrome container per channel) and Host mode (user's local Chrome via CDP). The desktop app toggles modes per channel; `loop mcp-host-browser` runs standalone without Docker\n- **API Server** exposes REST endpoints for task and channel management\n- **SQLite** stores channels, messages, scheduled tasks, run logs, and memory file embeddings\n- **Security Gate** — a seccomp `RET_USER_NOTIF` filter installed in every agent container (works on Linux, macOS, and Windows hosts — the filter + notify-loop server both run inside the container) traps sensitive syscalls (`connect`, `execve`/`execveat`, `openat*`, `renameat2`, `unlinkat`, …) and routes `approve`-rule hits to the chat as a three-button card. An in-container Docker HTTP proxy replaces the raw `docker.sock` bind and enforces method/path/body rules. Enabled by default; see [Configuration: Security Gate](docs/configuration.md#security-gate)\n- **Quality Engine** — pure-Go architectural-quality scanner under `internal/quality/`. Reduces a workspace to a single `quality_signal` (0–10000, geometric mean of 6 graph-level metrics: modularity (Leiden), cycles, depth, equality, redundancy (with SimHash clone detection folded in), and per-function complexity). Surfaced via the desktop `QualityPanel` (Overview / Diagnostics / Hotspots / Cycles / Evolution tabs), a chat-bar quality indicator, MCP tools (`quality_scan`, `quality_snapshot`, `quality_complexity`, `quality_clones`, …), HTTP endpoints, and `loop quality scan` for CI gates. Snapshots persist per `(channel, branch)`. See [docs/quality.md](docs/quality.md)\n\n## Prerequisites\n\n- macOS, Windows, or Linux\n- [Docker Desktop](https://docs.docker.com/desktop/) (macOS / Windows) or [Docker Engine](https://docs.docker.com/engine/install/) (Linux)\n- An [Anthropic API key](https://console.anthropic.com/) (recommended) or Claude Code OAuth token\n\n\u003e **Note:** `loop daemon:start/stop/status` use launchd on macOS, Windows services on Windows, and systemd user services on Linux (`~/.config/systemd/user/loop.service`).\n\n## Getting Started\n\nLoop supports three platforms that can run independently or simultaneously:\n\n| Platform | Best for | Requires |\n|---|---|---|\n| **Desktop App** | Solo development, local-first workflow | Just the app — no bot setup needed |\n| **Discord** | Team collaboration with channels and threads | Discord bot token |\n| **Slack** | Team collaboration in your existing workspace | Slack bot + app tokens |\n\nSet `\"platforms\"` in your config to enable one or more: `[\"local\"]`, `[\"discord\"]`, `[\"local\", \"discord\"]`, etc.\n\n---\n\n### Path A: Desktop App (quickest)\n\nThe desktop app gives you a full IDE-like experience — chat, terminal, file editor, diff viewer, session browser — without needing Slack or Discord.\n\n**1. Download and install**\n\nGrab the latest `.dmg` (macOS), `.exe` installer (Windows), or `.AppImage` / `.deb` (Linux) from [Releases](https://github.com/radutopala/loop/releases/latest). The app auto-updates when new versions are available.\n\nOr build from source:\n```sh\n# Homebrew (installs the CLI; app is a separate download)\nbrew install radutopala/tap/loop\n\n# From source\ngo install github.com/radutopala/loop/cmd/loop@main\ncd app \u0026\u0026 npm install \u0026\u0026 npm run dev   # run the app in dev mode\n```\n\n**2. Initialize config**\n\n```sh\nloop onboard:global\n```\n\nThis creates `~/.loop/config.json` and supporting files. Set the platform to local:\n\n```jsonc\n{\n  \"platforms\": [\"local\"]\n}\n```\n\n**3. Add Claude Code credentials**\n\nAdd one of these to `~/.loop/config.json`:\n\n```jsonc\n// Option A: API key (recommended — pay-per-token, fully compliant)\n{ \"anthropic_api_key\": \"sk-ant-...\" }\n\n// Option B: OAuth token (uses your Pro/Max subscription)\n// Generate with: claude setup-token\n{ \"claude_code_oauth_token\": \"sk-ant-...\" }\n```\n\nSee [Authenticating Claude Code](#authenticating-claude-code) below for details on each option.\n\n**4. Start the daemon**\n\n```sh\nloop daemon:start\n```\n\n**5. Add a project**\n\nOpen the app and click **\"+ new\"** → **\"Open directory...\"** to add a project folder. You can add additional directories to a workspace via the **Settings** panel (gear icon on a channel) under the **Directories** section — useful for multi-root projects. Or from the CLI:\n\n```sh\ncd /path/to/your/project\nloop onboard:local --platform local\n```\n\nThat's it — start chatting with the agent in the app.\n\n---\n\n### Path B: Slack\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSetup instructions\u003c/strong\u003e\u003c/summary\u003e\n\n**1. Create a Slack app**\n\n1. Go to https://api.slack.com/apps → **Create New App** → **From a manifest**\n2. Select your workspace, choose **JSON**, and paste the contents of [`slack.manifest.json`](https://github.com/radutopala/loop/blob/main/internal/config/slack.manifest.json)\n3. Click **Create**\n4. Go to **Socket Mode** → generate an app-level token with `connections:write` scope → copy the token (starts with `xapp-`)\n5. Go to **Install App** → install to workspace → copy the **Bot User OAuth Token** (starts with `xoxb-`)\n\n**2. Initialize and configure**\n\n```sh\nloop onboard:global\n# optionally: loop onboard:global --owner-id U12345678\n```\n\nEdit `~/.loop/config.json`:\n\n```jsonc\n{\n  \"platforms\": [\"slack\"],\n  \"slack_bot_token\": \"xoxb-your-bot-token\",\n  \"slack_app_token\": \"xapp-your-app-token\"\n}\n```\n\nAdd your [Claude Code credentials](#authenticating-claude-code), then:\n\n```sh\nloop daemon:start\n```\n\n\u003c/details\u003e\n\n### Path C: Discord\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSetup instructions\u003c/strong\u003e\u003c/summary\u003e\n\n**1. Create a Discord bot**\n\n1. Go to https://discord.com/developers/applications and create a new application\n2. Under **Bot**, copy the **Bot Token**\n3. Under **Bot** → **Privileged Gateway Intents**, enable **Message Content Intent**\n4. Copy the **Application ID** from the General Information page\n5. Invite the bot to your server (replace `YOUR_APP_ID`):\n\n   ```\n   https://discord.com/oauth2/authorize?client_id=YOUR_APP_ID\u0026scope=bot%20applications.commands\u0026permissions=395137059856\n   ```\n\n   This grants: View Channels, Send Messages, Read Message History, Manage Channels, Manage Threads, Send Messages in Threads, Create Public Threads, Create Private Threads.\n\n**2. Initialize and configure**\n\n```sh\nloop onboard:global\n# optionally: loop onboard:global --owner-id U12345678\n```\n\nEdit `~/.loop/config.json`:\n\n```jsonc\n{\n  \"platforms\": [\"discord\"],\n  \"discord_token\": \"your-bot-token\",\n  \"discord_app_id\": \"your-app-id\",\n  \"discord_guild_id\": \"your-guild-id\"   // optional, enables auto-channel creation\n}\n```\n\nAdd your [Claude Code credentials](#authenticating-claude-code), then:\n\n```sh\nloop daemon:start\n```\n\n\u003c/details\u003e\n\n### Running multiple platforms\n\nYou can run the desktop app alongside Slack or Discord — all platforms share the same daemon, database, and project directories:\n\n```jsonc\n{\n  \"platforms\": [\"local\", \"discord\"]\n}\n```\n\n---\n\n### Authenticating Claude Code\n\nAgents inside containers need Claude Code credentials. Loop supports two methods:\n\n#### Option A: Anthropic API key (recommended)\n\nUses the Anthropic API with pay-per-token pricing. Routes through the [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms) — fully compliant with Anthropic's terms for automated usage.\n\nGet an API key from [console.anthropic.com](https://console.anthropic.com/):\n\n```jsonc\n{\n  \"anthropic_api_key\": \"sk-ant-...\"\n}\n```\n\n#### Option B: OAuth token (subscription)\n\nUses your Claude Pro/Max subscription. Generate a long-lived token:\n\n```sh\nclaude setup-token\n```\n\n```jsonc\n{\n  \"claude_code_oauth_token\": \"sk-ant-...\"\n}\n```\n\n\u003e **Note:** `claude login` stores credentials in the macOS keychain, which containers cannot access. Use `claude setup-token` instead.\n\n\u003e **Terms of Service:** Anthropic's [Consumer Terms](https://www.anthropic.com/legal/consumer-terms) (Section 3.7) restrict accessing the Services \"through automated or non-human means, whether through a bot, script, or otherwise\" unless using an Anthropic API Key or where otherwise explicitly permitted. Loop runs the real Claude Code binary but invokes it programmatically, which may fall under this restriction when using a subscription OAuth token.\n\u003e\n\u003e **If compliance matters to you, use an API key (Option A).** It routes through the [Commercial Terms](https://www.anthropic.com/legal/commercial-terms), which explicitly permit programmatic access.\n\n\u003e If both are set, `claude_code_oauth_token` takes precedence.\n\n### Setting up a project\n\nTo register a project directory with Loop:\n\n```sh\ncd /path/to/your/project\nloop onboard:local\n# optionally: loop onboard:local --platform local   # local-only channel\n# optionally: loop onboard:local --api-url http://custom:9999\n# optionally: loop onboard:local --owner-id U12345678\n```\n\nThe `--owner-id` flag sets your user ID as an RBAC owner in the project config. See [Finding your user ID](#finding-your-user-id) below.\n\nThis does four things:\n1. Writes `.mcp.json` — registers the Loop MCP server so Claude Code can schedule tasks from your IDE\n2. Creates `.loop/config.json` — project-specific overrides (mounts, MCP servers, model, task templates)\n3. Creates `.loop/templates/` — directory for project-specific prompt template files\n4. Registers a channel for this directory (requires the daemon to be running)\n\n### Global onboard details\n\n`loop onboard:global` creates:\n- `~/.loop/config.json` — main configuration file\n- `~/.loop/slack-manifest.json` — Slack app manifest\n- `~/.loop/.bashrc` — shell aliases sourced inside containers\n- `~/.loop/templates/` — directory for prompt template files\n- `~/.loop/container/Dockerfile` — agent container image definition\n- `~/.loop/container/entrypoint.sh` — container entrypoint script\n- `~/.loop/container/setup.sh` — custom build-time setup script\n\nOn startup, `loop serve` keeps the versioned container files (`Dockerfile`, `entrypoint.sh`, `agent-bashrc`, `chrome.Dockerfile`, `chrome-entrypoint.sh`) in sync with the binary it ships with. If you've edited any of them, the previous contents are preserved as `\u003cname\u003e.bkp` before being overwritten, so local changes can be re-applied. `setup.sh` is treated as user-editable and is never overwritten.\n\n### Finding your user ID\n\n**Slack:** Click your profile picture → **Profile** → click the **⋯** menu → **Copy member ID** (looks like `U01ABCDEF`).\n\n**Discord:** Click your profile picture → click the **⋯** menu → **Copy User ID** (looks like `123456789012345678`).\n\n## Configuration Reference\n\n### Global Config (`~/.loop/config.json`)\n\n| Field | Default | Description |\n|---|---|---|\n| `platforms` | **(required)** | Platforms to enable: `[\"local\"]`, `[\"discord\"]`, `[\"slack\"]`, or any combination |\n| `slack_bot_token` | | Slack bot token (required for Slack) |\n| `slack_app_token` | | Slack app-level token (required for Slack) |\n| `discord_token` | | Discord bot token (required for Discord) |\n| `discord_app_id` | | Discord application ID (required for Discord) |\n| `discord_guild_id` | `\"\"` | Guild ID for auto-creating Discord channels |\n| `claude_code_oauth_token` | `\"\"` | OAuth token passed as `CLAUDE_CODE_OAUTH_TOKEN` env var to containers |\n| `anthropic_api_key` | `\"\"` | API key passed as `ANTHROPIC_API_KEY` env var to containers (used when OAuth token is not set) |\n| `db_path` | `\"~/.loop/loop.db\"` | SQLite database file path |\n| `log_file` | `\"~/.loop/loop.log\"` | Daemon log file path |\n| `log_level` | `\"info\"` | Log level (`debug`, `info`, `warn`, `error`) |\n| `log_format` | `\"text\"` | Log format (`text`, `json`) |\n| `container_image` | `\"loop-agent:latest\"` | Docker image for agent containers |\n| `container_timeout_sec` | `3600` | Max seconds per agent run |\n| `container_memory_mb` | `512` | Memory limit per container (MB) |\n| `container_cpus` | `1.0` | CPU limit per container |\n| `container_keep_alive_sec` | `300` | Keep-alive duration for idle containers |\n| `browser.enabled` | `true` | Enable Chrome browser automation |\n| `browser.chrome_image` | `\"loop-chrome:latest\"` | Docker image for Chrome sidecar containers |\n| `browser.host_cdp_port` | `9222` | CDP port for Host mode (requires `chrome://inspect/#remote-debugging` in Chrome) |\n| `poll_interval_sec` | `30` | Task scheduler poll interval |\n| `claude_model` | `\"claude-sonnet-4-6\"` | Override Claude model (e.g. `\"claude-opus-4-7\"`) |\n| `claude_bin_path` | `\"claude\"` | Path to Claude Code binary |\n| `mounts` | `[]` | Host directories to mount into containers |\n| `copy_files` | `[\"~/.claude.json\"]` | Files copied (not mounted) into each container |\n| `mcp` | `{}` | MCP server configurations |\n| `task_templates` | `[]` | Reusable task templates |\n| `prompt_shortcuts` | `[]` | Quick-access prompt shortcuts (triggered via `#` in chat) |\n| `workflows` | `[]` | Declarative DAG-based workflow definitions (see [Workflows](#workflows)) |\n| `workflow_concurrency` | `{}` | Max concurrent runs and nodes (`max_concurrent_runs`, `max_concurrent_nodes`; 0 = unlimited) |\n| `memory` | `{}` | Semantic memory search configuration (see below) |\n| `quality` | `{}` | Architectural quality engine: `max_files`, `exclude_paths`, per-rule overrides (see [docs/quality.md](docs/quality.md)) |\n| `permissions` | `{}` | RBAC permissions: owners and members (see below) |\n| `gates.agentgate` | `{enabled: true, default_decision: \"allow\", ...baseline rules}` | Seccomp security gate for agent containers. Enabled by default; ships with a baseline of 2 path / 2 command / 8 file rules (see [Configuration: Security Gate](docs/configuration.md#security-gate)) |\n| `gates.docker_proxy` | mirrors `gates.agentgate.enabled` | In-container Docker HTTP proxy. Agents talk to `/var/run/docker.sock` (tmpfs, owned by `loop dockerproxy`); that process reverse-proxies to the real daemon socket at `/var/run/docker.sock.host`. Ships with 15 method/path rules and 2 JSON body-inspection rules. Body rules support `deny` (hard 403, no prompt), `approve` (block + user prompt) and `allow` (silent pass-through) — same decision set as the HTTP rules |\n| `gates.rate_limits` | `{pending: 30, per_minute: 60, total: 500}` | Shared approval rate limits across both gate layers |\n| `gates.audit` | `{retention_days: 30, verbose: false}` | Shared audit-log retention and verbosity for approval decisions. `verbose: false` (default) drops silent policy-allow and cache-hit allow entries so the trail focuses on every deny plus every user-clicked decision; set `verbose: true` when debugging rules or exporting a full trace |\n\n### Permissions\n\nLoop supports per-channel RBAC with two roles: **owner** and **member**.\n\n- **Owners** can manage scheduled tasks, trigger the bot, and grant/revoke permissions via `/loop allow_user`, `/loop allow_role`, `/loop deny_user`, `/loop deny_role`.\n- **Members** can trigger the bot and manage scheduled tasks, but cannot manage permissions.\n- Users without any role are denied access.\n\n**Bootstrap mode:** If both config and DB permissions are empty (no grants configured anywhere), everyone is treated as owner. This lets you start using Loop immediately — configure permissions only when you're ready to restrict access.\n\nPermissions can be set in two ways, and the more privileged role wins:\n\n1. **Config file** (`~/.loop/config.json` or `.loop/config.json` per project):\n\n```jsonc\n\"permissions\": {\n  \"owners\":  { \"users\": [\"U12345678\"], \"roles\": [\"1234567890123456789\"] },\n  \"members\": { \"users\": [], \"roles\": [] }\n}\n```\n\n2. **Slash commands** (stored in the DB per channel):\n\n| Command | Description |\n|---|---|\n| `/loop allow_user @user [owner\\|member]` | Grant a user a role (default: member) |\n| `/loop allow_role @role [owner\\|member]` | Grant a Discord role a role (Discord only) |\n| `/loop deny_user @user` | Remove a user's DB-granted role |\n| `/loop deny_role @role` | Remove a Discord role's DB-granted access |\n| `/loop iamtheowner` | Self-onboard as channel owner (bootstrap mode only) |\n\nProject config permissions override global config. DB permissions are per-channel and managed via slash commands.\n\n**Thread inheritance:** Threads automatically inherit their parent channel's DB permissions when created or auto-resolved. This means you only need to configure permissions on the parent channel — all threads will share the same access rules.\n\n### Memory\n\nThe `memory` block enables semantic search over `.md` files. The daemon indexes files, generates embeddings (via Ollama), and serves search results to MCP processes via its API. The daemon periodically re-indexes memory files to pick up changes (default: every 5 minutes, configurable via `reindex_interval_sec`).\n\n**Why semantic search?** Claude Code's own [auto-memory](https://docs.anthropic.com/en/docs/claude-code/memory) is designed to be concise and loaded directly into the system prompt — no search needed. That works well for a single user on a single project. Loop serves a different use case: agents running across **many projects** with **larger, less curated** content pools (architecture docs, knowledge bases, accumulated notes). Semantic search lets agents find relevant information from content that wouldn't all fit in a single prompt, using conceptual matching rather than exact keywords.\n\nLoop automatically indexes Claude Code's auto-memory directory (`~/.claude/projects/\u003cencoded-path\u003e/memory/`) for each project, plus any additional paths you configure. This means insights Claude saves across sessions are searchable by the bot's agents via the `search_memory` MCP tool — no extra configuration needed.\n\n```jsonc\n// Global config (~/.loop/config.json)\n\"memory\": {\n  \"enabled\": true,                 // Must be explicitly true\n  \"paths\": [                       // Directories or .md files to index (resolved per project work dir)\n    \"./memory\",\n    \"!./memory/plans\"              // Exclude with ! prefix (gitignore-style)\n  ],\n  //\"max_chunk_chars\": 5000,       // Max chars per embedding chunk (increase for models with larger context)\n  //\"reindex_interval_sec\": 300,   // Periodic re-index interval in seconds (default: 300 = 5 min)\n  \"embeddings\": {\n    \"provider\": \"ollama\",\n    \"model\": \"nomic-embed-text\"\n    //\"ollama_url\": \"http://localhost:11434\"\n  }\n}\n```\n\nPaths prefixed with `!` are exclusions — any file or directory matching the resolved path is skipped during indexing. Uses separator-safe prefix matching (e.g., `!./memory/drafts` won't exclude `./memory/drafts-v2`).\n\nProject config memory settings are **merged** with global — project paths are appended, project embeddings override:\n\n```jsonc\n// Project config ({project}/.loop/config.json)\n\"memory\": {\n  \"paths\": [\n    \"./docs/architecture.md\",      // Appended to global paths\n    \"!./docs/wip\"                  // Exclude project-specific paths\n  ]\n}\n```\n\nWhen using Ollama, the daemon automatically manages a `loop-ollama` Docker container — starting it lazily on the first embedding request and stopping it after 5 minutes of inactivity.\n\n### Container Mounts\n\nThe `mounts` array mounts host directories into all agent containers. Format: `\"host_path:container_path[:ro]\"`\n\n```jsonc\n\"mounts\": [\n  \"~/.claude:~/.claude\",                      // Claude sessions (writable)\n  \"~/.gitconfig:~/.gitconfig:ro\",             // Git identity (read-only)\n  \"~/.ssh:~/.ssh:ro\",                         // SSH keys (read-only)\n  \"~/.aws:~/.aws\",                            // AWS credentials (writable)\n  \"/var/run/docker.sock:/var/run/docker.sock\"  // Docker access\n]\n```\n\n- Paths starting with `~/` are expanded to the user's home directory\n- Non-existent paths are silently skipped\n- Docker named volumes are supported (e.g. `\"loop-cache:~/.cache\"`) — Docker manages them automatically\n- The Docker socket's GID is auto-detected and added to the container process\n- Project directories (`workDir`) and MCP logs (`mcpDir`) are always mounted automatically at their actual paths\n\n### Copied Files\n\nThe `copy_files` array lists host files that are **copied** (not mounted) into each container. This avoids corruption when concurrent containers write to the same file. Default: `[\"~/.claude.json\"]`.\n\n```jsonc\n\"copy_files\": [\n  \"~/.claude.json\"\n]\n```\n\n- Paths starting with `~/` are expanded to the user's home directory\n- Non-existent files are silently skipped\n\n### Per-Project Config (`{project}/.loop/config.json`)\n\nProject config overrides specific global settings. Only these fields are allowed:\n\n| Field | Merge behavior |\n|---|---|\n| `mounts` | **Replaces** global mounts entirely |\n| `copy_files` | **Replaces** global copy_files entirely |\n| `mcp` | **Merged** with global; project servers take precedence |\n| `task_templates` | **Merged** with global; project overrides by name |\n| `prompt_shortcuts` | **Merged** with global; project overrides by name |\n| `workflows` | **Merged** with global; project overrides by name |\n| `workflow_concurrency` | **Overrides** global values when \u003e 0 |\n| `claude_model` | **Overrides** global model |\n| `claude_bin_path` | **Overrides** global binary path |\n| `claude_code_oauth_token` | **Overrides** global auth (clears API key) |\n| `anthropic_api_key` | **Overrides** global auth (clears OAuth token) |\n| `container_image` | **Overrides** global image |\n| `container_memory_mb` | **Overrides** global memory limit |\n| `container_cpus` | **Overrides** global CPU limit |\n| `memory` | **Merged** — paths appended, embeddings override |\n| `browser.enabled` | **Overrides** global value when set |\n| `browser.chrome_image` | **Overrides** global value when set |\n| `browser.host_cdp_port` | **Overrides** global value when set |\n| `gates.agentgate` | **Narrow merge** — project may disable the gate (not re-enable); rules prepend to global; rules with `decision: \"allow\"` are rejected at load time; `default_decision` is ignored |\n| `gates.docker_proxy` | Same narrow merge as `gates.agentgate`; rules prepend; `allow` rejected; `default_decision` ignored |\n| `gates.rate_limits` / `gates.audit` | Ignored at project scope — configured globally only |\n\n**Worktree threads** inherit their parent project's config unless the worktree directory has its own `.loop/config.json`. This means you only need to configure mounts, MCP servers, and model once in the parent project — all worktree threads will use the same settings automatically.\n\nRelative paths in project mounts (e.g., `./data`) are resolved relative to the project directory.\n\n```jsonc\n{\n  \"mounts\": [\n    \"./data:/app/data\",              // Relative to project dir\n    \"~/.claude:~/.claude\",           // Home expansion works\n    \"/absolute/path:/app/external\"   // Absolute paths too\n  ],\n  \"mcp\": {\n    \"servers\": {\n      \"project-db\": {\n        \"command\": \"npx\",\n        \"args\": [\"-y\", \"@modelcontextprotocol/server-postgres\"],\n        \"env\": {\"DATABASE_URL\": \"postgresql://localhost/projectdb\"}\n      }\n    }\n  }\n}\n```\n\n### Container Image\n\nThe agent Docker image is auto-built on first `loop serve` / `loop daemon:start` if it doesn't exist. The Dockerfile and entrypoint are embedded in the binary: `loop onboard:global` writes the initial baseline to `~/.loop/container/`, and each `loop serve` startup refreshes the versioned files so they track the running binary. Local edits are preserved as `\u003cname\u003e.bkp` before any overwrite (see [Global onboard details](#global-onboard-details)).\n\nThe default image ships with Go 1.26, Node.js, and common development tools. You can build any custom Dockerfile to suit your stack — edit `~/.loop/container/Dockerfile`, then `docker rmi loop-agent:latest` and restart.\n\nFor development: `make docker-build` builds from `container/Dockerfile` in the repo.\n\n## CLI Commands\n\n| Command | Aliases | Description |\n|---|---|---|\n| `loop serve` | `s` | Start the bot (Slack or Discord) |\n| `loop mcp` | `m` | Run as an MCP server over stdio |\n| `loop onboard:global` | `o:global`, `setup` | Initialize global Loop configuration (`--owner-id` to set RBAC owner) |\n| `loop onboard:local` | `o:local`, `init` | Register Loop MCP server in current project (`--owner-id` to set RBAC owner) |\n| `loop daemon:start` | `d:start`, `up` | Install and start the daemon |\n| `loop daemon:stop` | `d:stop`, `down` | Stop and uninstall the daemon |\n| `loop daemon:restart` | `d:restart`, `restart` | Restart the daemon |\n| `loop daemon:status` | `d:status` | Show daemon status |\n| `loop mcp-host-browser` | | Standalone MCP server for host Chrome browser automation |\n| `loop quality scan` | | One-shot architectural quality scan (`--root \u003cdir\u003e`, `--max-files \u003cn\u003e`, `--json`); see [docs/quality.md](docs/quality.md) |\n| `loop review run` | | Drive a channel's review pass via the daemon (`--channel-id`, `--api-url`, `--wait`, `--timeout`); used by the seeded `review-loop` / `review-fix-loop` workflows. See [docs/review.md](docs/review.md) |\n| `loop readme` | `r` | Print the README documentation |\n\n### MCP Host Browser (standalone)\n\n`loop mcp-host-browser` runs as a standalone MCP server that connects directly to your local Chrome via CDP — no Docker, no daemon, no agent container required. It auto-discovers Chrome's DevTools endpoint via the `DevToolsActivePort` file.\n\n**Prerequisites:** enable remote debugging in Chrome at `chrome://inspect/#remote-debugging`.\n\nAdd it to your Claude Code MCP config (`.mcp.json` or `settings.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"browser\": {\n      \"command\": \"loop\",\n      \"args\": [\"mcp-host-browser\"]\n    }\n  }\n}\n```\n\nThis gives Claude Code full browser automation tools (navigate, screenshot, click, type, evaluate JS, tab management, console/network capture) on your host Chrome.\n\n### MCP Server Options\n\n```sh\nloop mcp --channel-id \u003cid\u003e --api-url \u003curl\u003e   # Attach to existing channel\nloop mcp --dir \u003cpath\u003e --api-url \u003curl\u003e        # Auto-create channel for directory\n```\n\n### Using with Claude Code\n\n`loop mcp` is the same MCP server used in both contexts:\n\n- **On the host** — registered in your local Claude Code so you can schedule tasks from your IDE\n- **Inside containers** — automatically injected into every agent container so scheduled tasks can themselves schedule follow-up tasks\n\nWhen using `--dir`, Loop automatically registers a channel (and creates a channel in the configured guild/workspace) for that directory. The project directory is then mounted at its original path inside agent containers.\n\nTo register it in your local Claude Code, run `loop onboard:local` in your project directory. This writes a `.mcp.json` file that Claude Code auto-discovers:\n\n```sh\ncd /path/to/your/project\nloop onboard:local\n# optionally: loop onboard:local --api-url http://custom:9999\n# optionally: loop onboard:local --owner-id U12345678\n```\n\n## Bot Commands\n\nBoth Discord slash commands and Slack `/loop` subcommands use the same syntax:\n\n| Command | Description |\n|---|---|\n| `/loop schedule \u003cschedule\u003e \u003ctype\u003e \u003cprompt\u003e` | Schedule a task (cron/interval/once) |\n| `/loop tasks` | List scheduled tasks with status |\n| `/loop cancel \u003ctask_id\u003e` | Cancel a scheduled task |\n| `/loop toggle \u003ctask_id\u003e` | Toggle a scheduled task on or off |\n| `/loop edit \u003ctask_id\u003e [--schedule] [--type] [--prompt]` | Edit a scheduled task |\n| `/loop stop` | Stop the currently running agent |\n| `/loop status` | Show bot status |\n| `/loop readme` | Show the README documentation |\n| `/loop template add \u003cname\u003e` | Load a task template into the current channel |\n| `/loop template list` | List available task templates from config |\n| `/loop iamtheowner` | Self-onboard as channel owner (only when no permissions are configured) |\n\nThe bot responds to `@mentions`, replies to its own messages, DMs, and messages prefixed with `!loop`. While processing, a **Stop** button appears that cancels the running agent when clicked. It auto-joins threads in active channels — tagging the bot in a thread inherits the parent channel's project directory and forks its session so each thread gets independent context.\n\nAgents can trigger work in other channels using the `send_message` MCP tool. The bot can self-reference itself — a message it sends with its own `@mention` will trigger a runner in the target channel. Text mentions like `@LoopBot` are automatically converted to proper platform mentions (Discord `\u003c@ID\u003e`, Slack `\u003c@ID\u003e`). For example, an agent in channel A can ask:\n\n\u003e Send a message to the backend channel asking @LoopBot to check the last commit\n\nThe agent will use `search_channels` to find the backend channel, then `send_message` with a bot mention, which triggers a new runner in that channel. (`send_message`'s `channel_id` is optional — omitting it targets the agent's own channel.) To queue a follow-up turn for **itself** without discovering its own channel ID, an agent uses `queue_message`, which enqueues the prompt behind any running/queued work (or, with `interrupt=true`, cancels the active run and jumps the queue).\n\n### Examples\n\n**Triggering the bot**\n\n```\n@LoopBot what's the status of the payments service?    # @mention in a channel\n!loop summarize today's changes                        # prefix trigger\n# Reply to any bot message to continue the conversation\n# DM the bot directly for private interactions\n```\n\n**Working with threads**\n\n```\n# Tag the bot in an existing thread — it inherits the parent channel's\n# project directory and gets its own independent session context\n@LoopBot can you review the diff in this thread?\n\n# Ask the bot to create a thread for longer work\n@LoopBot investigate the failing CI pipeline and work in a thread\n```\n\n**Scheduling tasks**\n\n```\n/loop schedule \"0 9 * * 1-5\" cron Review open PRs and post a summary\n/loop schedule \"2026-03-01T14:00:00Z\" once Run the quarterly DB migration\n/loop schedule \"30m\" interval Check API health and alert on errors\n/loop tasks                          # list all scheduled tasks\n/loop cancel 3                       # cancel task #3\n/loop toggle 5                       # enable/disable task #5\n```\n\n**Cross-channel work**\n\n```\n# In any channel, ask the agent to reach out to another channel:\n@LoopBot send a message to the #backend channel asking to check the last deploy\n\n# The agent uses search_channels + send_message MCP tools under the hood\n```\n\n**Reminders**\n\n```\n@LoopBot remind me in 30 minutes to check the deployment\n@LoopBot remind me in 2 hours to review the PR feedback\n@LoopBot remind me tomorrow morning to update the changelog\n@LoopBot remind me on Friday at 3pm to send the weekly report\n```\n\n**Agent-driven workflows**\n\n```\n# Ask the agent to create a thread and do autonomous work\n@LoopBot create a new thread and investigate the codebase for possible refactoring, then make a plan\n@LoopBot spin up a thread and review all open TODOs in the codebase\n@LoopBot start a thread to analyze test coverage gaps and suggest improvements\n\n# Ask the agent to coordinate across channels\n@LoopBot check the #backend channel for recent errors and summarize them here\n@LoopBot create a thread in #devops asking to rotate the API keys\n```\n\n**Stopping a run**\n\n```\n# Click the Stop button that appears while the agent is running\n# Or use the slash command:\n/loop stop\n```\n\n### Parallel Work with `tk` Tickets\n\nLoop ships with a ticket-driven workflow that lets you split work across multiple parallel threads, each working in its own git worktree. A dispatcher automatically creates worker threads and chains merge tickets so branches are merged back into main in order.\n\nThe **Kanban panel** in the desktop app provides a visual interface for the same ticket system — see all tickets by status, create/edit/delete, and assign worktrees with one click. The CLI (`tk`) and Kanban UI share the same `.tickets/` directory, so changes from either side are reflected everywhere. See [Kanban docs](docs/kanban.md) for the full UI reference.\n\n#### How it works\n\n1. **You ask the bot** to break a task into work tickets:\n   ```\n   @LoopBot analyze the test files and create tk work tickets to reduce verbosity\n     in each test file. Tag them with \"work\". Don't start working on them yet.\n   ```\n   The agent creates tickets like:\n   ```\n   tk create \"Reduce db_test.go verbosity\" -d \"Extract helpers, table-drive tests...\" --tags work\n   tk create \"Reduce api_test.go verbosity\" -d \"Consolidate error scenarios...\" --tags work\n   ```\n\n2. **The heartbeat** (`tk-heartbeat` template) polls every 5 minutes. When it detects ready work tickets, it enables the dispatcher.\n\n3. **The dispatcher** (`tk-auto-worker` template) runs every minute when enabled:\n   - For each ready **work ticket**: creates a thread with a worker agent that checks out a git worktree (`tk-\u003cid\u003e` branch), implements the solution, commits, and closes the ticket — without merging into main.\n   - For each work ticket, also creates a **merge ticket** (tagged `merge`) chained in dependency order so merges happen sequentially.\n   - For each ready **merge ticket**: creates a thread with a worker that rebases the branch onto main and fast-forward merges it (`git rebase main \u0026\u0026 git merge --ff-only`), then cleans up the worktree and branch.\n\n4. **Work happens in parallel** — multiple worker threads run simultaneously in isolated worktrees. Merges happen one at a time in the correct order via the dependency chain.\n\n5. **When no tickets remain**, the heartbeat disables the dispatcher to save resources.\n\n#### Setup\n\nAdd both templates to your `~/.loop/config.json`:\n\n```jsonc\n{\n  \"task_templates\": [\n    {\n      \"name\": \"tk-auto-worker\",\n      \"description\": \"Dispatch ready tickets to worker threads\",\n      \"schedule\": \"* * * * *\",\n      \"type\": \"cron\",\n      \"prompt_path\": \"tk-auto-worker.md\"\n    },\n    {\n      \"name\": \"tk-heartbeat\",\n      \"description\": \"Enable/disable dispatcher based on ready tickets\",\n      \"schedule\": \"5m\",\n      \"type\": \"interval\",\n      \"prompt_path\": \"tk-heartbeat.md\",\n      \"auto_delete_sec\": 60\n    }\n  ]\n}\n```\n\nThe prompt files (`tk-auto-worker.md`, `tk-heartbeat.md`) are installed to `~/.loop/templates/` during `loop onboard:global`.\n\nThen add both templates to your channel:\n```\n/loop template add tk-heartbeat\n/loop template add tk-auto-worker\n```\n\nThe heartbeat starts polling immediately. The dispatcher stays disabled until work tickets appear.\n\n#### Example workflow\n\n```\n# 1. Ask the bot to plan and create work tickets\n@LoopBot look at all test files over 1000 lines, create a tk work ticket for\n  each one to reduce verbosity with table-driven tests. Tag them with \"work\".\n\n# 2. The bot creates tickets:\n#    tic-a1b2 [work] Reduce db_test.go verbosity\n#    tic-c3d4 [work] Reduce api_test.go verbosity\n#    tic-e5f6 [work] Reduce bot_test.go verbosity\n\n# 3. Heartbeat detects ready tickets → enables dispatcher\n# 4. Dispatcher creates worker threads + merge tickets:\n#    Thread \"tic-a1b2\" → worker creates worktree, implements, commits, closes\n#    Thread \"tic-c3d4\" → worker creates worktree, implements, commits, closes\n#    Thread \"tic-e5f6\" → worker creates worktree, implements, commits, closes\n#    (all three run in parallel)\n\n# 5. As work tickets close, merge tickets become ready (in order):\n#    Thread \"tic-m001\" → rebase tk-a1b2, merge --ff-only into main\n#    Thread \"tic-m002\" → rebase tk-c3d4, merge --ff-only into main (after m001)\n#    Thread \"tic-m003\" → rebase tk-e5f6, merge --ff-only into main (after m002)\n\n# 6. All done — heartbeat disables dispatcher\n```\n\n### Task Templates\n\nThe config.json file can include a `task_templates` array with reusable task patterns. Use `/loop template add \u003cname\u003e` in Discord to load a template as a scheduled task in the current channel. Templates are idempotent — adding the same template twice to a channel is a no-op.\n\nEach template requires exactly one of:\n- `prompt` — inline prompt text\n- `prompt_path` — path to a prompt file relative to the `templates/` directory (`~/.loop/templates/` for global, `.loop/templates/` for project)\n\nOptional: `auto_delete_sec` — when set (\u003e 0), the agent is instructed to prefix its response with `[EPHEMERAL]` if it has nothing meaningful to report. If the prefix is detected, the thread is renamed (💨 on Discord/Slack, `[ephemeral]` on local) and auto-deleted after the specified number of seconds. Non-ephemeral responses keep the thread permanently (0 = disabled, default). On local platform, recurring tasks reuse the same thread across executions.\n\nExample templates in `~/.loop/config.json`:\n\n```jsonc\n{\n  \"task_templates\": [\n    {\n      \"name\": \"tk-auto-worker\",\n      \"description\": \"Dispatch ready tickets to worker threads\",\n      \"schedule\": \"* * * * *\",\n      \"type\": \"cron\",\n      \"prompt_path\": \"tk-auto-worker.md\"  // loaded from ~/.loop/templates/tk-auto-worker.md\n    },\n    {\n      \"name\": \"tk-heartbeat\",\n      \"description\": \"Check for ready work tickets; enable/disable tk-auto-worker accordingly\",\n      \"schedule\": \"5m\",\n      \"type\": \"interval\",\n      \"prompt_path\": \"tk-heartbeat.md\",  // loaded from ~/.loop/templates/tk-heartbeat.md\n      \"auto_delete_sec\": 60  // auto-delete thread 1 min after execution (0 = disabled)\n    },\n    {\n      \"name\": \"daily-summary\",\n      \"description\": \"Generate a daily summary of completed tickets\",\n      \"schedule\": \"0 17 * * *\",\n      \"type\": \"cron\",\n      \"prompt\": \"Generate a summary of all tickets closed today using 'tk list --status=closed'. Include ticket IDs, titles, and brief descriptions of what was accomplished.\"\n    },\n    {\n      \"name\": \"dependency-audit\",\n      \"description\": \"Check for outdated or vulnerable dependencies\",\n      \"schedule\": \"0 8 * * 1\",\n      \"type\": \"cron\",\n      \"prompt_path\": \"dependency-audit.md\"  // loaded from ~/.loop/templates/dependency-audit.md\n    }\n  ]\n}\n```\n\nExample `~/.loop/templates/tk-auto-worker.md`:\n\n```markdown\nYou are a ticket dispatcher. Process ready tickets in two categories:\n\n## A) Work Tickets\n\nRun `tk ready -T work`. Find the merge chain tail via `tk list --status=open -T merge` — the last\nticket is the tail. For each ready work ticket (skip if already in `tk list --status=in_progress`):\n\n1. Create a merge ticket tagged `merge`:\n   `tk create \"merge-\u003cid\u003e: merge branch tk-\u003cid\u003e into main\" -d \"Merge worktree branch tk-\u003cid\u003e into main. Run: git checkout tk-\u003cid\u003e \u0026\u0026 git rebase main, resolve any conflicts, then git checkout main \u0026\u0026 git merge --ff-only tk-\u003cid\u003e, delete the worktree and branch, then tk close this ticket.\" --tags merge`\n2. Chain with `tk dep add \u003cmerge-id\u003e \u003cwork-id\u003e` and if a tail exists `tk dep add \u003cmerge-id\u003e \u003ctail-id\u003e`.\n   Update tail to this merge ticket.\n3. Create a thread via `create_thread` MCP tool with the work ticket ID as name and a message telling\n   the worker to:\n   a) `tk start \u003cid\u003e`\n   b) Create a git worktree on branch `tk-\u003cid\u003e`\n   c) Implement the solution in the worktree — do NOT create new work tickets (`tk create --tags work`)\n   d) Commit and `tk close \u003cid\u003e` — do NOT merge into main\n\n## B) Merge Tickets\n\nRun `tk ready -T merge`. For each ready merge ticket (skip if already in progress), create a thread\nvia `create_thread` MCP tool with the merge ticket ID as name and a message telling the worker to\nfollow the ticket description (`tk show \u003cid\u003e`) to merge the branch into main — do NOT create new work tickets (`tk create --tags work`).\n\n---\n\nIf no ready tickets exist in either category, do nothing — do NOT send any messages to this channel.\n```\n\n#### Project-Level Templates\n\nProject configs (`.loop/config.json`) can define their own `task_templates` that merge with global templates. Project templates override global ones by name, and new templates are appended.\n\n```jsonc\n// .loop/config.json\n{\n  \"task_templates\": [\n    {\n      \"name\": \"test-suite\",\n      \"description\": \"Run full test suite and report failures\",\n      \"schedule\": \"0 6 * * *\",\n      \"type\": \"cron\",\n      \"prompt_path\": \"test-suite.md\"  // loaded from .loop/templates/test-suite.md\n    }\n  ]\n}\n```\n\n### Prompt Shortcuts\n\nThe `prompt_shortcuts` array defines quick-access prompts that users can trigger by typing `#` in the chat input. Each shortcut has a name, optional description, and either an inline prompt or a path to a prompt file.\n\n```jsonc\n{\n  \"prompt_shortcuts\": [\n    {\n      \"name\": \"coverage\",\n      \"description\": \"Run coverage check\",\n      \"prompt\": \"Run make coverage-check and report results\"\n    },\n    {\n      \"name\": \"review\",\n      \"description\": \"Review uncommitted and branch changes\",\n      \"prompt_path\": \"review-code.md\"  // loaded from ~/.loop/shortcuts/review-code.md\n    }\n  ]\n}\n```\n\n| Field | Type | Description |\n|---|---|---|\n| `name` | `string` | Unique shortcut identifier. Shown in the `#` picker. |\n| `description` | `string` | Human-readable description shown below the name. |\n| `prompt` | `string` | Inline prompt text. Mutually exclusive with `prompt_path`. |\n| `prompt_path` | `string` | Path to a prompt file, resolved as `~/.loop/shortcuts/{prompt_path}` (global) or `.loop/shortcuts/{prompt_path}` (project). Mutually exclusive with `prompt`. |\n\nProject configs (`.loop/config.json`) can define their own `prompt_shortcuts` that merge with global shortcuts. Project shortcuts override global ones by name, and new shortcuts are appended.\n\nAgents can manage shortcuts via the `prompt_shortcut` MCP tool — list, add, update, or delete shortcuts in either global or project scope.\n\n### Bash Shortcuts\n\nThe `bash_shortcuts` array defines quick-access shell commands that users can trigger by typing `$` in the terminal shortcuts bar. Each shortcut has a name, optional description, and either an inline `command` or a `command_path` to a script file (resolved as `~/.loop/bash-shortcuts/{path}` globally, or `.loop/bash-shortcuts/{path}` per-project).\n\n```jsonc\n{\n  \"bash_shortcuts\": [\n    { \"name\": \"make lint\", \"description\": \"Run the linter\", \"command\": \"make lint\" },\n    { \"name\": \"tests\", \"description\": \"Run unit tests\", \"command_path\": \"run-tests.sh\" }\n  ]\n}\n```\n\nThe `$` picker mounts on **Docker Shell** and **Host Shell** panes (raw bash; sent with a trailing newline) and on **Docker Agent** panes (Claude TUI; sent as a bracketed paste + `\\r`). The `#` prompt-picker and the `$` bash-picker are mutually exclusive on a given pane. Agents can manage shortcuts via the `bash_shortcut` MCP tool.\n\n### Workflows\n\nWorkflows are declarative DAG-based pipelines of prompt and bash nodes. They provide repeatable, structured execution with parallel fan-out, dependency tracking, and real-time status events. Defined in the `workflows` array in config, using the same merge-by-name system as `task_templates`.\n\n```jsonc\n{\n  \"workflows\": [\n    {\n      \"name\": \"fix-issue\",\n      \"description\": \"Analyze a GitHub issue, implement, test, and create a PR\",\n      \"inputs\": {\n        \"issue_url\": { \"description\": \"GitHub issue URL\", \"required\": true }\n      },\n      \"nodes\": [\n        { \"id\": \"analyze\", \"type\": \"bash\", \"script\": \"gh issue view {{.Inputs.issue_url}} --json title,body,labels\" },\n        { \"id\": \"plan\", \"type\": \"prompt\", \"depends_on\": [\"analyze\"], \"prompt\": \"Create a plan:\\n\\n{{.NodeOutputs.analyze}}\" },\n        { \"id\": \"implement\", \"type\": \"prompt\", \"depends_on\": [\"plan\"], \"prompt\": \"Implement:\\n\\n{{.NodeOutputs.plan}}\" },\n        { \"id\": \"test\", \"type\": \"bash\", \"depends_on\": [\"implement\"], \"script\": \"make test 2\u003e\u00261\" },\n        { \"id\": \"pr\", \"type\": \"prompt\", \"depends_on\": [\"test\"], \"prompt\": \"Create a PR. Tests:\\n{{.NodeOutputs.test}}\" }\n      ]\n    }\n  ]\n}\n```\n\nNodes support four types: `prompt` (AI agent), `bash` (shell script), `loop` (iterative prompt), and `approval` (human gate). Independent nodes execute in parallel. Templates use Go `text/template` syntax with access to `{{.Inputs.name}}` and `{{.NodeOutputs.node_id}}`. Use `workflow_concurrency` to limit parallel runs and nodes. Both workflows and individual nodes support enforced `timeout` deadlines (Go duration strings like `\"30m\"` or `\"5m\"`).\n\nAgents can start workflows via the `run_workflow` MCP tool, and the Workflows panel in the desktop app provides an interactive DAG graph visualization for monitoring runs with per-node status, output, and real-time updates. Available as both a global overlay and an embedded per-channel split panel. See [Workflows docs](docs/workflows.md) for the full reference.\n\n## Desktop App\n\nLoop includes a cross-platform desktop app for macOS, Windows, and Linux, built with Electron + React. Download from [Releases](https://github.com/radutopala/loop/releases/latest) or build from source.\n\n### Features\n\n- **Chat** — send messages, stream agent responses in real-time, search messages (Cmd+K), copy-on-select, persistent drafts across channel switches, message history navigation (ArrowUp/ArrowDown), prompt shortcuts (`#` picker), clickable file links in message text and tool blocks (paths with optional `:line` suffix are auto-detected, validated against the working tree, and open the editor panel — placed opposite the chat horizontally if missing — scrolled to the target line). Pasting an image into the chat input (PNG/JPEG/GIF/WebP) saves it under `\u003cworkspace\u003e/.loop/pastes/` and inserts the absolute path at the caret, so the agent's `Read` tool can pick it up\n- **Terminal** — interactive xterm.js terminals with horizontal/vertical splits. Three panel types: **Docker Agent** launches Claude Code inside the project's docker container, **Docker Shell** opens a plain bash shell in the same container (no Claude), and **Host Shell** runs on the local machine. Docker Agent and Docker Shell share the container — files and processes are visible across panes. The Docker Agent split menu and empty-layout picker expose three session-handling variants — **Resume**, **Resume with fork**, **Fresh session** — chosen up front per pane. **Shift+Enter** inserts a newline (matching Claude `/terminal-setup`) instead of submitting. Each pane has a footer shortcuts bar: Docker Agent panes show a `#` picker for prompt shortcuts, Docker Shell and Host Shell panes show a `$` picker for bash shortcuts. Clicked URLs route via `shell.openExternal` and open only in the OS default browser, never inside Loop\n- **File editor** — CodeMirror-powered editor with syntax highlighting, markdown preview, in-file search, context menus, auto-save, and directory creation/deletion. VCS change markers highlight uncommitted edits versus git HEAD — a per-line gutter stripe (green added / blue modified / grey deletion triangle) plus a right-side overview ruler that maps all changes across the whole file and jumps to a line on click. Image files (PNG/JPEG/GIF/WebP) render inline as `\u003cimg\u003e` instead of the text editor, with cache-busting on agent edits so the picture refreshes automatically\n- **Git panel** — git changes with per-file addition/deletion stats, maximizable to full width, expandable context rows between hunks (GitLab-style \"load more\"), branch-to-branch diff mode for comparing any two branches, renamed file support with `{old =\u003e new}` notation, commit history view with branch selector and lazy pagination, worktrees tab for managing git worktrees (import, navigate, delete). On multi-root channels (primary `dir_path` + `extra_dirs`), the panel header surfaces a root selector dropdown listing each root's absolute path; switching root re-scopes the diff and commit views to that workspace via the `?root=N` query parameter. When the current branch has an open GitHub PR, the diff source defaults to the PR's base ref and a state-coloured PR link chip surfaces in the panel header (requires `gh` on the host; account selectable via `github.gh_user` config)\n- **Kanban panel** — visual ticket board with Open / In Progress / Closed columns backed by filesystem-based `tk` tickets (`.tickets/` directory). Create, edit, delete tickets with full metadata (priority, type, assignee, tags, dependencies, design notes, acceptance criteria). One-click \"Assign Worktree\" atomically claims a ticket, creates a git worktree, spawns a thread, and auto-starts an agent. Live updates via WebSocket. See [Kanban](docs/kanban.md)\n- **Workflows panel** — start, monitor, and manage DAG-based workflow runs. Available as both a global overlay panel (shows runs across **all** channels) and an embedded split panel scoped to a single channel. Two-pane layout with run list and interactive DAG graph visualization — nodes are rendered on an SVG canvas with a dot grid background, pan/zoom (scroll + Ctrl+Scroll), cursor-anchored zoom, minimap with draggable viewport, and zoom controls. Nodes show status (pending, running, success, failed, skipped, paused), type badges, retry counts, and elapsed time with connected dependency edges. `loop` nodes with a `body` expand into per-iteration synthetic nodes laid out in columns inside a dashed group container labeled `\u003cloop-id\u003e · N iters`, so each iteration's children are visible side-by-side. Click a node to expand its output in a 50/50 split below the graph. Approval widget for paused runs. Real-time updates via WebSocket events. See [Workflows](docs/workflows.md)\n- **Containers panel** — global view of all Docker containers (agent, shell, chrome) with real-time status lifecycle (running → stopped → pending-removal), type labels, scheduled removal countdown, and live updates via WebSocket events\n- **Memory panel** — browse and search semantic memory files\n- **Quality panel** — per-channel architectural-quality dashboard. Headline `quality_signal` (0–10000) with red/amber/green band, per-metric cards (Modularity, Cycles, Depth, Equality, Dead Code), `@visx/hierarchy` treemap (tile size = file LOC, color = per-file deficit), diagnostic popover on tile click, and failed-rule citation cards. Round indicator in the chat-bar toolbar shows the current band; click to add a Quality leaf to the channel's split-pane tree. Live-rescan opt-in via `quality.live_rescan = true` (agentgate `OnFileWrite` + `OnToolUse` feeds, debounced 250 ms). See [Quality](docs/quality.md)\n- **Custom layouts** — named split-pane workspaces with drag-to-resize, saved per channel. Create, rename, delete, and restore default layouts from the tab bar\n- **Islands layout** — panels float as rounded cards over a deep canvas background with gaps between them. Enable via `\"islands\": true` in the `desktop` config section (on by default)\n- **Multi-window** — open multiple windows (Cmd+N), each navigating independently\n- **Sidebar** — browse channels and threads, create new ones, batch-delete, see running status (green dot), and open directories directly from the sidebar. Status pills surface live conditions: `rev` when a review session is open, `ask` when an agent is parked on an `AskUserQuestion` card (sourced from `/api/review/sessions` and `/api/asks/pending`, rehydrated on WebSocket reconnect)\n- **Auto-update** — checks for new releases every 30 minutes, download and install with one click\n- **Deep links** — `loop://channel/\u003cid\u003e` opens the app directly to a channel\n- **Branch picker** — switch branches from the header bar, create worktree threads, import existing worktrees. Threads show branches only; parent channels show branches + worktrees in a 50/50 split. Double-click a branch name to copy it\n- **Browser** — live Chrome screencast via WebSocket, click/type/navigate directly in the browser pane. Two panel types: Docker Browser (headless container) and Host Browser (local Chrome via CDP), mutually exclusive per layout\n- **Playground** — live interactive sandbox where agents generate HTML/CSS/JS and it renders in a sandboxed iframe. Multiple named playgrounds with two scopes: global (`~/.loop/playground/`, shared across channels) and project (`.loop/playground/` in the channel's working directory). Multi-instance panels, hot-reloads on updates, console capture, import maps, multi-file support with relative imports. Agents use `playground` + `playground_file` MCP tools\n- **Settings** — schema-driven config form with typed controls (toggles, dropdowns, number inputs, password fields, arrays, key-value editors) plus a raw JSON editor, with Form/JSON toggle and unsaved changes confirmation. The **Workflows** and **Prompt Shortcuts** sections each gain a \"Restore built-ins\" bar that re-seeds any missing built-in entries (`review-loop`, `review-fix-loop`, `builtin code review`, `builtin simplify`) without overwriting user-edited ones\n- **Plan mode** — run agents in read-only preview mode via Claude Code's `EnterPlanMode` tool\n- **Agent activity** — see model info, thinking blocks, tool calls with their results, and completion summaries in the chat view. Thinking and tool events are persisted alongside messages and replayed in chain order on reload, so the timeline survives page refreshes\n- **Security gate approvals** — when an agent container trips a rule with `decision: approve`, an inline `ApprovalCard` appears in chat with three buttons (Allow once / Allow for session / Deny). The same card is also rendered as a centered, dimmed overlay inside Docker Agent terminal panels, so operators working in a layout without a Chat panel (e.g. Swarm) can approve without switching layouts. Resolves via WebSocket — same decision is also reflected in Discord/Slack cards on those surfaces\n- **Audit panel** — singleton panel showing the agent-gate JSONL files per channel. Left pane lists files newest-first (size, last-modified, delete); right pane opens a fresh `tail -f -n 100` xterm via docker exec against `/var/log/loop-gate/agentgate-\u003cdate\u003e.jsonl`. Same files exposed over HTTP at `GET/DELETE /api/channels/{id}/audit[/{date}]` for SIEM or tooling\n- **Message queue** — processing indicators and trigger quote showing which message is being handled, with timestamp. A collapsible popup above the input surfaces waiting messages and lets you remove them from the queue before the agent picks them up\n\n### Platforms\n\n| Platform | Format | Auto-update |\n|---|---|---|\n| macOS (Apple Silicon) | `.dmg` (arm64) | Yes |\n| macOS (Intel) | `.dmg` (x64) | Yes |\n| Windows (x64) | `.exe` (NSIS installer) | Yes |\n| Windows (ARM64) | `.exe` (NSIS installer) | Yes |\n| Linux (x64) | `.AppImage`, `.deb` | AppImage only |\n| Linux (ARM64) | `.AppImage`, `.deb` | AppImage only |\n\nRelease builds for macOS are signed with a Developer ID Application certificate and notarized by Apple.\n\n### Build from source\n\nRequires [Node.js 24+](https://nodejs.org/).\n\n```sh\n# Development (expects a running daemon on :8222 — `loop serve` first)\ncd app \u0026\u0026 npm install \u0026\u0026 npm run dev\n\n# Frontend unit tests (vitest)\nmake app-test\n\n# Build and install to /Applications (macOS)\nmake app-install\n```\n\nIn headless/Linux environments where Electron can't launch, `LOOP_NO_ELECTRON=1 npm run dev` serves the same renderer as a plain browser app on `:5173` (`make app-dev-docker` wraps this in Docker). See [CONTRIBUTING.md](CONTRIBUTING.md) for the full dev-setup guide.\n\n## REST API\n\n| Method | Endpoint | Description |\n|---|---|---|\n| `POST` | `/api/tasks` | Create a scheduled task |\n| `GET` | `/api/tasks?channel_id=\u003cid\u003e` | List tasks for a channel |\n| `GET` | `/api/tasks/{id}` | Get a single task by ID |\n| `PATCH` | `/api/tasks/{id}` | Update a task (enabled, schedule, type, prompt) |\n| `DELETE` | `/api/tasks/{id}` | Delete a task |\n| `POST` | `/api/tasks/{id}/run` | Run a task immediately (409 if already running) |\n| `GET` | `/api/tasks/{id}/runs` | List recent run logs for a task |\n| `GET` | `/api/channels?query=\u003cterm\u003e` | Search channels and threads (optional query filter) |\n| `POST` | `/api/channels` | Ensure/create a channel for a directory |\n| `POST` | `/api/channels/create` | Create a channel by name |\n| `POST` | `/api/channels/ensure-all` | Ensure channels exist for all configured directories |\n| `DELETE` | `/api/channels/{id}` | Delete a channel and its child threads |\n| `POST` | `/api/messages` | Send a message to a channel or thread |\n| `POST` | `/api/threads` | Create a thread in an existing channel |\n| `DELETE` | `/api/threads/{id}` | Delete a thread (cleans up worktree and branch if applicable) |\n| `GET` | `/api/channels/{id}/branches` | List branches and worktrees for a channel |\n| `POST` | `/api/channels/{id}/branches/switch` | Switch git branch |\n| `POST` | `/api/channels/{id}/branches/create` | Create and checkout a new branch |\n| `POST` | `/api/worktrees` | Create a git worktree as a new thread |\n| `POST` | `/api/worktrees/import` | Import an existing worktree as a thread |\n| `DELETE` | `/api/worktrees` | Remove a git worktree from disk and optionally delete its thread |\n| `GET` | `/api/channels/{id}/roots` | List all root directories (primary + extra from project config) |\n| `POST` | `/api/channels/{id}/files/exists` | Batched existence check for path candidates (used by the chat file-link UX); returns `{exists, root_index, rel_path}` per path |\n| `GET` | `/api/channels/{id}/diff` | Get git diff (working changes, or `?source=X\u0026target=Y` for branch diff; `?root=N` selects a non-primary workspace root) |\n| `GET` | `/api/channels/{id}/pr` | Look up the open GitHub PR for the channel's current branch (shells out to `gh`) |\n| `GET` | `/api/channels/{id}/messages` | List messages with cursor-based pagination |\n| `GET` | `/api/channels/{id}/timeline` | List interleaved messages, thinking blocks, and tool events with cursor-based pagination |\n| `POST` | `/api/commands` | Send a slash command to a channel |\n| `POST` | `/api/memory/search` | Semantic search across memory files |\n| `POST` | `/api/memory/index` | Re-index memory files |\n| `POST` | `/api/channels/{id}/quality/scan` | Kick a quality scan asynchronously (returns `202 Accepted`; report ships via the `quality.scanned` event) |\n| `GET` | `/api/channels/{id}/quality/snapshot` | Fetch the persisted quality snapshot (404 when none exists yet) |\n| `GET` | `/api/channels/{id}/quality/complexity` | Per-function complexity hotspots (cyclomatic, cognitive, max nesting, params, LOC) with `?limit=` and `?offset=` paging |\n| `GET` | `/api/channels/{id}/quality/clones` | Clone clusters (SimHash + Hamming distance) with member functions, total LOC, and `?limit=` / `?offset=` paging |\n| `GET` | `/api/readme` | Get the Loop README documentation |\n| `PUT` | `/api/playground?name=...` | Create/update a playground (html, title, description) |\n| `GET` | `/api/playground?name=...` | Get playground metadata |\n| `DELETE` | `/api/playground?name=...` | Delete entire playground |\n| `GET` | `/api/playground/items` | List all playgrounds |\n| `PUT` | `/api/playground/file?name=...\u0026path=...` | Write a file |\n| `GET` | `/api/playground/file?name=...\u0026path=...` | Read a file |\n| `DELETE` | `/api/playground/file?name=...\u0026path=...` | Delete a file |\n| `GET` | `/api/playground/files?name=...` | List files in a playground |\n| `POST` | `/api/browser/action` | Browser automation (navigate, tabs, screenshot, input, etc.) |\n| `POST` | `/api/browser/mode` | Switch browser mode (docker/host) |\n| `GET` | `/api/tickets` | List tickets for a project directory (filter by status, tag, assignee, type) |\n| `POST` | `/api/tickets` | Create a ticket |\n| `GET` | `/api/tickets/{id}` | Get a single ticket by ID |\n| `PATCH` | `/api/tickets/{id}` | Update ticket fields (status, title, description, deps, etc.) |\n| `DELETE` | `/api/tickets/{id}` | Delete a ticket |\n| `POST` | `/api/tickets/{id}/assign` | Assign a worktree to a ticket (claim, create worktree, start agent) |\n| `GET` | `/api/workflows` | List workflow definitions from merged config |\n| `POST` | `/api/workflows/runs` | Start a new workflow run |\n| `GET` | `/api/workflows/runs` | List workflow runs (optional `channel_id`, `limit`, `offset` for pagination) |\n| `GET` | `/api/workflows/runs/{id}` | Get run detail with node statuses |\n| `POST` | `/api/workflows/runs/{id}/resume` | Resume a paused workflow (body: `{\"response\": \"...\"}`) |\n| `POST` | `/api/workflows/runs/{id}/cancel` | Cancel a running workflow |\n| `DELETE` | `/api/workflows/runs/{id}` | Permanently delete a workflow run from the database |\n| `POST` | `/api/workflows/runs/{id}/retry` | Retry a completed/failed workflow run (creates a new run) |\n| `GET` | `/api/gate/approvals` | Snapshot of every in-flight gate approval across all live containers; the renderer hits this on every WebSocket reconnect to reconcile its UI card map and the electron dock-bouncer against the source of truth |\n| `POST` | `/api/gate/approvals/{id}` | Resolve a pending security-gate approval (body: `{\"decision\": \"once\"\\|\"session\"\\|\"deny\", \"author_id\"?: \"...\"}`) |\n| `POST` | `/api/gate/container-approval` | In-container callback (used by `loop dockerproxy` and `loop syscallwrap`) — authenticated via per-container `Authorization: Bearer \u003cLOOP_GATE_TOKEN\u003e`; blocks until the user clicks in chat |\n| `GET` | `/api/channels/{id}/audit` | List agent-gate audit files for a channel (paginated, newest-first) |\n| `DELETE` | `/api/channels/{id}/audit/{date}` | Delete one audit file |\n| `GET` | `/api/shortcuts` | List resolved prompt shortcuts (optional `channel_id` for project merge) |\n| `POST` | `/api/shortcuts` | Add, update, or delete a prompt shortcut (global or project scope) |\n| `GET` | `/api/config/schema` | JSON Schema for all config fields |\n| `GET` | `/api/config` | Get global config (parsed + raw HJSON) |\n| `PUT` | `/api/config` | Save global config |\n| `GET` | `/api/config/project?channel_id=\u003cid\u003e` | Get project config for a channel |\n| `PUT` | `/api/config/project?channel_id=\u003cid\u003e` | Save project config for a channel |\n| `GET` | `/api/ws` | WebSocket for real-time event streaming |\n| `GET` | `/api/ws/terminal` | WebSocket for interactive terminal sessions |\n| `GET` | `/api/ws/browser` | WebSocket for browser screencast frames and input |\n\n## MCP Tools\n\n| Tool | Description |\n|---|---|\n| `schedule_task` | Create a scheduled task (cron/interval/once) |\n| `list_tasks` | List all scheduled tasks for this channel |\n| `show_task` | Show details of a scheduled task by ID |\n| `cancel_task` | Cancel a scheduled task by ID |\n| `toggle_task` | Enable or disable a scheduled task by ID |\n| `edit_task` | Edit a task's schedule, type, and/or prompt |\n| `create_channel` | Create a new channel by name |\n| `create_thread` | Create a new thread; optional `message` triggers a runner immediately |\n| `create_worktree_thread` | Create a thread backed by a fresh git worktree; `branch` is the base to fork from (a new `worktree/\u003cname\u003e` branch is checked out off it); optional `message` triggers a runner immediately |\n| `rename_thread` | Rename a thread or channel's display name (sessions and directory preserved) |\n| `rename_worktree_thread` | Rename a worktree thread — renames its directory and branch, relocates the Claude session store (sessions preserved); rejected while a run is active |\n| `delete_thread` | Delete a thread by ID (cleans up worktree and branch if applicable) |\n| `search_channels` | Search for channels and threads by name |\n| `send_message` | Send a message to a channel or thread (`channel_id` optional — defaults to the current channel) |\n| `queue_message` | Queue a follow-up prompt for yourself in the current channel/thread/worktree; `interrupt=true` cancels the active run and jumps the queue |\n| `search_memory` | Semantic search across memory files (ranked by similarity) |\n| `index_memory` | Force re-index all memory files |\n| `quality_scan` | Trigger an architectural-quality scan for the current channel (status hint returns immediately; report ships via the `quality.scanned` event) |\n| `quality_snapshot` | Read the persisted quality snapshot (current branch first, then most recent) |\n| `quality_complexity` | Per-function complexity hotspots (cyclomatic, cognitive, max nesting, params, LOC), worst-first, with offset/limit paging |\n| `quality_clones` | Clone clusters from the cached graph (SimHash near-duplicate detection), with offset/limit paging |\n| `get_readme` | Get the full Loop README documentation |\n| `playground` | Manage playgrounds (create/update/delete) |\n| `playground_file` | Manage files within a playground (create/update/read/delete/list) |\n| `prompt_shortcut` | Manage prompt shortcuts (list, add, update, delete) in global or project scope |\n| `bash_shortcut` | Manage bash shortcuts (list, add, update, delete) in global or project scope |\n| | **Workflows** |\n| `run_workflow` | Start a workflow run by name with optional inputs |\n| `get_workflow_run` | Get run status and node outputs |\n| `list_workflows` | List available workflow definitions |\n| `list_workflow_runs` | List recent workflow runs |\n| `cancel_workflow_run` | Cancel a running workflow |\n| `resume_workflow_run` | Resume a paused workflow with an optional response |\n| | **Browser Automation** |\n| `navigate` | Navigate the browser to a URL |\n| `read_page` | Get the accessibility tree of interactive elements |\n| `computer` | Perform click, type, key, scroll, move, screenshot, drag actions |\n| `screenshot` | Take a screenshot of the current page |\n| `find` | Find interactive elements by natural language query |\n| `form_input` | Fill in a form field (click, clear, type) |\n| `evaluate` | Evaluate JavaScript in the page context |\n| `get_page_text` | Get all text content from the page |\n| `read_console_messages` | Read captured browser console messages |\n| `read_network_requests` | Read captured network requests |\n| `list_tabs` | List all open browser tabs |\n| `new_tab` | Open a new browser tab |\n| `switch_tab` | Switch to a browser tab by target ID |\n| `close_tab` | Close a browser tab |\n| `resize_window` | Resize the browser viewport |\n\n## Development\n\nRequires [Go 1.26+](https://go.dev/dl/).\n\n```sh\nmake build            # Build the loop binary\nmake install          # Install to $GOPATH/bin\nmake docker-build     # Build the Docker agent image (from local source)\nmake restart          # Reinstall + restart daemon\nmake test             # Run tests\nmake lint             # Run linter\nmake coverage-check   # Enforce 100% test coverage\nmake coverage         # Generate HTML coverage report\nmake app-test         # Run frontend unit tests (vitest)\nmake app-install      # Build Electron app and copy to /Applications\nmake app-dev-docker   # Run Vite dev server in Docker (browser-only, no Electron)\nmake clean            # Remove build artifacts\n```\n\n### Integration Tests\n\nIntegration tests run against the real platform APIs to verify bot behavior end-to-end. Both Discord and Slack suites are available — each creates temporary channels, runs all tests, and cleans up on teardown.\n\n#### Slack\n\nThe Slack integration tests run against the real Slack API using Socket Mode. They require a dedicated Slack app with bot, app-level, and user tokens.\n\n**Setup:**\n\n1. Create a Slack app (or reuse the one from your main config) with these additional **User Token Scopes**: `channels:write`, `channels:read`, `chat:write`, `reactions:read`, `im:write`\n2. Add the following to `~/.loop/config.integration.json`:\n\n```json\n{\n  \"slack_bot_token\": \"xoxb-...\",\n  \"slack_app_token\": \"xapp-...\",\n  \"slack_user_token\": \"xoxp-...\"\n}\n```\n\nAlternatively, set environment variables: `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `SLACK_USER_TOKEN`.\n\nThe user token is optional — tests requiring it (e.g. DM events) will be skipped if not provided.\n\n#### Discord\n\nThe Discord integration tests run against the real Discord API using a bot token. They require a Discord bot with appropriate permissions in a test guild (server).\n\n**Setup:**\n\n1. Use an existing Discord bot or create one with the required permissions (View Channels, Send Messages, Manage Channels, Manage Threads, Read Message History, Send/Create Threads)\n2. Add the following to `~/.loop/config.integration.json`:\n\n```json\n{\n  \"discord_token\": \"MTA...\",\n  \"discord_app_id\": \"...\",\n  \"discord_guild_id\": \"...\"\n}\n```\n\nAlternatively, set environment variables: `DISCORD_BOT_TOKEN`, `DISCORD_APP_ID`, `DISCORD_GUILD_ID`.\n\n#### Running\n\n```sh\nmake test-integration\n```\n\nBoth suites create temporary channels, run all tests, and clean up on teardown. Tests are skipped automatically when the required credentials are not configured.\n\n## Documentation\n\nFull documentation is published at **\u003chttps://radutopala.github.io/loop/\u003e** (source in [docs/](docs/README.md)). For common issues such as LaunchAgents permissions or corporate proxy TLS errors during Docker builds, see the [Troubleshooting Guide](https://radutopala.github.io/loop/troubleshooting/).\n\n## License\n\nThis project is licensed under the [Apache License 2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fradutopala%2Floop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fradutopala%2Floop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fradutopala%2Floop/lists"}