Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
198df1d
Add Claude Code as an agent.
imsobear Oct 5, 2026
98b8604
Reload the dev runner when its code changes.
imsobear Oct 5, 2026
2e7eafe
Clear the dev server warnings.
imsobear Oct 5, 2026
ee63b01
Refuse server function calls from other sites.
imsobear Oct 5, 2026
9fdd0ad
Say plainly that GitHub is connected as the team's bot account.
imsobear Oct 5, 2026
af8009e
Trim the workflows to the team's jobs, and call GitHub a shared account.
imsobear Oct 5, 2026
4178bb3
Show the trimmed workflow list on the site.
imsobear Oct 5, 2026
822262f
Rework the loop page: a short form in three sections, and Tasks and S…
imsobear Oct 5, 2026
4b89b74
Turn Inbox, Loops, and New loop into tight lists with one line each.
imsobear Oct 5, 2026
f87958b
Bring Connectors, Agents, and Runners into the same compact rows.
imsobear Oct 5, 2026
65fb58f
Name a Slack connection by its bot, and run waiting items one at a time.
imsobear Oct 5, 2026
8547efc
Add custom loops, split Send to again, and call keeping an answer Inb…
imsobear Oct 5, 2026
21d35db
Test that a Slack connection is named by its bot.
imsobear Oct 5, 2026
c561a0d
List the loops that use a connector on its page.
imsobear Oct 5, 2026
785a8f4
Move asking an agent into a dialog behind a button on Inbox.
imsobear Oct 5, 2026
73f8278
Show each connector and agent by its own logo, in colour.
imsobear Oct 5, 2026
6d3b686
Let a loop send its answer as an email through Gmail.
imsobear Oct 5, 2026
80aefdc
Fold items a loop did not run into one quiet line.
imsobear Oct 5, 2026
045675c
Stop hydration warnings from folds and relative times.
imsobear Oct 5, 2026
3015e58
Give connectors triggers of their own, and let Gmail start a loop and…
imsobear Oct 5, 2026
e2e4d1e
Show the install line with the runner join command.
imsobear Oct 5, 2026
c09fd42
Describe triggers, workflows, and actions, and use the short GitHub n…
imsobear Oct 5, 2026
ce3a0bd
Mark an inbox prompt done when the agent answers.
imsobear Oct 5, 2026
b09e300
Make a loop's folder relative to the runner's home, keep it with the …
imsobear Oct 5, 2026
5177bd1
Put Settings first on a loop's page, and open there.
imsobear Oct 5, 2026
0ec1926
Bring the docs up to custom loops, Gmail, and folders relative to eac…
imsobear Oct 5, 2026
7edb401
Show Claude Code, custom loops, and Gmail on the site.
imsobear Oct 5, 2026
8427926
Run typecheck, tests, and both builds on every PR and push to main.
imsobear Oct 5, 2026
de64a69
Release from a clean, pushed main with one command, and tag every ver…
imsobear Oct 5, 2026
1a2cbd4
Have runners say their version, and keep folder jobs off runners too …
imsobear Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

# Every pull request, and main after a merge: the same checks a release runs,
# so nothing reaches npm that has not passed here first.
on:
pull_request:
push:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

# The version comes from packageManager in package.json.
- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm typecheck

- name: Test
run: pnpm test

- name: Build the CLI
run: pnpm build

- name: Build the site
run: pnpm site:build
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Loopable is your team’s own agent. It runs on a machine the team owns, and the team decides what it can do.

Work comes in from Slack, Lark, GitHub, or a schedule. A loop says what to do. Codex or Cursor Agent does it on a runner, and the answer is written back where it was asked. Every run is recorded. The story is in `docs/narrative.md`.
Work comes in from Slack, Lark, GitHub, or a schedule. A loop says what to do. Codex, Claude Code, or Cursor Agent does it on a runner, and the answer is written back where it was asked. Every run is recorded. The story is in `docs/narrative.md`.

## Stack

