@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.
Files changed (177) hide show
  1. package/ARCHITECTURE.md +211 -0
  2. package/CHANGELOG.md +470 -0
  3. package/LICENSE +21 -0
  4. package/README.md +630 -0
  5. package/dist/adapters/base.d.ts +71 -0
  6. package/dist/adapters/base.js +187 -0
  7. package/dist/adapters/base.js.map +1 -0
  8. package/dist/adapters/bridges/antigravity.d.ts +53 -0
  9. package/dist/adapters/bridges/antigravity.js +106 -0
  10. package/dist/adapters/bridges/antigravity.js.map +1 -0
  11. package/dist/adapters/bridges/cdp.d.ts +82 -0
  12. package/dist/adapters/bridges/cdp.js +222 -0
  13. package/dist/adapters/bridges/cdp.js.map +1 -0
  14. package/dist/adapters/bridges/gui-chat.d.ts +113 -0
  15. package/dist/adapters/bridges/gui-chat.js +498 -0
  16. package/dist/adapters/bridges/gui-chat.js.map +1 -0
  17. package/dist/adapters/bridges/kiro.d.ts +19 -0
  18. package/dist/adapters/bridges/kiro.js +22 -0
  19. package/dist/adapters/bridges/kiro.js.map +1 -0
  20. package/dist/adapters/bridges/profiles.d.ts +82 -0
  21. package/dist/adapters/bridges/profiles.js +125 -0
  22. package/dist/adapters/bridges/profiles.js.map +1 -0
  23. package/dist/adapters/claude-code.d.ts +26 -0
  24. package/dist/adapters/claude-code.js +209 -0
  25. package/dist/adapters/claude-code.js.map +1 -0
  26. package/dist/adapters/codex.d.ts +51 -0
  27. package/dist/adapters/codex.js +291 -0
  28. package/dist/adapters/codex.js.map +1 -0
  29. package/dist/adapters/echo.d.ts +20 -0
  30. package/dist/adapters/echo.js +79 -0
  31. package/dist/adapters/echo.js.map +1 -0
  32. package/dist/adapters/grok.d.ts +61 -0
  33. package/dist/adapters/grok.js +217 -0
  34. package/dist/adapters/grok.js.map +1 -0
  35. package/dist/adapters/index.d.ts +9 -0
  36. package/dist/adapters/index.js +33 -0
  37. package/dist/adapters/index.js.map +1 -0
  38. package/dist/adapters/opencode.d.ts +64 -0
  39. package/dist/adapters/opencode.js +426 -0
  40. package/dist/adapters/opencode.js.map +1 -0
  41. package/dist/cli/common.d.ts +11 -0
  42. package/dist/cli/common.js +40 -0
  43. package/dist/cli/common.js.map +1 -0
  44. package/dist/cli/doctor.d.ts +12 -0
  45. package/dist/cli/doctor.js +181 -0
  46. package/dist/cli/doctor.js.map +1 -0
  47. package/dist/cli/index.d.ts +8 -0
  48. package/dist/cli/index.js +641 -0
  49. package/dist/cli/index.js.map +1 -0
  50. package/dist/cli/tui-model.d.ts +73 -0
  51. package/dist/cli/tui-model.js +312 -0
  52. package/dist/cli/tui-model.js.map +1 -0
  53. package/dist/cli/tui.d.ts +15 -0
  54. package/dist/cli/tui.js +451 -0
  55. package/dist/cli/tui.js.map +1 -0
  56. package/dist/cli/ui.d.ts +10 -0
  57. package/dist/cli/ui.js +135 -0
  58. package/dist/cli/ui.js.map +1 -0
  59. package/dist/core/ades.d.ts +79 -0
  60. package/dist/core/ades.js +130 -0
  61. package/dist/core/ades.js.map +1 -0
  62. package/dist/core/baton.d.ts +37 -0
  63. package/dist/core/baton.js +92 -0
  64. package/dist/core/baton.js.map +1 -0
  65. package/dist/core/brain-extract.d.ts +111 -0
  66. package/dist/core/brain-extract.js +248 -0
  67. package/dist/core/brain-extract.js.map +1 -0
  68. package/dist/core/brain-index.d.ts +128 -0
  69. package/dist/core/brain-index.js +338 -0
  70. package/dist/core/brain-index.js.map +1 -0
  71. package/dist/core/brain.d.ts +214 -0
  72. package/dist/core/brain.js +425 -0
  73. package/dist/core/brain.js.map +1 -0
  74. package/dist/core/claude-cli.d.ts +12 -0
  75. package/dist/core/claude-cli.js +47 -0
  76. package/dist/core/claude-cli.js.map +1 -0
  77. package/dist/core/distill.d.ts +19 -0
  78. package/dist/core/distill.js +88 -0
  79. package/dist/core/distill.js.map +1 -0
  80. package/dist/core/eventlog.d.ts +43 -0
  81. package/dist/core/eventlog.js +211 -0
  82. package/dist/core/eventlog.js.map +1 -0
  83. package/dist/core/git.d.ts +203 -0
  84. package/dist/core/git.js +460 -0
  85. package/dist/core/git.js.map +1 -0
  86. package/dist/core/logbook.d.ts +59 -0
  87. package/dist/core/logbook.js +118 -0
  88. package/dist/core/logbook.js.map +1 -0
  89. package/dist/core/memory.d.ts +34 -0
  90. package/dist/core/memory.js +124 -0
  91. package/dist/core/memory.js.map +1 -0
  92. package/dist/core/notify.d.ts +11 -0
  93. package/dist/core/notify.js +30 -0
  94. package/dist/core/notify.js.map +1 -0
  95. package/dist/core/projection.d.ts +20 -0
  96. package/dist/core/projection.js +96 -0
  97. package/dist/core/projection.js.map +1 -0
  98. package/dist/core/registry.d.ts +76 -0
  99. package/dist/core/registry.js +147 -0
  100. package/dist/core/registry.js.map +1 -0
  101. package/dist/core/router.d.ts +42 -0
  102. package/dist/core/router.js +99 -0
  103. package/dist/core/router.js.map +1 -0
  104. package/dist/core/routes.d.ts +93 -0
  105. package/dist/core/routes.js +399 -0
  106. package/dist/core/routes.js.map +1 -0
  107. package/dist/core/search.d.ts +63 -0
  108. package/dist/core/search.js +0 -0
  109. package/dist/core/search.js.map +1 -0
  110. package/dist/core/setup.d.ts +70 -0
  111. package/dist/core/setup.js +250 -0
  112. package/dist/core/setup.js.map +1 -0
  113. package/dist/core/suggestions.d.ts +15 -0
  114. package/dist/core/suggestions.js +38 -0
  115. package/dist/core/suggestions.js.map +1 -0
  116. package/dist/core/worktree.d.ts +32 -0
  117. package/dist/core/worktree.js +83 -0
  118. package/dist/core/worktree.js.map +1 -0
  119. package/dist/daemon/app-page.d.ts +32 -0
  120. package/dist/daemon/app-page.js +6363 -0
  121. package/dist/daemon/app-page.js.map +1 -0
  122. package/dist/daemon/auth.d.ts +51 -0
  123. package/dist/daemon/auth.js +135 -0
  124. package/dist/daemon/auth.js.map +1 -0
  125. package/dist/daemon/board.d.ts +117 -0
  126. package/dist/daemon/board.js +202 -0
  127. package/dist/daemon/board.js.map +1 -0
  128. package/dist/daemon/brand-icons.d.ts +29 -0
  129. package/dist/daemon/brand-icons.js +44 -0
  130. package/dist/daemon/brand-icons.js.map +1 -0
  131. package/dist/daemon/client.d.ts +127 -0
  132. package/dist/daemon/client.js +237 -0
  133. package/dist/daemon/client.js.map +1 -0
  134. package/dist/daemon/geist-font.d.ts +10 -0
  135. package/dist/daemon/geist-font.js +783 -0
  136. package/dist/daemon/geist-font.js.map +1 -0
  137. package/dist/daemon/guards.d.ts +18 -0
  138. package/dist/daemon/guards.js +40 -0
  139. package/dist/daemon/guards.js.map +1 -0
  140. package/dist/daemon/linear.d.ts +59 -0
  141. package/dist/daemon/linear.js +117 -0
  142. package/dist/daemon/linear.js.map +1 -0
  143. package/dist/daemon/push.d.ts +22 -0
  144. package/dist/daemon/push.js +58 -0
  145. package/dist/daemon/push.js.map +1 -0
  146. package/dist/daemon/runtime.d.ts +299 -0
  147. package/dist/daemon/runtime.js +1016 -0
  148. package/dist/daemon/runtime.js.map +1 -0
  149. package/dist/daemon/server.d.ts +125 -0
  150. package/dist/daemon/server.js +1728 -0
  151. package/dist/daemon/server.js.map +1 -0
  152. package/dist/daemon/tasks.d.ts +153 -0
  153. package/dist/daemon/tasks.js +264 -0
  154. package/dist/daemon/tasks.js.map +1 -0
  155. package/dist/daemon/terminals.d.ts +82 -0
  156. package/dist/daemon/terminals.js +372 -0
  157. package/dist/daemon/terminals.js.map +1 -0
  158. package/dist/index.d.ts +20 -0
  159. package/dist/index.js +20 -0
  160. package/dist/index.js.map +1 -0
  161. package/dist/sdk.d.ts +17 -0
  162. package/dist/sdk.js +17 -0
  163. package/dist/sdk.js.map +1 -0
  164. package/dist/types.d.ts +236 -0
  165. package/dist/types.js +18 -0
  166. package/dist/types.js.map +1 -0
  167. package/docs/SETUP.md +265 -0
  168. package/docs/adapters.md +132 -0
  169. package/docs/design-system.md +245 -0
  170. package/docs/img/board-linear.png +0 -0
  171. package/docs/img/board-projects.png +0 -0
  172. package/docs/img/brain.png +0 -0
  173. package/docs/img/settings.png +0 -0
  174. package/docs/img/workspace.png +0 -0
  175. package/docs/integration-notes.md +76 -0
  176. package/docs/proposals/brain.md +262 -0
  177. package/package.json +84 -0
package/README.md ADDED
@@ -0,0 +1,630 @@
1
+ # Loom
2
+
3
+ [![ci](https://github.com/nickthelegend/loom/actions/workflows/ci.yml/badge.svg)](https://github.com/nickthelegend/loom/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/threadloom)](https://www.npmjs.com/package/threadloom)
5
+ [![license](https://img.shields.io/badge/license-MIT-blue)](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