@loompad/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +211 -0
- package/CHANGELOG.md +470 -0
- package/LICENSE +21 -0
- package/README.md +630 -0
- package/dist/adapters/base.d.ts +71 -0
- package/dist/adapters/base.js +187 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/bridges/antigravity.d.ts +53 -0
- package/dist/adapters/bridges/antigravity.js +106 -0
- package/dist/adapters/bridges/antigravity.js.map +1 -0
- package/dist/adapters/bridges/cdp.d.ts +82 -0
- package/dist/adapters/bridges/cdp.js +222 -0
- package/dist/adapters/bridges/cdp.js.map +1 -0
- package/dist/adapters/bridges/gui-chat.d.ts +113 -0
- package/dist/adapters/bridges/gui-chat.js +498 -0
- package/dist/adapters/bridges/gui-chat.js.map +1 -0
- package/dist/adapters/bridges/kiro.d.ts +19 -0
- package/dist/adapters/bridges/kiro.js +22 -0
- package/dist/adapters/bridges/kiro.js.map +1 -0
- package/dist/adapters/bridges/profiles.d.ts +82 -0
- package/dist/adapters/bridges/profiles.js +125 -0
- package/dist/adapters/bridges/profiles.js.map +1 -0
- package/dist/adapters/claude-code.d.ts +26 -0
- package/dist/adapters/claude-code.js +209 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +51 -0
- package/dist/adapters/codex.js +291 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/echo.d.ts +20 -0
- package/dist/adapters/echo.js +79 -0
- package/dist/adapters/echo.js.map +1 -0
- package/dist/adapters/grok.d.ts +61 -0
- package/dist/adapters/grok.js +217 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +33 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.d.ts +64 -0
- package/dist/adapters/opencode.js +426 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/cli/common.d.ts +11 -0
- package/dist/cli/common.js +40 -0
- package/dist/cli/common.js.map +1 -0
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +181 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +641 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui-model.d.ts +73 -0
- package/dist/cli/tui-model.js +312 -0
- package/dist/cli/tui-model.js.map +1 -0
- package/dist/cli/tui.d.ts +15 -0
- package/dist/cli/tui.js +451 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/ui.d.ts +10 -0
- package/dist/cli/ui.js +135 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/core/ades.d.ts +79 -0
- package/dist/core/ades.js +130 -0
- package/dist/core/ades.js.map +1 -0
- package/dist/core/baton.d.ts +37 -0
- package/dist/core/baton.js +92 -0
- package/dist/core/baton.js.map +1 -0
- package/dist/core/brain-extract.d.ts +111 -0
- package/dist/core/brain-extract.js +248 -0
- package/dist/core/brain-extract.js.map +1 -0
- package/dist/core/brain-index.d.ts +128 -0
- package/dist/core/brain-index.js +338 -0
- package/dist/core/brain-index.js.map +1 -0
- package/dist/core/brain.d.ts +214 -0
- package/dist/core/brain.js +425 -0
- package/dist/core/brain.js.map +1 -0
- package/dist/core/claude-cli.d.ts +12 -0
- package/dist/core/claude-cli.js +47 -0
- package/dist/core/claude-cli.js.map +1 -0
- package/dist/core/distill.d.ts +19 -0
- package/dist/core/distill.js +88 -0
- package/dist/core/distill.js.map +1 -0
- package/dist/core/eventlog.d.ts +43 -0
- package/dist/core/eventlog.js +211 -0
- package/dist/core/eventlog.js.map +1 -0
- package/dist/core/git.d.ts +203 -0
- package/dist/core/git.js +460 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/logbook.d.ts +59 -0
- package/dist/core/logbook.js +118 -0
- package/dist/core/logbook.js.map +1 -0
- package/dist/core/memory.d.ts +34 -0
- package/dist/core/memory.js +124 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/notify.d.ts +11 -0
- package/dist/core/notify.js +30 -0
- package/dist/core/notify.js.map +1 -0
- package/dist/core/projection.d.ts +20 -0
- package/dist/core/projection.js +96 -0
- package/dist/core/projection.js.map +1 -0
- package/dist/core/registry.d.ts +76 -0
- package/dist/core/registry.js +147 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/router.d.ts +42 -0
- package/dist/core/router.js +99 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/routes.d.ts +93 -0
- package/dist/core/routes.js +399 -0
- package/dist/core/routes.js.map +1 -0
- package/dist/core/search.d.ts +63 -0
- package/dist/core/search.js +0 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/setup.d.ts +70 -0
- package/dist/core/setup.js +250 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/suggestions.d.ts +15 -0
- package/dist/core/suggestions.js +38 -0
- package/dist/core/suggestions.js.map +1 -0
- package/dist/core/worktree.d.ts +32 -0
- package/dist/core/worktree.js +83 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/daemon/app-page.d.ts +32 -0
- package/dist/daemon/app-page.js +6363 -0
- package/dist/daemon/app-page.js.map +1 -0
- package/dist/daemon/auth.d.ts +51 -0
- package/dist/daemon/auth.js +135 -0
- package/dist/daemon/auth.js.map +1 -0
- package/dist/daemon/board.d.ts +117 -0
- package/dist/daemon/board.js +202 -0
- package/dist/daemon/board.js.map +1 -0
- package/dist/daemon/brand-icons.d.ts +29 -0
- package/dist/daemon/brand-icons.js +44 -0
- package/dist/daemon/brand-icons.js.map +1 -0
- package/dist/daemon/client.d.ts +127 -0
- package/dist/daemon/client.js +237 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/geist-font.d.ts +10 -0
- package/dist/daemon/geist-font.js +783 -0
- package/dist/daemon/geist-font.js.map +1 -0
- package/dist/daemon/guards.d.ts +18 -0
- package/dist/daemon/guards.js +40 -0
- package/dist/daemon/guards.js.map +1 -0
- package/dist/daemon/linear.d.ts +59 -0
- package/dist/daemon/linear.js +117 -0
- package/dist/daemon/linear.js.map +1 -0
- package/dist/daemon/push.d.ts +22 -0
- package/dist/daemon/push.js +58 -0
- package/dist/daemon/push.js.map +1 -0
- package/dist/daemon/runtime.d.ts +299 -0
- package/dist/daemon/runtime.js +1016 -0
- package/dist/daemon/runtime.js.map +1 -0
- package/dist/daemon/server.d.ts +125 -0
- package/dist/daemon/server.js +1728 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/tasks.d.ts +153 -0
- package/dist/daemon/tasks.js +264 -0
- package/dist/daemon/tasks.js.map +1 -0
- package/dist/daemon/terminals.d.ts +82 -0
- package/dist/daemon/terminals.js +372 -0
- package/dist/daemon/terminals.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.js +17 -0
- package/dist/sdk.js.map +1 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/docs/SETUP.md +265 -0
- package/docs/adapters.md +132 -0
- package/docs/design-system.md +245 -0
- package/docs/img/board-linear.png +0 -0
- package/docs/img/board-projects.png +0 -0
- package/docs/img/brain.png +0 -0
- package/docs/img/settings.png +0 -0
- package/docs/img/workspace.png +0 -0
- package/docs/integration-notes.md +76 -0
- package/docs/proposals/brain.md +262 -0
- package/package.json +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
# Loom
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nickthelegend/loom/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/threadloom)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**The shared-memory layer for AI dev environments.** Every coding agent — Claude Code,
|
|
8
|
+
OpenCode, Antigravity, Codex, … — keeps its own brain in its own files. Loom makes them
|
|
9
|
+
**one brain**: connect your ADEs, and their memory, decisions, and context become a
|
|
10
|
+
single shared thread that flows from one agent to the next.
|
|
11
|
+
|
|
12
|
+
Today that means **Claude Code, Codex, OpenCode and Grok Code** as full agents, each
|
|
13
|
+
verified against a real version, plus **Antigravity and Kiro** driven through their
|
|
14
|
+
own windows —
|
|
15
|
+
see [Supported agents](#supported-agents) for exactly how far each one goes, and
|
|
16
|
+
[How memory actually reaches a model](#how-memory-actually-reaches-a-model) for the
|
|
17
|
+
part most tools gloss over.
|
|
18
|
+
|
|
19
|
+
Loom is **not** another IDE. It's the thin layer *between* your agents — the continuity
|
|
20
|
+
and memory they don't share on their own.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
CLAUDE.md AGENTS.md .antigravity/ ← each ADE's native memory
|
|
24
|
+
│ │ │
|
|
25
|
+
└──────────────┼───────────────┘ import
|
|
26
|
+
▼
|
|
27
|
+
╔═══════════════════╗
|
|
28
|
+
║ ONE SHARED BRAIN ║ decisions · imported ADE memory · the thread
|
|
29
|
+
╚═════════╤═════════╝
|
|
30
|
+
│ projected on every handoff
|
|
31
|
+
┌──────────────┼───────────────┐
|
|
32
|
+
Claude Code ──▶ OpenCode ──▶ Claude Code ← baton carries the brain forward
|
|
33
|
+
(plan) (execute) (review)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="docs/img/workspace.png" alt="Loom workspace — one thread over every agent, with the Explorer, the composer, and the agent baton" width="100%">
|
|
38
|
+
<br>
|
|
39
|
+
<em>One thread over every agent — projects and chats on the left, the shared conversation in the middle, the Explorer on the right, and a composer you switch agents from without leaving the box.</em>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
## Why
|
|
43
|
+
|
|
44
|
+
Every coding agent keeps its own brain. Claude Code's memory can't be read by
|
|
45
|
+
OpenCode; Antigravity doesn't know what you decided with Claude an hour ago. Switch
|
|
46
|
+
tools and you re-explain your project every time.
|
|
47
|
+
|
|
48
|
+
Other multi-agent tools answer this by keeping agents **apart** — each in its own
|
|
49
|
+
worktree, run in parallel, compare and merge. Loom makes the opposite bet: keep the
|
|
50
|
+
agents' **memory together** so work *continues* across them instead of forking.
|
|
51
|
+
|
|
52
|
+
- **One brain across every ADE** — Loom imports each agent's native memory
|
|
53
|
+
(`CLAUDE.md`, `AGENTS.md`, …) into a unified store, merges it with your decisions and
|
|
54
|
+
the shared thread, and hands the whole thing to whoever picks up next. `loom memory`.
|
|
55
|
+
- **The baton** — exactly one agent works at a time; passing it *carries the context*
|
|
56
|
+
(interrupt-safe, memory projected, briefing armed). Not isolation — continuation.
|
|
57
|
+
- **Routes** — let Loom drive the chain: `loom route ship "add dark mode"` runs
|
|
58
|
+
plan → execute → review as one command, the brain flowing hop to hop; or `loom route
|
|
59
|
+
auto` lets an LLM pick each next agent.
|
|
60
|
+
- **Every surface, one daemon** — a full-screen TUI, a web app, a desktop window, and a
|
|
61
|
+
phone app (voice input, per-prompt diffs, push) — each a paired client of the same
|
|
62
|
+
local daemon over your tailnet.
|
|
63
|
+
- **Local-first & yours** — one `npm i -g`, no account, MIT, runs headless on a server.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
Requires **Node ≥ 22.5** (Loom's event log uses the built-in `node:sqlite`).
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install -g threadloom # → `loom` on your PATH
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Other paths:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# one-liner from source (clones ~/.loom-src, builds, links; re-run to update)
|
|
77
|
+
curl -fsSL https://raw.githubusercontent.com/nickthelegend/loom/main/scripts/install.sh | bash
|
|
78
|
+
|
|
79
|
+
# straight from git
|
|
80
|
+
npm install -g github:nickthelegend/loom
|
|
81
|
+
|
|
82
|
+
# hackable checkout
|
|
83
|
+
git clone https://github.com/nickthelegend/loom.git && cd loom
|
|
84
|
+
npm install && npm run build && npm link
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then verify the setup:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
loom doctor # checks node, agents, tailscale, daemon, and your project
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Surfaces, all talking to the same daemon:
|
|
94
|
+
- **TUI / CLI** — `loom` (default), `loom chat`, `loom send`, …
|
|
95
|
+
- **Desktop app** — [`desktop/`](desktop/README.md): `cd desktop && npm install && npm
|
|
96
|
+
start` opens a native Electron window (starts the daemon and pairs itself) on the
|
|
97
|
+
workspace below.
|
|
98
|
+
- **Phone app** — [`app/`](app/README.md): `cd app && npx expo install && npx expo start`,
|
|
99
|
+
scan with Expo Go (voice input, per-prompt diffs, push).
|
|
100
|
+
- **Web app** — no install; `loom pair` → open the link. Same workspace in the browser.
|
|
101
|
+
|
|
102
|
+
## The workspace
|
|
103
|
+
|
|
104
|
+
On a wide screen the web app (and the desktop shell around it) is a full workspace for
|
|
105
|
+
*driving* agents — still not an editor: Loom shows you the context and the agents do
|
|
106
|
+
the writing.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
┌───────────┬────────────────────────────┬──────────────┐
|
|
110
|
+
│ projects │ Thread · Board · Brain │ Explorer │
|
|
111
|
+
│ └ chats │ │ Search │
|
|
112
|
+
│ │ the conversation, with │ Source ctl │
|
|
113
|
+
│ New task │ Update(n files) cards ────┼─▶ diff opens │
|
|
114
|
+
│ New proj │ │ Agents │
|
|
115
|
+
│ Search ├────────────────────────────┤ │
|
|
116
|
+
│ │ terminal (a real shell) │ │
|
|
117
|
+
└───────────┴────────────────────────────┴──────────────┘
|
|
118
|
+
live · host · baton · spend ← status bar
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- **Projects + chats** in the left rail: a project holds as many conversations as you
|
|
122
|
+
want, and they share one brain, one baton and one working tree. The **agents** live in
|
|
123
|
+
the right panel — click one to aim your next message at it, click its role to rename
|
|
124
|
+
the job (roles are free text: "architect", "the one that writes docs", anything).
|
|
125
|
+
- **Thread** is the shared conversation; **Brain** is the unified memory; **Board**
|
|
126
|
+
is everything in flight.
|
|
127
|
+
- **Board** is one board with three sources — **GitHub**, **Projects**, **Linear** —
|
|
128
|
+
switched from a segmented control (see [GitHub & Linear, native](#github--linear-native)).
|
|
129
|
+
GitHub is four live columns (working → needs you → in review → ready to merge); cards
|
|
130
|
+
come from **yours** (`+ Task`), **Loom** (which agents are running or blocked), and
|
|
131
|
+
your repo's **PRs** (draft, CI failed, changes requested, approved — read through your
|
|
132
|
+
own `gh`). Search issues and PRs in GitHub's own query language; **Start** hands an
|
|
133
|
+
issue to an agent. Dragging your own card really moves it; dragging a PR card only moves
|
|
134
|
+
where you *see* it — the badge keeps telling the truth, because a drag can't approve a
|
|
135
|
+
review or turn CI green. Each card wears its agent's own logo.
|
|
136
|
+
- **Click any change** — an `Update(n files)` card, or a file in Source Control — and the
|
|
137
|
+
diff opens to the right of the chat. It stays closed until you ask for it.
|
|
138
|
+
- **Explorer / Search / Source Control / Tasks** in the right panel; every column is
|
|
139
|
+
drag-resizable (double-click a handle to reset).
|
|
140
|
+
- **Terminal** (`Ctrl` + `` ` ``) is a real terminal in the project directory — a
|
|
141
|
+
proper pty, so the shell draws its own prompt and `vim`, `less`, `htop`, `^C`
|
|
142
|
+
and `^Z` all behave. `node-pty` is optional: without it (Linux with no build
|
|
143
|
+
toolchain) you get a pipe-backed shell instead, where `cd` and variables still
|
|
144
|
+
persist and `^C` still works, driven a line at a time.
|
|
145
|
+
- **New task** (`n`) picks a project, a task, and **one agent — or several**, which run
|
|
146
|
+
it as a pipeline, hop to hop. **New project** (`p`) adds a repo — a native folder
|
|
147
|
+
picker in the desktop app — and reports which ADEs it found on the host.
|
|
148
|
+
|
|
149
|
+
### Settings, in one place
|
|
150
|
+
|
|
151
|
+
Everything about a Loom lives behind the gear in the bottom-left: **Setup** (what the
|
|
152
|
+
machine still needs to run agents), **Diagnostics** (`loom doctor`, run live on the
|
|
153
|
+
daemon), **Updates** (build rev, and how far the checkout is behind its remote),
|
|
154
|
+
**Preferences** (theme, the brain extractor, handoff brief style, default agent),
|
|
155
|
+
**Devices** (paired clients — revoke one, or pair another), and **About**.
|
|
156
|
+
|
|
157
|
+
<p align="center">
|
|
158
|
+
<img src="docs/img/settings.png" alt="Loom's Settings screen — Setup, Diagnostics, Updates, Preferences, Devices, About" width="100%">
|
|
159
|
+
</p>
|
|
160
|
+
|
|
161
|
+
## GitHub & Linear, native
|
|
162
|
+
|
|
163
|
+
Browse pull requests, issues, and **GitHub Project boards** in-app; open a worktree from
|
|
164
|
+
any task; review and approve PRs; and file **Linear** issues with a team selector — no
|
|
165
|
+
context switch, no second browser tab.
|
|
166
|
+
|
|
167
|
+
<p align="center">
|
|
168
|
+
<img src="docs/img/board-projects.png" alt="A GitHub Project board rendered in Loom, items grouped by their Status column" width="100%">
|
|
169
|
+
<br>
|
|
170
|
+
<em>A GitHub Project (v2) board, in-app — items grouped by their Status column, each linking back to its issue or PR.</em>
|
|
171
|
+
</p>
|
|
172
|
+
|
|
173
|
+
- **Browse PRs, issues, and Project boards.** The **GitHub** source is the live kanban;
|
|
174
|
+
the **Projects** source lists the owner's GitHub Project (v2) boards and lays a
|
|
175
|
+
project's items out by Status; search takes github.com's own query language.
|
|
176
|
+
- **Review and approve PRs in place.** Open a PR's diff without leaving Loom and post the
|
|
177
|
+
three things a reviewer does — **comment**, **request changes**, **approve**. The review
|
|
178
|
+
is signed as you (through your `gh`); approve asks first, because it publishes.
|
|
179
|
+
- **Open a worktree from any task.** One click cuts a checked-out branch in its own
|
|
180
|
+
directory, a sibling of the repo: a **PR** worktree checks the branch out — forks
|
|
181
|
+
included — and an **issue** worktree cuts a fresh branch to start it. An agent can work
|
|
182
|
+
a PR while your main tree stays exactly where you left it.
|
|
183
|
+
- **Create Linear issues with a team selector.** Pick a team, write a title and
|
|
184
|
+
description, file it — the new issue's identifier comes straight back.
|
|
185
|
+
|
|
186
|
+
<p align="center">
|
|
187
|
+
<img src="docs/img/board-linear.png" alt="Loom's Linear source, honest about being off until you connect it" width="100%">
|
|
188
|
+
<br>
|
|
189
|
+
<em>Linear is off until you connect it — and Loom tells you exactly how, because it holds no token of its own.</em>
|
|
190
|
+
</p>
|
|
191
|
+
|
|
192
|
+
**Loom holds no token of its own.** GitHub goes through your `gh` CLI; Linear reads
|
|
193
|
+
`LINEAR_API_KEY` from the daemon's own environment (you `export` it) and never stores,
|
|
194
|
+
logs, or transmits it anywhere else. No key → an honest "not connected", never a dead
|
|
195
|
+
form — the same bet the agent adapters make by shelling out to the CLIs you already have.
|
|
196
|
+
|
|
197
|
+
## Quickstart
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cd your-project
|
|
201
|
+
loom init # detects installed agents (claude, opencode), assigns roles
|
|
202
|
+
loom # opens the TUI — a tabbed workspace (Thread · Board · Brain · Diff)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
██ ▄████▄ ▄████▄ ▄█▄▄█▄
|
|
207
|
+
██ ██ ██ ██ ██ ██▀▀██
|
|
208
|
+
██ ██ ██ ██ ██ ██ ██
|
|
209
|
+
██████ ▀████▀ ▀████▀ ██ ██
|
|
210
|
+
one thread · every agent
|
|
211
|
+
|
|
212
|
+
1 Thread 2 Board 3 Brain 4 Diff ↑12 pgup/pgdn
|
|
213
|
+
10:44 claude-code here's the plan: …
|
|
214
|
+
⟶ baton: claude-code → opencode
|
|
215
|
+
10:45 opencode implementing step 1 …
|
|
216
|
+
|
|
217
|
+
╭──────────────────────────────────────────────╮
|
|
218
|
+
│ › Ask anything… "/route ship: add dark mode" │
|
|
219
|
+
│ opencode · executor ⟵ baton │
|
|
220
|
+
╰──────────────────────────────────────────────╯
|
|
221
|
+
shift+tab view · tab agent · ctrl+p palette · esc back/interrupt
|
|
222
|
+
~ my-project · baton opencode ➤ ship 2/3
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The TUI is a **tabbed workspace**, not just a thread:
|
|
226
|
+
|
|
227
|
+
- **Thread** — the conversation, streamed. **`tab` shifts the active agent/IDE**
|
|
228
|
+
(claude-code → opencode → back); the handoff (interrupt-safe, memory projected, briefing
|
|
229
|
+
armed) happens when you hit enter, so switching is one keystroke, not a ceremony.
|
|
230
|
+
- **Board** — agents, your cards, issues and PRs in the four flow columns.
|
|
231
|
+
- **Brain** — the memory the project has learned, grouped by kind (failures first), each
|
|
232
|
+
tagged with who learned it.
|
|
233
|
+
- **Diff** — the working tree: changed files and a colourised patch.
|
|
234
|
+
|
|
235
|
+
**`shift+tab` cycles the tabs** (or `/board`, `/brain`, `/diff`, `/thread`, or the
|
|
236
|
+
palette); `pgup/pgdn` scrolls the view. **`ctrl+p` opens the command palette**
|
|
237
|
+
(fuzzy-filtered: jump to a view, shift to any agent, launch a named route, decision,
|
|
238
|
+
interrupt, pair…). `esc` steps back to the thread or interrupts the running turn; `/help`
|
|
239
|
+
lists the slash commands.
|
|
240
|
+
|
|
241
|
+
Prefer plain line-mode (SSH, scripts)? `loom chat` is the same thread as a classic REPL,
|
|
242
|
+
and every action also exists as a one-shot command (`loom send`, `loom handoff`, …).
|
|
243
|
+
|
|
244
|
+
## Routing — multi-hop pipelines
|
|
245
|
+
|
|
246
|
+
Handoffs are unlimited and manual by default. **Routes** automate a chain of them:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
loom route auto "add a dark-mode toggle" # DYNAMIC: an LLM picks each hop
|
|
250
|
+
loom route ship "add a dark-mode toggle" # named pipeline from config
|
|
251
|
+
loom route planner,executor "fix the flaky test" # ad-hoc: roles…
|
|
252
|
+
loom route claude-code,opencode,claude-code "…" # …or agent ids, any length
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**`auto` is dynamic routing**: after every hop, a router looks at the task, the hop
|
|
256
|
+
history, and the last replies, then picks the next agent — or declares the task done.
|
|
257
|
+
The router is Claude (headless, small model, JSON out) with a deterministic
|
|
258
|
+
plan→execute→review rules engine as automatic fallback, so routes never stall on a
|
|
259
|
+
router failure. Every decision is logged with its reason
|
|
260
|
+
(`➤ hop 2 → opencode (plan ready — execute it)`), a hop budget caps runaways
|
|
261
|
+
(`--max-hops`, default 8), and `--router rules` skips the LLM entirely.
|
|
262
|
+
|
|
263
|
+
What happens per hop: interrupt-safe **handoff** → shared-memory **projection** →
|
|
264
|
+
**briefing** → the step's role instruction. Then:
|
|
265
|
+
|
|
266
|
+
- step finishes cleanly → Loom advances to the next agent automatically;
|
|
267
|
+
- the agent asks a question → the route **pauses** (`waiting_human`), you get a
|
|
268
|
+
notification, `loom route --status` and the board show the question; you answer in
|
|
269
|
+
the shared thread (`loom send "…"`) and the route **resumes by itself**;
|
|
270
|
+
- an agent errors or a step times out (45 min default) → the route fails loudly;
|
|
271
|
+
- **you always outrank the route**: any manual `handoff`/`interrupt` cancels it, and
|
|
272
|
+
`loom route --abort` stops it and interrupts the in-flight turn.
|
|
273
|
+
|
|
274
|
+
`--detach` returns immediately (fire-and-notify); following with Ctrl-C also leaves the
|
|
275
|
+
route running server-side. One route per project at a time (the baton is one write
|
|
276
|
+
lock); run routes across *different* projects in parallel freely.
|
|
277
|
+
|
|
278
|
+
Define named pipelines in `.loom/config.json` — steps are roles or agent ids, and any
|
|
279
|
+
step can carry its own focus:
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
"routes": {
|
|
283
|
+
"ship": ["planner", "executor", "reviewer"],
|
|
284
|
+
"api-only": [
|
|
285
|
+
{ "step": "planner", "instruction": "design the endpoint contract only" },
|
|
286
|
+
{ "step": "executor", "instruction": "only touch src/api — no schema changes" },
|
|
287
|
+
"reviewer"
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Per-step instructions are appended to the role guidance for exactly that step — the
|
|
293
|
+
next hop never sees them. `loom init` seeds a `ship` route automatically when it
|
|
294
|
+
detects at least two roles.
|
|
295
|
+
|
|
296
|
+
## Commands
|
|
297
|
+
|
|
298
|
+
| Command | What it does |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `loom` | **The TUI** — tabbed workspace (Thread · Board · Brain · Diff), `shift+tab` switches view, `tab` shifts agents, `/`-commands + `ctrl+p` palette inline |
|
|
301
|
+
| `loom init` | Make the current directory a Loom project (auto-detects agents) |
|
|
302
|
+
| `loom chat` | Same thread as a plain line REPL (`/handoff`, `/interrupt`, `@agent`) |
|
|
303
|
+
| `loom send <text>` | One-shot message (`-a <agent>` to address someone specific) |
|
|
304
|
+
| `loom handoff <agent>` | Pass the baton — interrupts, projects memory, briefs the target |
|
|
305
|
+
| `loom route <spec> "<task>"` | Run a pipeline (name, or `a,b,c` ids/roles); `--status` / `--abort` / `--detach` |
|
|
306
|
+
| `loom routes` | List named pipelines defined for this project |
|
|
307
|
+
| `loom interrupt` | Stop the current holder's turn (cancels an active route) |
|
|
308
|
+
| `loom decision <text>` | Record a decision into shared memory |
|
|
309
|
+
| `loom memory [import]` | The unified brain — one memory across every connected ADE |
|
|
310
|
+
| `loom log [-f]` | Show (or follow) the project event log |
|
|
311
|
+
| `loom costs` | Project spend: total + per-agent turns, $ and agent time |
|
|
312
|
+
| `loom agents` / `loom projects` / `loom status` | Who's who, board of projects, daemon health |
|
|
313
|
+
| `loom up [--tailnet] [--restart]` / `loom down` / `loom daemon` | Daemon lifecycle (`--tailnet` binds to your Tailscale IP) |
|
|
314
|
+
| `loom pair` | QR deep link that pairs a phone (single-use token) |
|
|
315
|
+
| `loom clients [--revoke <id>] [--ping]` | Paired devices: list, revoke, or send a test push |
|
|
316
|
+
| `loom doctor` | Diagnose env, daemon, binding, and project config — with fixes |
|
|
317
|
+
|
|
318
|
+
## Supported agents
|
|
319
|
+
|
|
320
|
+
| Agent | Tier | Transport | Status |
|
|
321
|
+
|---|---|---|---|
|
|
322
|
+
| Claude Code | adapter (full-duplex) | headless CLI, `stream-json`, `--resume`, briefing via `--append-system-prompt` | ✅ verified against 2.1.83 |
|
|
323
|
+
| Codex | adapter (full-duplex) | `codex exec --json` (JSONL), `exec resume <thread>`; found on PATH **or inside Codex.app** | ✅ verified against codex-cli 0.142.4 |
|
|
324
|
+
| OpenCode | adapter (full-duplex) | `opencode serve` HTTP + SSE (`/prompt`, `/interrupt`, `/event`) | ✅ verified against 1.17.20 |
|
|
325
|
+
| Grok Code | adapter (full-duplex) | `grok -p --output-format json`, `-r <session>` | 🔶 verified against 0.2.54 — **answers only, no tool or edit events** (see below) |
|
|
326
|
+
| Echo | adapter (demo/tests) | in-process | ✅ |
|
|
327
|
+
| Antigravity | **bridge** (driveable) | Chromium debug port — types into the real chat panel and reads the panel back | 🔶 mechanism verified; its selectors are not (see below) |
|
|
328
|
+
| Kiro | **bridge** (driveable) | same, via the same driver | 🔶 mechanism verified; its selectors are not |
|
|
329
|
+
|
|
330
|
+
Three of those need their asterisks spelled out, because the table row is
|
|
331
|
+
shorter than the truth:
|
|
332
|
+
|
|
333
|
+
**Codex reports tokens, never money.** Its `turn.completed` carries
|
|
334
|
+
`input_tokens` / `output_tokens` and no dollar figure, so Loom shows tokens and
|
|
335
|
+
no cost for Codex turns. A USD number derived from a price table we'd have to
|
|
336
|
+
keep current is fiction with a decimal point in it.
|
|
337
|
+
|
|
338
|
+
**Grok can't show you its steps.** `--output-format streaming-json` sounds like
|
|
339
|
+
it would help and doesn't: it emits `thought` and `text` deltas and a final
|
|
340
|
+
`end`, with no tool calls and no file edits — not even when the turn
|
|
341
|
+
demonstrably ran a shell command and wrote a file. So a Grok turn in the thread
|
|
342
|
+
is what it said, and `git status` is what it did. Inferring the edits by diffing
|
|
343
|
+
the tree would put guesses in the event log dressed as facts. Its permission
|
|
344
|
+
mode also defaults to `bypassPermissions`, because headless with no TTY to ask,
|
|
345
|
+
every other mode ends the turn `Cancelled` having written nothing.
|
|
346
|
+
|
|
347
|
+
**Antigravity and Kiro are driven, not routed.** Both are Electron apps with no
|
|
348
|
+
API; Loom connects to the debugging port, finds the chat box, types through the
|
|
349
|
+
input pipeline and reads back what the panel gained — the approach
|
|
350
|
+
[antigravity_phone_chat](https://github.com/krishnakanthb13/antigravity_phone_chat)
|
|
351
|
+
takes, and for the same reason: never touch the provider APIs, drive the app
|
|
352
|
+
that's already signed in. Launch them with
|
|
353
|
+
`--remote-debugging-port=9222` first.
|
|
354
|
+
|
|
355
|
+
The driver refuses more than it accepts, on purpose. Both apps are VS Code
|
|
356
|
+
family and Monaco — the editor holding your source file — is a
|
|
357
|
+
`contenteditable`. Anything under `.monaco-editor` is never a candidate, a
|
|
358
|
+
candidate must be labelled like a chat box, and zero-or-several matches is a
|
|
359
|
+
refusal that names the fix (`options.selectors.composer`). Typing a prompt into
|
|
360
|
+
your code and pressing Enter is not a mistake an error message repairs.
|
|
361
|
+
|
|
362
|
+
What's verified is the mechanism, against a real Chromium. What is **not**
|
|
363
|
+
verified is either app's actual chat DOM: Antigravity shows a sign-in screen and
|
|
364
|
+
Kiro shows no chat panel until you open one, so there was no composer to read
|
|
365
|
+
the selectors from. Reachable and driveable are separate questions, and Loom
|
|
366
|
+
answers both — a signed-out Antigravity replies to CDP cheerfully and reports
|
|
367
|
+
`driveable: false — no chat box on screen`.
|
|
368
|
+
|
|
369
|
+
**Adapters** implement the full contract (send / stream / injectMemory / interrupt /
|
|
370
|
+
diff) and may hold the baton. **Bridges** only observe and receive shared-memory
|
|
371
|
+
projections — they never hold the write lock. That's a design decision, not a gap: GUI
|
|
372
|
+
agents without a stable API can't be trusted with interrupt-safe writes. See
|
|
373
|
+
[docs/integration-notes.md](docs/integration-notes.md) for the verified surfaces.
|
|
374
|
+
|
|
375
|
+
## How memory actually reaches a model
|
|
376
|
+
|
|
377
|
+
Worth being precise about, because this is the whole premise and it has a soft edge.
|
|
378
|
+
|
|
379
|
+
Loom **reliably builds** the shared brain (every ADE's imported memory + your decisions
|
|
380
|
+
+ the thread) and **reliably writes** it to `.loom/memory/<agent>.md` on every handoff.
|
|
381
|
+
That part is solid and tested.
|
|
382
|
+
|
|
383
|
+
Getting it into the model's context is a different problem, and it depends on the agent:
|
|
384
|
+
|
|
385
|
+
| Agent | How the brain arrives | Strength |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| Claude Code | briefing via `--append-system-prompt` — the model *always* sees a summary (recent decisions + messages) plus a pointer to `.loom/memory/claude-code.md`, which it can Read | **strong** — the summary is guaranteed; the full file is one tool-call away |
|
|
388
|
+
| Grok Code | the briefing rides in `--rules`, Grok's real system-prompt channel, so `-p` stays your clean prompt | **strong** — `--rules` is a genuine system channel, not text in the turn |
|
|
389
|
+
| Codex | no `--append-system-prompt` on `codex exec`, so the briefing rides in front of your prompt — **framed** as an unmissable `LOOM SESSION MEMORY — authoritative, read first` block | **reliable** — one prompt either way, but framed so it can't be mistaken for chatter |
|
|
390
|
+
| OpenCode | no per-prompt system field on `/prompt`, so the same **framed** block is prepended to your prompt | **reliable** — delivered as an authoritative block, not loose text |
|
|
391
|
+
| Antigravity, Kiro | nothing tells them the file exists — Loom types into their chat box, which is not a system prompt | **none** — a human has to open it |
|
|
392
|
+
|
|
393
|
+
So: the **summary always lands**; the **full brain is an invitation**. An agent that
|
|
394
|
+
ignores the pointer works from the summary alone. If you need something remembered for
|
|
395
|
+
certain, put it in a decision (`loom decide`) — decisions ride in the briefing itself.
|
|
396
|
+
There's an opt-in eval (`LOOM_TEST_REAL=1`) that checks a real model actually *uses* an
|
|
397
|
+
injected brief, and declines rather than invents when the brief is silent.
|
|
398
|
+
|
|
399
|
+
Memory also flows **one way**. Loom reads `CLAUDE.md` / `AGENTS.md` and never writes
|
|
400
|
+
them, so your ADE's own memory files stay yours. And the import is a **merge, not a
|
|
401
|
+
parse**: files are read, capped at 8000 chars, and concatenated under headers. Claude
|
|
402
|
+
Code's `@path` imports are **not followed** — a `CLAUDE.md` that is mostly `@` pointers
|
|
403
|
+
imports the pointers, not what they point at.
|
|
404
|
+
|
|
405
|
+
The brain also **learns on its own**. After each turn a small Claude reads what changed
|
|
406
|
+
and files what's worth keeping as typed memory *units* — a constraint, a decision, a
|
|
407
|
+
convention, a fact, a failure — reconciled on write (add / update / forget, never a
|
|
408
|
+
growing blob), the approach [mem0](https://github.com/mem0ai/mem0) pioneered, adapted to
|
|
409
|
+
Loom's event log. Every unit's evidence is verified against the turn before it's kept, so
|
|
410
|
+
the brain doesn't remember things that were never said. Retrieval is hybrid too — exact
|
|
411
|
+
entity matches (file paths, symbols, error codes) unioned with BM25 over the text, no
|
|
412
|
+
embedding model to ship — with failures and constraints biased to the top of the brief,
|
|
413
|
+
because getting burned twice is worse than missing a detail.
|
|
414
|
+
|
|
415
|
+
The brain is the **project's**, not each agent's: a fact one agent learns is scoped to the
|
|
416
|
+
chat, not walled off to whoever happened to learn it, so it reaches whichever agent takes
|
|
417
|
+
the baton next. (The [`brain-shared` test](test/brain-shared.test.ts) makes this concrete —
|
|
418
|
+
five agents each learn one fact, and every other agent's brief then carries all five.) The
|
|
419
|
+
**Brain tab** — and `loom tui`'s **Brain** view — show exactly what it has learned; toggle
|
|
420
|
+
the extractor off per project in Settings.
|
|
421
|
+
|
|
422
|
+
<p align="center">
|
|
423
|
+
<img src="docs/img/brain.png" alt="Loom's Brain tab — the memory units it has learned, by kind" width="100%">
|
|
424
|
+
<br>
|
|
425
|
+
<em>The Brain tab — learned memory units, grouped by kind, each traceable to the turn it came from.</em>
|
|
426
|
+
</p>
|
|
427
|
+
|
|
428
|
+
## How it works
|
|
429
|
+
|
|
430
|
+
- **Event log** (`.loom/log.db`, SQLite via `node:sqlite`, JSONL fallback) — every
|
|
431
|
+
message, tool call, file edit, decision, and handoff, appended in order. The log *is*
|
|
432
|
+
the project's memory; everything else is a view of it.
|
|
433
|
+
- **Projection** — on handoff, Loom distills the log into
|
|
434
|
+
`.loom/memory/<agent>.md` (persistent, namespaced) and arms a short one-shot briefing
|
|
435
|
+
injected with the target's next turn (system-prompt append for Claude Code, delimited
|
|
436
|
+
preamble for OpenCode). Two renderers behind one interface:
|
|
437
|
+
- **template** (default) — deterministic, instant, free;
|
|
438
|
+
- **llm** — a small Claude model distills the recent log into a dense doc
|
|
439
|
+
(mission / current state / decisions / risks / next moves). Opt in per project:
|
|
440
|
+
`"projection": { "mode": "llm", "model": "haiku" }`. Any failure or timeout falls
|
|
441
|
+
back to the template — a broken Claude never blocks a handoff. Bridges always get
|
|
442
|
+
template views (no N×LLM waste per hop).
|
|
443
|
+
- **Baton** — persisted per project (`.loom/state.json`). Messages route to the holder;
|
|
444
|
+
addressing a non-holder returns `409 not_holder` and the surface asks you to confirm a
|
|
445
|
+
handoff. Ghost holders (agent removed from config) self-heal. Every handoff snapshots
|
|
446
|
+
the outgoing agent's working-tree state (dirty flag + `git status`) into the log.
|
|
447
|
+
- **Unified memory ("multiple memory in one")** — each connected ADE keeps its own
|
|
448
|
+
native memory (`CLAUDE.md`, `AGENTS.md`, …). Loom imports them all into one brain
|
|
449
|
+
(`memory_import` events, content-hash deduped), merges them with the project's
|
|
450
|
+
decisions and shared thread, and projects the union into whoever holds the baton.
|
|
451
|
+
Connect a new agent → its knowledge joins the brain, and everything the others learned
|
|
452
|
+
flows into it. `loom memory` shows the merged brain; it refreshes on open and on every
|
|
453
|
+
handoff. This is the seam an isolation-first tool (separate worktrees) can't own.
|
|
454
|
+
- **Decisions** — `loom decision <text>` pins a fact, and any agent line starting
|
|
455
|
+
`Decision: …` is captured automatically. Decisions ride every future projection.
|
|
456
|
+
- **Cost telemetry** — agents that report per-turn cost (Claude Code, OpenCode) feed a
|
|
457
|
+
live ledger: `loom costs` breaks it down per agent, the board/TUI/phone app show the
|
|
458
|
+
project total, and every route logs exactly what it spent
|
|
459
|
+
(`✔ route completed (3 steps) · $0.0421`). Totals rehydrate from the event log, so
|
|
460
|
+
they survive restarts.
|
|
461
|
+
- **Daemon** — one process, many projects. REST for commands, WebSocket for the live
|
|
462
|
+
stream. Config edits hot-reload when the project is quiet.
|
|
463
|
+
|
|
464
|
+
## Your phone (Android today, over Tailscale)
|
|
465
|
+
|
|
466
|
+
The daemon serves a full phone app at `/app` — board, live thread, agent chips, routes.
|
|
467
|
+
No app store, no build step; it ships inside Loom.
|
|
468
|
+
|
|
469
|
+
**Pair from the app.** The web/desktop window has a **Connect a phone** button next to the
|
|
470
|
+
terminal: it opens a modal with a QR (and a copy link) and a **Local network / Tailnet**
|
|
471
|
+
toggle. Pick one and, if the daemon is still localhost-only, hit **Enable phone access** —
|
|
472
|
+
Loom adds a *second listener* on that LAN or tailnet IP (localhost is never disturbed, so
|
|
473
|
+
the window you're in doesn't blink) and shows a QR your phone can actually reach. Same
|
|
474
|
+
single-use token, no terminal needed.
|
|
475
|
+
|
|
476
|
+
**Or pair from the terminal:**
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
loom up --tailnet # daemon binds to your Tailscale IP (never 0.0.0.0)
|
|
480
|
+
loom pair # QR appears in the terminal (also `/pair` inside `loom tui`)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Scan the QR with your phone camera (for the tailnet path, the phone must be on your
|
|
484
|
+
tailnet — install the Tailscale app and sign in; the local-network path just needs the
|
|
485
|
+
same Wi-Fi). The link opens `…/app#pair=<token>`; the app claims the **single-use,
|
|
486
|
+
10-minute** pairing token from the URL fragment (fragments never hit the network log) and
|
|
487
|
+
exchanges it for its own client token. Then:
|
|
488
|
+
|
|
489
|
+
- **Board** — every project, needs-input dots, baton holder, live route progress.
|
|
490
|
+
- **Thread** — the same shared conversation, streaming over WebSocket.
|
|
491
|
+
- **Agent chips** — tap `opencode`, hit send: baton shifts (projection + briefing
|
|
492
|
+
included), exactly like `tab` in the TUI.
|
|
493
|
+
- **Routes** — the ➤ button opens a picker: choose **auto** (LLM picks each hop), any
|
|
494
|
+
named pipeline, or custom steps, type the task, go. Live banner with hop progress and
|
|
495
|
+
reasons, an abort button, and when a route pauses on a question you answer right
|
|
496
|
+
there and it resumes.
|
|
497
|
+
- Chrome menu → *Add to Home screen* installs it like an app.
|
|
498
|
+
|
|
499
|
+
**Push notifications** come with the native app ([`app/`](app/README.md)): open it once
|
|
500
|
+
after pairing and it registers its Expo push token with the daemon. From then on your
|
|
501
|
+
phone buzzes when an agent **needs input**, when a **route completes or fails**, and
|
|
502
|
+
when a solo turn finishes — route hops are deliberately silent (a 5-step pipeline
|
|
503
|
+
buzzes once, not five times). Verify with `loom clients --ping`.
|
|
504
|
+
|
|
505
|
+
## Security model
|
|
506
|
+
|
|
507
|
+
- The daemon binds to `127.0.0.1` by default, or your **Tailscale interface** with
|
|
508
|
+
`--tailnet` — never `0.0.0.0` on its own. **Connect-a-phone** can *add* a listener on a
|
|
509
|
+
specific LAN/tailnet IP (never `0.0.0.0`, never an arbitrary host — the target is
|
|
510
|
+
allow-listed to this machine's own addresses), and only when you ask. The tailnet is the
|
|
511
|
+
trust boundary: device auth and E2E encryption come from Tailscale.
|
|
512
|
+
- Every request needs a bearer token (`~/.loom/daemon.json`, mode 0600). Tokens are
|
|
513
|
+
256-bit random and compared in constant time; nothing state-changing is served before
|
|
514
|
+
the auth wall.
|
|
515
|
+
- **The local admin console.** A same-machine window bootstraps the admin token via
|
|
516
|
+
`GET /api/bootstrap` — gated by *both* a loopback TCP peer *and* a loopback `Host` header
|
|
517
|
+
(the second is the anti-DNS-rebinding check: a malicious page carries its own hostname,
|
|
518
|
+
so it's refused even though its socket rebound to 127.0.0.1). A remote window gets 403
|
|
519
|
+
and pairs like any device. Caveat: on a **shared multi-user host**, any local user can
|
|
520
|
+
reach loopback, so treat "same machine" as "trusted" — don't run the daemon on a box
|
|
521
|
+
where you don't trust the other logins.
|
|
522
|
+
- Pairing: `loom pair` (or the in-app button) mints a **short-lived (10 min), single-use**
|
|
523
|
+
token as a QR. The device exchanges it for a long-lived client token. The pairing token
|
|
524
|
+
rides in a URL *fragment* (`…/app#pair=…`), which browsers never put on the wire; the
|
|
525
|
+
client/admin token rides in the `Authorization` header (HTTP) and the WebSocket
|
|
526
|
+
**subprotocol** (never a URL query — so it stays out of history and proxy logs).
|
|
527
|
+
- **What a paired client can do:** everything in the project, *including a real shell*
|
|
528
|
+
(the terminal). Pairing a device therefore grants **arbitrary code execution as the
|
|
529
|
+
daemon's user** — the shell is not confined to the project directory. That is the
|
|
530
|
+
deliberate trade for a dev tool (bearer + tailnet is the boundary); pair only devices
|
|
531
|
+
you control. Paired clients are **not** admins, though: they can't mint pairing tokens
|
|
532
|
+
or open new network exposure — those need the admin token, which only the local console
|
|
533
|
+
or the CLI holds.
|
|
534
|
+
- The daemon survives a bad turn: unhandled rejections and exceptions are caught and
|
|
535
|
+
logged (Console + `~/.loom/daemon.log`) rather than taking every project down, and
|
|
536
|
+
`SIGINT`/`SIGTERM` shut it down cleanly.
|
|
537
|
+
|
|
538
|
+
## Adapter SDK
|
|
539
|
+
|
|
540
|
+
Add an agent in ~40 lines — implement the contract, register the kind:
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
import { AdapterBase, registerAgentKind, type SendInput } from "threadloom/sdk";
|
|
544
|
+
|
|
545
|
+
class MyAgentAdapter extends AdapterBase {
|
|
546
|
+
async available() { return true; }
|
|
547
|
+
async start() {}
|
|
548
|
+
async stop() {}
|
|
549
|
+
async send(input: SendInput) {
|
|
550
|
+
this._busy = true;
|
|
551
|
+
try {
|
|
552
|
+
// …drive your agent; stream progress:
|
|
553
|
+
this.emit({ kind: "message", payload: { text: "done!" } });
|
|
554
|
+
this.emit({ kind: "run_complete", payload: {} });
|
|
555
|
+
} finally { this._busy = false; }
|
|
556
|
+
}
|
|
557
|
+
async interrupt() {}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
registerAgentKind("my-agent", (cfg, dir) => new MyAgentAdapter(cfg.id, "my-agent", dir));
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Full guide: [docs/adapters.md](docs/adapters.md). Design rationale and every decision
|
|
564
|
+
with its why: [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
565
|
+
|
|
566
|
+
## Configuration
|
|
567
|
+
|
|
568
|
+
`.loom/config.json` (created by `loom init`, hot-reloaded on edit):
|
|
569
|
+
|
|
570
|
+
```json
|
|
571
|
+
{
|
|
572
|
+
"name": "my-project",
|
|
573
|
+
"agents": [
|
|
574
|
+
{ "id": "claude-code", "kind": "claude-code", "role": "planner" },
|
|
575
|
+
{ "id": "opencode", "kind": "opencode", "role": "executor",
|
|
576
|
+
"options": {} },
|
|
577
|
+
{ "id": "antigravity", "kind": "antigravity", "role": "general",
|
|
578
|
+
"options": { "debugPort": 9222 } }
|
|
579
|
+
],
|
|
580
|
+
"defaultAgent": "claude-code",
|
|
581
|
+
"routes": { "ship": ["planner", "executor", "planner"] }
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Roles: `planner` · `executor` · `reviewer` · `general`. Claude Code options:
|
|
586
|
+
`permissionMode` (default `acceptEdits`), `model`. OpenCode options:
|
|
587
|
+
`model` (`"providerID/modelID"`, e.g. `"opencode/minimax-m2.5"` — **set this**: headless
|
|
588
|
+
sessions don't inherit your TUI default), `agent`, `baseUrl` to reuse a running server.
|
|
589
|
+
|
|
590
|
+
## Development
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
npm test # 179 tests: unit + full HTTP/WS end-to-end
|
|
594
|
+
npm run build # tsc → dist/
|
|
595
|
+
npm run dev # run the CLI from source (tsx)
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
## Environment
|
|
599
|
+
|
|
600
|
+
| Variable | What it does |
|
|
601
|
+
|---|---|
|
|
602
|
+
| `LOOM_HOME` | Where the registry, daemon config, and pair tokens live. Default `~/.loom`. Point it at a temp dir to try Loom without touching real state. |
|
|
603
|
+
| `LOOM_STORE` | `jsonl` forces the portable event store instead of `node:sqlite`. Loom falls back on its own if sqlite is unavailable; this makes it explicit. |
|
|
604
|
+
| `LOOM_NO_PTY` | `1` forces the pipe-backed shell instead of a real pty. CI runs the suite both ways. |
|
|
605
|
+
| `LOOM_NODE` | Node binary the desktop shell spawns the daemon with (Electron's own Node predates `node:sqlite`). |
|
|
606
|
+
| `LOOM_NO_NOTIFY` | `1` silences desktop notifications. |
|
|
607
|
+
| `LOOM_NO_PUSH` | `1` silences phone push. |
|
|
608
|
+
| `LOOM_ROUTE_STEP_TIMEOUT_MS` | Per-hop route timeout. Default 45 min. |
|
|
609
|
+
|
|
610
|
+
Going the other way, Loom **sets `LOOM_TERMINAL=1`** inside every terminal it opens, so
|
|
611
|
+
your shell profile can tell it's running in Loom's pane. (`LOOM_EXPO_PUSH_URL` and
|
|
612
|
+
`LOOM_TUI_SMOKE` also exist, but they're test plumbing — not configuration.)
|
|
613
|
+
|
|
614
|
+
## Roadmap
|
|
615
|
+
|
|
616
|
+
- Tasks beyond GitHub — GitLab and Linear sit disabled in the provider row today.
|
|
617
|
+
- More adapters/bridges via the SDK — contributions welcome.
|
|
618
|
+
|
|
619
|
+
## Design
|
|
620
|
+
|
|
621
|
+
Every Loom surface (web app, desktop shell, phone app) wears one design system —
|
|
622
|
+
**quiet graphite**: neutral monochrome chrome, hairline borders, Geist type, and
|
|
623
|
+
color reserved for state (thread cyan = live, shuttle magenta = the baton).
|
|
624
|
+
Adapted from the [Orca](https://github.com/stablyai/orca) design system (MIT,
|
|
625
|
+
© Lovecast Inc.); the Geist typeface is © Vercel under the SIL Open Font
|
|
626
|
+
License 1.1. Tokens and rules: [docs/design-system.md](docs/design-system.md).
|
|
627
|
+
|
|
628
|
+
## License
|
|
629
|
+
|
|
630
|
+
MIT © Nivesh Gajengi
|