Expand Down
38 changes: 25 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ A lot of team work waits on one person: a review request, a question in #oncall,
Loopable is a shared agent for small and mid-size teams.

- **Shared.** Work comes in from Slack, Lark, GitHub, or a schedule. A loop says what to do. Nobody has to be there. Every run is recorded in the inbox.
- **Yours.** Codex or Cursor Agent runs on a machine the team owns. It reaches what that machine reaches, uses the MCP servers and skills you install, and keeps data in-house.
- **Yours.** Codex, Claude Code, or Cursor Agent runs on a machine the team owns. It reaches what that machine reaches, uses the MCP servers and skills you install, and keeps data in-house.

The story is in [docs/narrative.md](docs/narrative.md). The words for the parts — loop, workflow, runner, and the rest — are in [docs/concepts.md](docs/concepts.md).

## Loops you can run today

| Comes in from | Loop | Writes back |
| --- | --- | --- |
| GitHub | Review pull requests I am asked to review | A review, as a comment |
| GitHub | Plan issues assigned to me | A comment with a plan |
| GitHub | Implement issues assigned to me | A draft pull request |
| Slack bot | Do what I ask the bot (@mention or DM) | A reply in the thread |
| Feishu / Lark bot | Do what I ask the bot (@mention in a group) | A reply in the thread |
| Schedule | Do something on a schedule | The log, or a Slack or Lark channel |
| Gmail | Tell me about new mail | A short note wherever you look |
| WeChat | Do what I ask the bot | A reply in the chat |
| Any trigger | Custom loop: pick what starts it, write your own prompt | Anywhere below, or Inbox only |
| Schedule | Do something on a schedule | Inbox, or a Slack, Lark, or GitHub thread |
| GitHub | Review pull requests | A review, as a comment |
| GitHub | Reply to issues | A comment on the issue |
| Slack bot | Do what I ask the Slack bot (@mention or DM) | A reply in the thread |
| Feishu / Lark bot | Do what I ask the Feishu / Lark bot (@mention in a group) | A reply in the thread |
| WeChat | Do what I ask the WeChat bot | A reply in the chat |
| Gmail | Custom loop: “An email arrives” | Anywhere, such as an email back to the same inbox |

A loop’s answer can go to any connector that accepts it, so a scheduled check can post to #oncall.
A loop’s answer can go to any connector that accepts it, so a scheduled check can post to #oncall. To ask an agent something once, use **Ask an agent** in the inbox.

## Architecture

Expand All @@ -34,7 +34,7 @@ Three processes. The agent always runs on a Runner, never on the App or Dispatch
<img src="docs/architecture.svg" alt="Slack bot, GitHub, and Lark bot into Loopable. A runner pulls the job. Loopable writes back to the same three." width="920" />
</p>

Slack bot, GitHub, and Lark bot send work in. Loopable watches, matches a loop, and queues the job. A Runner pulls it, runs Codex or Cursor Agent, and Loopable writes the result back.
Slack bot, GitHub, and Lark bot send work in. Loopable watches, matches a loop, and queues the job. A Runner pulls it, runs Codex, Claude Code, or Cursor Agent, and Loopable writes the result back.

```text
Loopable ──HTTP pull── Runner ── Agent CLI
Expand All @@ -53,16 +53,17 @@ App and Dispatcher share one database and stay on the same host. Only one Dispat

The usual setup is one computer that stays on — a Mac in the office is enough. App, Dispatcher, and a Runner all live there. It looks like a spare machine on a desk. For a small team, that is the product.

You need Node 22+ and an agent CLI signed in on that machine (Codex or Cursor Agent).
You need Node 22+ and an agent CLI signed in on that machine (Codex, Claude Code, or Cursor Agent).

```bash
npm install -g loopable-cli
loopable start # App + Dispatcher, bound on all interfaces
```

On the same computer, open `http://127.0.0.1:4321`. Connect GitHub from that address. Write a loop. On Runners, copy the join command and start a runner (this machine is fine):
On the same computer, open `http://127.0.0.1:4321`. Connect GitHub from that address, signed in as the account your team will send work to (see [GitHub: a shared account](docs/connectors.md#github-a-shared-account)). Write a loop. On Runners, copy the join command and start a runner (this machine is fine):

```bash
npm install -g loopable-cli # on another machine; already done here
loopable runner --url http://<LAN-IP>:4321 --token <from Runners>
```

Expand All @@ -89,6 +90,17 @@ pnpm db:studio

`pnpm runner` is only for a second machine while `pnpm dev` is already running.

### Release

CI runs typecheck, tests, and both builds on every pull request and on `main`. To publish `loopable-cli`, merge to `main`, then from a clean, pushed `main`:

```bash
pnpm release minor --dry-run # every check, nothing changed
pnpm release minor # or patch, major, or an exact x.y.z
```

It checks git and npm, runs typecheck, tests, and build, then bumps the version, commits, tags `vX.Y.Z`, publishes, and pushes. Runners on other machines need `npm install -g loopable-cli` too. The site deploys on its own with `pnpm site:deploy`.

The public site is `site/` (static HTML, Vite). `pnpm site:dev` to preview. `pnpm site:build` writes `site/dist` for a Cloudflare Worker with assets only. `pnpm site:deploy` when you are ready to publish it.

| Path | Contents |
Expand Down
26 changes: 17 additions & 9 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,35 @@ There is no second path where “the local dispatcher runs the agent.” The age

**Connector.** A kind of service: GitHub, Slack bot, Feishu / Lark bot, Gmail, WeChat, the clock. It declares what it can watch and what it can write. It is not an account.

**Connection.** One credential of a connector — a GitHub login, a Slack bot, a Feishu bot, a Gmail inbox. Credentials live in Loopable. Agents never see them.
**Connection.** One credential of a connector — the team's shared GitHub account, a Slack bot, a Feishu bot, a Gmail inbox. Credentials live in Loopable. Agents never see them.

**Workflow.** A whole job a connector already knows how to do, named the way a person would name it: “Review pull requests I am asked to review.” It owns what to watch for, what to ask the agent, and where the answer is written. A loop is one instance of a workflow with its knobs set.
A connector registers three lists:

A workflow is not a loop template. Creating a loop copies the prompt onto the loop, and the loop owns that copy from then on. The loop still points at the workflow for the rest: the query, how the answer is parsed, whether the agent gets a checkout. If a connector stops offering a workflow, existing loops remain readable and deletable but not runnable.
- **Trigger** (“Starts when” in the UI). Something it can watch for: “A GitHub review is requested”, “An email arrives”. It owns the query, where the agent runs, and how the answer is read.
- **Workflow.** A ready-made job on one trigger, named the way a person would name it: “Review pull requests”. It adds a prompt and where the answer goes by default.
- **Action** (“Send to” in the UI). Somewhere an answer can go: “Submit a review”, “Reply in Slack”, “Email this account”.

A loop is a workflow with its knobs set, or, for a custom loop, a trigger with your own prompt. Either way it can send its answer to any connector's action.

A workflow is not a loop template. Creating a loop copies the prompt onto the loop, and the loop owns that copy from then on. The loop still points at the trigger for the rest: the query, how the answer is parsed, whether the agent gets a checkout. If a connector stops offering a trigger, existing loops remain readable and deletable but not runnable.

The UI does not need to say “workflow.” New loop is choosing a job by the name the connector gave it. In code and in this document the word is workflow, and the column is `workflowId`.

**Loop.** One instance of a workflow: your repositories, your notes, which agent. This is what people edit. Loops run by themselves. A loop names an agent, not a runner. The dispatcher picks an online runner that has that agent signed in. If none does, the loop is saved anyway and the page says it cannot run yet.
**Loop.** One instance of a workflow, or a custom loop: a trigger with your own prompt. It holds your settings, which agent, the folder it works in, and where the answer goes. This is what people edit. Loops run by themselves. A loop names an agent, not a runner. The dispatcher picks an online runner that has that agent signed in. If none does, the loop is saved anyway and the page says it cannot run yet.

**Signal.** One thing a loop noticed. The connector’s key on a signal has to change exactly when there is something new to do. Same signal does not run twice.

The first look never acts. Whatever is already waiting when a loop is created is that loop’s backlog, recorded rather than run.

**Task.** One run of one loop against one signal: fetch the context, let the agent work, write the result back. The inbox is the list of tasks. A task is kept whether it wrote anything or not.
**Task.** One run of one loop against one signal: fetch the context, let the agent work, write the result back. A prompt asked from the inbox with “Ask an agent” is a task with no loop, and nothing to write back. The inbox is the list of tasks. A task is kept whether it wrote anything or not.

**Folder.** Loops that read your code (chat bots and schedules) work in a folder, given relative to the runner's home, such as `code/web`. Each runner looks for it under its own home, so any runner with that checkout can take the job.

## What does the work

**Agent.** A coding CLI Loopable knows how to invoke: Codex, Cursor Agent. The list is a catalog in the repo, not discovered from the network. Loopable stores only the choices a person makes: which agent is the default, permission mode, model, timeout. Whether one is installed lives on each runner’s inventory.
**Agent.** A coding CLI Loopable knows how to invoke: Codex, Claude Code, Cursor Agent. The list is a catalog in the repo, not discovered from the network. Loopable stores only the choices a person makes: which agent is the default, permission mode, model, timeout. Whether one is installed lives on each runner’s inventory.

**Agent login.** That CLI’s own login on the runner’s host. It is not a Connection. Connection is GitHub, a Slack bot, a Feishu bot, Gmail, or WeChat. Agent login is Codex or Cursor Agent. They live in different places and must not be merged into one “account.”
**Agent login.** That CLI’s own login on the runner’s host. It is not a Connection. Connection is GitHub, a Slack bot, a Feishu bot, Gmail, or WeChat. Agent login is Codex, Claude Code, or Cursor Agent. They live in different places and must not be merged into one “account.”

**Runner.** A process that can run agents. It reports an inventory: which agents are installed and signed in. It heartbeats, pulls a job, runs the agent, and returns output and logs. It does not poll connectors, does not hold Connections, and does not write back to GitHub or Slack. The command is `loopable runner`. The join token lives on the Runners page.

Expand Down Expand Up @@ -68,11 +76,11 @@ The path does not change with how many runners you have. One runner on the same

| | Connection | Runtime auth | Agent login |
| --- | --- | --- | --- |
| What | GitHub, Slack bot, Feishu / Lark bot, Gmail, WeChat | git / `gh` on the runner | Codex, Cursor Agent |
| What | GitHub, Slack bot, Feishu / Lark bot, Gmail, WeChat | git / `gh` on the runner | Codex, Claude Code, Cursor Agent |
| Where | Loopable’s secret store | That host’s home directory | That host’s home directory |
| Who uses it | App and Dispatcher, to read signals and write back | The agent, via git on that host | Only the agent process |

They stay apart even when two of them are GitHub. The Connection is the bot that watches and writes through the API. Runtime auth is git on that host, used by the agent when the prompt asks it to clone. Loopable does not copy the Connection token onto a runner or into an agent.
They stay apart even when two of them are GitHub. The Connection is the team's shared account, which watches and writes through the API. Runtime auth is git on that host, used by the agent when the prompt asks it to clone. Loopable does not copy the Connection token onto a runner or into an agent.

The agent’s environment never contains Loopable’s service credentials. When a job needs a repository, the prompt tells the agent to clone it. That uses the machine’s git, not the Connection.

Expand Down
28 changes: 27 additions & 1 deletion docs/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ Each connector is a folder under `src/connectors/`. It exports two halves, and t

The pages render entirely from manifests. Adding a connector means adding a folder and one line in `src/connectors/manifests.ts` and `src/connectors/runtimes.ts`. No page changes. The contract lives in `src/connectors/types.ts`.

## GitHub: a shared account

Connect GitHub as the account the team sends work to. On GitHub, that account is Loopable:

- **People send it work the normal way.** Request a review from it, or assign it an issue. The workflows watch `review-requested:@me` and `assignee:@me`, and `@me` is the connected account.
- **What it writes appears under its name.** Reviews and comments come from that account.
- **Its access is what that account can reach.** OAuth asks for the `repo` scope, but the account only reaches repositories it has access to.

Which account to use:

- **An account made for this**, such as `acme-loopable`, is the cleanest. Work sent to it is clearly for Loopable, what it writes is clearly from Loopable, and it does not stop working when someone leaves. Create it with an email address the team controls, keep its password and 2FA in the team's password manager, and add it to the repositories Loopable should work in. On paid org plans it takes a seat.
- **Your own account** works too, if you are happy to share it. Then every review requested from you and every issue assigned to you is picked up, and Loopable answers as you.

To connect, sign in to GitHub as that account, then click Connect in Loopable. GitHub authorizes whichever account the browser is signed in as, so a private window that holds only that session is the easiest way. If jobs clone private repositories, sign the runner's git in as an account that can read them.

A GitHub App was considered and set aside for now. An App cannot be requested as a reviewer or assigned an issue, so the workflows would need labels or mentions instead. Its installation tokens also expire after an hour, which does not suit the runner's git.

## GitHub OAuth

End users never register an OAuth app. They click Connect. Loopable ships one GitHub app (and one Google app) whose callback is loopback:
Expand All @@ -37,7 +54,16 @@ WeChat is reached through Tencent's iLink bot API. Loopable speaks it directly r

Connecting means scanning. The connector hands over what the code should contain and a way to ask how the scan is going; the page renders it and asks every couple of seconds. A code lasts about two minutes. The token never reaches the browser: a confirmed scan is saved server-side.

There are no WeChat workflows yet. Binding an account is worth having on its own.
A loop fires when someone messages the bot, and the answer goes back to whoever asked. WeChat only delivers a reply into a conversation someone started, so it is a place to answer, not a place to send news to.

## Gmail

Gmail is connected by signing in with Google, from `http://127.0.0.1:4321` on the Loopable host.

- **Starts when an email arrives.** A custom loop on “An email arrives” checks the inbox every 30 minutes. It can be narrowed with Gmail search terms, skip mail the account sent, and skip mail that needs nothing, after one quick look at what arrived.
- **Email this account.** As an answer, Gmail sends to the connected account itself, the way a bot answers whoever asked. When the run started from an email in the same inbox, the answer joins that thread.

Gmail asks for `gmail.readonly` and `gmail.send`. A connection made before sending was offered has to be reconnected to send.

## Slack bot

Expand Down
2 changes: 1 addition & 1 deletion docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ loopable runner --url http://192.168.1.10:4321 --token <from Runners>

Open the UI as `http://192.168.1.10:4321/?token=<LOOPABLE_APP_TOKEN>` to copy the join command. Connect GitHub or Gmail from `http://127.0.0.1:4321` on this laptop. A Slack bot or Feishu / Lark bot is a pasted app credential, so it can be added from the LAN UI; it is not a Slack or Feishu login.

Each runner needs the agent CLI signed in, and git / `gh` if jobs clone. Google will not accept a private IP as an OAuth redirect, so Gmail login from another machine on the LAN will not work. GitHub is the same unless you click Connect on this laptop.
Each runner needs the agent CLI signed in, and git / `gh` if jobs clone. A loop that works in a folder names it relative to the runner's home, such as `code/web`, so check that folder out under the same path on every runner that should take those jobs. Sign git in as an account that can read the repositories jobs clone. Google will not accept a private IP as an OAuth redirect, so Gmail login from another machine on the LAN will not work. GitHub is the same unless you click Connect on this laptop.
Loading
Loading