@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
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Loom — shared types.
3
+ *
4
+ * The event log is the source of truth; everything else (agent memory,
5
+ * board views, phone surfaces) is a projection of it.
6
+ */
7
+ export type EventKind = "message" | "tool_call" | "tool_result" | "file_edit" | "decision" | "handoff" | "suggestion" | "needs_input" | "run_complete" | "agent_join" | "agent_leave" | "role_change" | "route_started" | "route_step" | "route_paused" | "route_resumed" | "route_completed" | "route_failed" | "turn_diff" | "memory_import" | "memory_add" | "memory_update" | "memory_forget" | "status" | "error";
8
+ /**
9
+ * The chat every event belongs to. A project can hold several conversations;
10
+ * they share one brain, one baton and one working tree — only the talking is
11
+ * split. Events written before chats existed carry no id and read as MAIN.
12
+ */
13
+ export declare const MAIN_CHAT = "main";
14
+ export interface LoomEvent {
15
+ id: number;
16
+ ts: number;
17
+ kind: EventKind;
18
+ /** Author agent id; absent for human/system events. */
19
+ agentId?: string;
20
+ /** Conversation this belongs to. Absent means the main chat. */
21
+ chat?: string;
22
+ payload: Record<string, unknown>;
23
+ }
24
+ export type NewEvent = {
25
+ kind: EventKind;
26
+ agentId?: string;
27
+ chat?: string;
28
+ payload: Record<string, unknown>;
29
+ ts?: number;
30
+ };
31
+ /** A named conversation inside a project. */
32
+ export interface ChatInfo {
33
+ id: string;
34
+ title: string;
35
+ createdAt: number;
36
+ }
37
+ /**
38
+ * An agent's job, in your words. Free text: call an agent "architect",
39
+ * "tester", "the one that writes docs" — whatever your project actually does.
40
+ *
41
+ * A few names carry extra meaning if you use them, and none if you don't:
42
+ * `buildDefaultRoutes` seeds a plan→execute→review pipeline when it sees
43
+ * planner/executor/reviewer, the rules router prefers a reviewer last, and a
44
+ * route step matches an agent by role as well as by id (so a route named
45
+ * ["architect","tester"] just works). Nothing requires them.
46
+ */
47
+ export type AgentRole = string;
48
+ /** Roles Loom suggests when it sets a project up. Only suggestions. */
49
+ export declare const SUGGESTED_ROLES: readonly ["planner", "executor", "reviewer", "general"];
50
+ /** Adapter = full-duplex; bridge = read-mostly, never holds the baton. */
51
+ export type AgentTier = "adapter" | "bridge";
52
+ export interface AgentConfig {
53
+ /** Stable instance id within the project, e.g. "claude-code". */
54
+ id: string;
55
+ /** Adapter kind, e.g. "claude-code" | "opencode" | "echo" | "antigravity". */
56
+ kind: string;
57
+ role: AgentRole;
58
+ options?: Record<string, unknown>;
59
+ /** Override the ADE's native memory files to import into the shared brain. */
60
+ memoryFiles?: string[];
61
+ }
62
+ /** One ADE's native memory pulled into the unified brain. */
63
+ export interface MemorySource {
64
+ agentId: string;
65
+ kind: string;
66
+ file: string;
67
+ chars: number;
68
+ }
69
+ export interface UnifiedMemory {
70
+ projectName: string;
71
+ decisions: string[];
72
+ sources: MemorySource[];
73
+ /** The merged brain document (decisions + imported ADE memories + context). */
74
+ document: string;
75
+ }
76
+ /**
77
+ * A route step: which agent (by id or role), and what job it does.
78
+ *
79
+ * `role` is the job for THIS step — planner/executor/reviewer/… — chosen when
80
+ * the task is created, independent of the agent's own default role. So the same
81
+ * agent can plan in one task and review in another. `instruction` is optional
82
+ * free-text focus on top of the role.
83
+ */
84
+ export type RouteStepSpec = string | {
85
+ step: string;
86
+ role?: string;
87
+ instruction?: string;
88
+ };
89
+ /** How shared memory is rendered on handoff. */
90
+ export interface ProjectionConfig {
91
+ /** "template" (default, free, instant) or "llm" (distilled by a small Claude). */
92
+ mode?: "template" | "llm";
93
+ model?: string;
94
+ timeoutMs?: number;
95
+ }
96
+ /** The brain's phase-2 extractor — learning from each turn. See core/brain-extract.ts. */
97
+ export interface BrainConfig {
98
+ /**
99
+ * Who reads finished turns for memory:
100
+ * "auto" (default) — a small Claude, when the CLI is available; a no-op when not.
101
+ * "off" — never extract; the brain only holds what you type.
102
+ */
103
+ extractor?: "auto" | "off";
104
+ /** Model for the extractor call; small+fast is right. */
105
+ model?: string;
106
+ }
107
+ export interface ProjectConfig {
108
+ name: string;
109
+ agents: AgentConfig[];
110
+ /** Agent that receives messages when nobody holds the baton. */
111
+ defaultAgent?: string;
112
+ /** Named multi-hop pipelines; steps are agent ids/roles, optionally with instructions. */
113
+ routes?: Record<string, RouteStepSpec[]>;
114
+ projection?: ProjectionConfig;
115
+ brain?: BrainConfig;
116
+ }
117
+ export type RouteStatus = "running" | "waiting_human" | "completed" | "failed" | "aborted";
118
+ export type RouteMode = "static" | "dynamic";
119
+ export type RouterKind = "rules" | "llm";
120
+ export interface RouteState {
121
+ id: string;
122
+ name?: string;
123
+ task: string;
124
+ /** Resolved agent ids, in order. Dynamic routes grow this per hop. */
125
+ steps: string[];
126
+ stepRoles: AgentRole[];
127
+ /** Optional per-step focus text (parallel to steps; null = role default only). */
128
+ stepInstructions?: Array<string | null>;
129
+ /** Index of the step currently executing (or last executed). */
130
+ current: number;
131
+ status: RouteStatus;
132
+ /** static = fixed pipeline; dynamic = a router picks each next hop. */
133
+ mode: RouteMode;
134
+ router?: RouterKind;
135
+ maxHops?: number;
136
+ startedAt: number;
137
+ updatedAt: number;
138
+ reason?: string;
139
+ pendingQuestion?: string;
140
+ /** Project cost total when the route started (internal baseline). */
141
+ costStartUsd?: number;
142
+ /** Spend attributed to this route (set when it ends). */
143
+ costUsd?: number;
144
+ }
145
+ export interface ProjectInfo {
146
+ id: string;
147
+ name: string;
148
+ dir: string;
149
+ }
150
+ export interface SendInput {
151
+ text: string;
152
+ /** One-shot handoff briefing injected alongside this turn. */
153
+ briefing?: string;
154
+ }
155
+ export interface AdapterEvent {
156
+ kind: EventKind;
157
+ payload: Record<string, unknown>;
158
+ }
159
+ export interface AgentCapabilities {
160
+ tier: AgentTier;
161
+ send: boolean;
162
+ stream: boolean;
163
+ injectMemory: boolean;
164
+ interrupt: boolean;
165
+ diff: boolean;
166
+ }
167
+ export interface BaseAgent {
168
+ readonly id: string;
169
+ readonly kind: string;
170
+ readonly capabilities: AgentCapabilities;
171
+ /** Is the underlying tool installed/reachable? */
172
+ available(): Promise<boolean>;
173
+ start(): Promise<void>;
174
+ stop(): Promise<void>;
175
+ /** Persist Loom's namespaced memory projection for this agent. */
176
+ injectMemory(projection: string): Promise<void>;
177
+ /** Subscribe to live events; returns unsubscribe. */
178
+ onEvent(cb: (e: AdapterEvent) => void): () => void;
179
+ }
180
+ /** Full-duplex adapter — may hold the baton. */
181
+ export interface Adapter extends BaseAgent {
182
+ send(input: SendInput): Promise<void>;
183
+ interrupt(): Promise<void>;
184
+ /** Working-tree changes attributable to the agent (porcelain-ish). */
185
+ diff(): Promise<string>;
186
+ busy(): boolean;
187
+ }
188
+ /** Read-mostly bridge — never holds the baton (GUI agents). */
189
+ export interface Bridge extends BaseAgent {
190
+ }
191
+ export type AnyAgent = Adapter | Bridge;
192
+ export declare function isAdapter(a: AnyAgent): a is Adapter;
193
+ export interface AgentStatus {
194
+ id: string;
195
+ kind: string;
196
+ role: AgentRole;
197
+ tier: AgentTier;
198
+ available: boolean;
199
+ busy: boolean;
200
+ holdsBaton: boolean;
201
+ /** The model override in effect, or "" for the CLI's own default. */
202
+ model: string;
203
+ }
204
+ export interface ProjectStatus {
205
+ id: string;
206
+ name: string;
207
+ dir: string;
208
+ holder: string | null;
209
+ agents: AgentStatus[];
210
+ lastEvent: LoomEvent | null;
211
+ needsInput: boolean;
212
+ /**
213
+ * The agent whose question is still unanswered, when `needsInput` — the
214
+ * board puts a name on the card, not just "someone is blocked".
215
+ */
216
+ blockedAgent?: string | null;
217
+ /** Conversations in this project, main first. */
218
+ chats?: ChatInfo[];
219
+ route?: RouteState | null;
220
+ /** Named pipelines defined in config (for pickers/dropdowns). */
221
+ routeNames?: string[];
222
+ /** Lifetime spend across all agents in this project (USD). */
223
+ costUsd?: number;
224
+ }
225
+ export interface AgentCost {
226
+ agentId: string;
227
+ usd: number;
228
+ turns: number;
229
+ ms: number;
230
+ }
231
+ export interface CostSummary {
232
+ totalUsd: number;
233
+ turns: number;
234
+ totalMs: number;
235
+ byAgent: AgentCost[];
236
+ }
package/dist/types.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Loom — shared types.
3
+ *
4
+ * The event log is the source of truth; everything else (agent memory,
5
+ * board views, phone surfaces) is a projection of it.
6
+ */
7
+ /**
8
+ * The chat every event belongs to. A project can hold several conversations;
9
+ * they share one brain, one baton and one working tree — only the talking is
10
+ * split. Events written before chats existed carry no id and read as MAIN.
11
+ */
12
+ export const MAIN_CHAT = "main";
13
+ /** Roles Loom suggests when it sets a project up. Only suggestions. */
14
+ export const SUGGESTED_ROLES = ["planner", "executor", "reviewer", "general"];
15
+ export function isAdapter(a) {
16
+ return a.capabilities.tier === "adapter";
17
+ }
18
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoCH;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AA4ChC,uEAAuE;AACvE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,CAAU,CAAC;AAsKvF,MAAM,UAAU,SAAS,CAAC,CAAW;IACnC,OAAO,CAAC,CAAC,YAAY,CAAC,IAAI,KAAK,SAAS,CAAC;AAC3C,CAAC"}
package/docs/SETUP.md ADDED
@@ -0,0 +1,265 @@
1
+ # Setting up Loom
2
+
3
+ Read this before installing. Loom drives other people's coding agents, so most
4
+ of the setup is really *their* setup — and the parts that bite are the ones
5
+ nobody warns you about.
6
+
7
+ Everything here was hit for real on a machine, not copied from a wiki. Where
8
+ something is a guess, it says so.
9
+
10
+ ---
11
+
12
+ ## 1. The one hard requirement: Node ≥ 22.5
13
+
14
+ ```sh
15
+ node --version
16
+ ```
17
+
18
+ Loom's event log uses `node:sqlite`, which arrived in Node 22.5. On anything
19
+ older Loom still runs and **silently falls back to a JSONL store with no
20
+ history** — the app looks fine and your past turns aren't there.
21
+
22
+ > **The installed desktop app has this problem too.** Electron 33 bundles Node
23
+ > 20, so `Loom.app` looks for a real `node` on your machine (`$LOOM_NODE`, then
24
+ > the usual install paths, then `PATH`). If it can't find one, it runs on
25
+ > Electron's Node and degrades exactly as above. There is no warning yet. See
26
+ > `desktop/BUILD.md`.
27
+
28
+ - **macOS**: `brew install node`
29
+ - **Windows**: `winget install OpenJS.NodeJS`
30
+ - **Linux**: your distro's node, or [nodesource](https://github.com/nodesource/distributions)
31
+
32
+ ---
33
+
34
+ ## 2. Install at least one agent
35
+
36
+ Loom has nothing to drive on its own. A new project is given whichever of these
37
+ it finds — and **an empty roster if it finds none**, which is honest but not
38
+ useful.
39
+
40
+ | Agent | Install | Sign in | Check |
41
+ |---|---|---|---|
42
+ | **Claude Code** | `npm i -g @anthropic-ai/claude-code` | `claude` (once, interactively) | `claude --version` |
43
+ | **Codex** | [Codex.app](https://openai.com/codex/), or `npm i -g @openai/codex` | `codex login` | `codex login status` |
44
+ | **OpenCode** | `curl -fsSL https://opencode.ai/install \| bash` | `opencode auth login` | `opencode --version` |
45
+ | **Grok Code** | [docs.x.ai](https://docs.x.ai) | `grok` (once, interactively) | `grok --version` |
46
+
47
+ Two things worth knowing:
48
+
49
+ **Codex's CLI hides inside its app.** On a Mac with Codex.app installed and
50
+ nothing on `PATH`, the binary is at
51
+ `/Applications/Codex.app/Contents/Resources/codex`. Loom looks there as well as
52
+ on `PATH`, so you don't have to do anything — but that's why `which codex`
53
+ coming up empty doesn't mean it's missing.
54
+
55
+ **Being logged in is not optional and not detectable in advance.** Loom probes
56
+ whether a CLI *exists*, not whether it's authenticated. An unauthenticated agent
57
+ looks installed, takes your turn, and fails. Run each one once by hand first.
58
+
59
+ Then ask Loom what it can see:
60
+
61
+ ```sh
62
+ loom doctor
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 3. GUI agents: Antigravity IDE and Kiro
68
+
69
+ These have no API and no headless mode. Loom drives them through the Chrome
70
+ DevTools port they open *only if you ask*, so they need a launch flag.
71
+
72
+ ### Which Antigravity?
73
+
74
+ There are two apps and only one of them is the right one.
75
+
76
+ | | What it is | Use it? |
77
+ |---|---|---|
78
+ | **Antigravity.app** | the Manager — a web page behind a Google sign-in | ❌ no chat DOM at all |
79
+ | **Antigravity IDE.app** | the VS Code fork with the agent in it | ✅ this one |
80
+
81
+ Pointing Loom at the Manager finds nothing and reports that Antigravity has no
82
+ chat. It's the wrong app, not a broken bridge.
83
+
84
+ ### Why port 9333 and not 9222
85
+
86
+ Antigravity's own Browser Control (the Chrome button in its toolbar) already
87
+ uses **9222**. Ask for it and you get `EADDRINUSE` — and the IDE starts anyway,
88
+ just without a debugger, silently. Use **9333**.
89
+ (Discovered by the [AntiGravity-AutoAccept](https://github.com/yazanbaker94/AntiGravity-AutoAccept)
90
+ project.)
91
+
92
+ ### macOS
93
+
94
+ ```sh
95
+ open -a "Antigravity IDE" --args --remote-debugging-port=9333 /path/to/your/repo
96
+ ```
97
+
98
+ Make it permanent in `~/.zshrc`:
99
+
100
+ ```sh
101
+ alias antigravity='open -a "Antigravity IDE" --args --remote-debugging-port=9333'
102
+ ```
103
+
104
+ If `open -a` doesn't pass the flag through on your build, go direct:
105
+
106
+ ```sh
107
+ alias antigravity='"/Applications/Antigravity IDE.app/Contents/MacOS/Electron" --remote-debugging-port=9333 & disown'
108
+ ```
109
+
110
+ Or make an Automator **Application** with one *Run Shell Script* action
111
+ containing the `open -a` line, and launch that instead of the dock icon.
112
+
113
+ **Only one instance gets the port.** If Antigravity is already running, a second
114
+ launch just focuses the first window and your flag is ignored — the port stays
115
+ closed and nothing says so. Quit it fully first:
116
+
117
+ ```sh
118
+ pkill -f "Antigravity" && sleep 2
119
+ ```
120
+
121
+ ### Windows
122
+
123
+ Right-click the Antigravity IDE shortcut → **Properties** → append to *Target*:
124
+
125
+ ```
126
+ --remote-debugging-port=9333
127
+ ```
128
+
129
+ Launch from that shortcut afterwards, not from search results or a pinned
130
+ taskbar entry pointing elsewhere.
131
+
132
+ ### Linux
133
+
134
+ ```sh
135
+ # find the launcher
136
+ find /usr/share/applications ~/.local/share/applications -name "*ntigravity*"
137
+ # then edit its Exec line:
138
+ Exec=/path/to/antigravity --remote-debugging-port=9333 %F
139
+ ```
140
+
141
+ or an alias in `~/.bashrc`:
142
+
143
+ ```sh
144
+ alias antigravity='antigravity --remote-debugging-port=9333'
145
+ ```
146
+
147
+ ### Kiro
148
+
149
+ Same idea, **port 9334** so it can't collide with Antigravity:
150
+
151
+ ```sh
152
+ open -a "Kiro" --args --remote-debugging-port=9334 # macOS
153
+ ```
154
+
155
+ Kiro's chat panel must be **open** — on a fresh window its only editable area is
156
+ Monaco, holding your source file, and Loom refuses to type into that on purpose.
157
+
158
+ ### Then check it
159
+
160
+ ```sh
161
+ loom doctor
162
+ ```
163
+
164
+ Loom distinguishes *reachable* from *driveable*, because a signed-out app
165
+ answers the debugger cheerfully and has no usable chat:
166
+
167
+ - `Antigravity IDE isn't listening — open -a "Antigravity IDE" --args --remote-debugging-port=9333` → the flag didn't take
168
+ - `Antigravity IDE is signed out — log in from its window` → the app is up; **log into it**
169
+ - `no chat panel — open a chat` → it's up and signed in; open a conversation
170
+
171
+ ---
172
+
173
+ ## 4. Permissions
174
+
175
+ ### macOS
176
+
177
+ | What | Needed for | How |
178
+ |---|---|---|
179
+ | **Notifications** | "your agent needs you" while you're elsewhere | First `loom up` asks. Otherwise: System Settings → Notifications → Loom |
180
+ | **Open a downloaded app** | Loom.app isn't notarized yet | **Right-click → Open** the first time. Double-clicking gives "cannot be opened" with no way forward |
181
+ | **Local Network** | your phone reaching the daemon over LAN | Prompted on first connection. System Settings → Privacy & Security → Local Network |
182
+
183
+ **Not needed, despite what you might expect:** Accessibility, Screen Recording,
184
+ Automation, Full Disk Access. Loom drives GUI agents through their debugging
185
+ port, not by pretending to be a mouse, so it never asks the OS for control of
186
+ another app. If something tells you to grant Accessibility to Loom, that isn't
187
+ Loom.
188
+
189
+ ### Windows
190
+
191
+ | What | Needed for | How |
192
+ |---|---|---|
193
+ | **Firewall (private networks)** | your phone reaching the daemon | Windows Defender prompts on first `loom up` — tick **Private networks**. Public: leave off |
194
+ | **SmartScreen** | the installer isn't signed yet | **More info → Run anyway** |
195
+ | **Notifications** | agent alerts | Settings → System → Notifications → Loom |
196
+
197
+ ---
198
+
199
+ ## 5. Staying open, and staying running
200
+
201
+ **The daemon already outlives the window.** It's spawned detached — close the
202
+ app and your agents keep working; reopen and you're back where you were. That's
203
+ the design, not a leak. To stop it for real:
204
+
205
+ ```sh
206
+ loom down
207
+ ```
208
+
209
+ **Run it at login (optional).** No installer wires this up yet; do it yourself:
210
+
211
+ - **macOS** — System Settings → General → Login Items → **+** → Loom.app. Or a
212
+ LaunchAgent running `loom up`.
213
+ - **Windows** — `Win+R` → `shell:startup` → shortcut to Loom.
214
+ - **Linux** — a user systemd unit running `loom up`.
215
+
216
+ **Always on top** is the window manager's job, not Loom's: right-click the title
217
+ bar (Windows), or use a tiling/stage manager (macOS). Loom doesn't ask for it —
218
+ a tool that forces itself in front of your editor is a tool you'll uninstall.
219
+
220
+ ---
221
+
222
+ ## 6. Your phone
223
+
224
+ The phone app talks to the daemon on your own machine. Nothing goes through a
225
+ server of ours, because there isn't one.
226
+
227
+ 1. **Install Tailscale** on both, sign into the same tailnet: [tailscale.com/download](https://tailscale.com/download)
228
+ 2. **Bind Loom to the tailnet** — by default it listens on localhost only and your phone cannot see it:
229
+ ```sh
230
+ loom up --restart --tailnet
231
+ ```
232
+ 3. **Pair**: `loom pair` prints a QR code. Scan it.
233
+
234
+ Two things that will confuse you otherwise:
235
+
236
+ - `loom doctor` saying *"binding localhost only — phones can't reach it"* is the
237
+ whole answer. Re-run with `--tailnet`.
238
+ - The pairing token is single-use and the client token it hands back lives on the
239
+ phone. Losing the phone means `loom` → unpair that client, not rotating
240
+ everything.
241
+
242
+ ---
243
+
244
+ ## 7. When it's wrong
245
+
246
+ ```sh
247
+ loom doctor # what Loom can see, and what it can't
248
+ tail -f ~/.loom/daemon.log
249
+ ```
250
+
251
+ **A change you made isn't showing up.** The daemon is a long-lived process; it
252
+ serves the code it started with. `loom up` compares fingerprints and restarts a
253
+ stale one automatically, but if you're staring at yesterday's behaviour:
254
+
255
+ ```sh
256
+ loom down && loom up
257
+ ```
258
+
259
+ **"daemon already running" but it's on the wrong port.** Something else has
260
+ 7420. `lsof -ti tcp:7420` (macOS/Linux) or `netstat -ano | findstr 7420`
261
+ (Windows).
262
+
263
+ **An agent takes the turn and nothing happens.** It's almost always
264
+ authentication. Run that agent by hand in a terminal once — it'll tell you what
265
+ Loom can't.
@@ -0,0 +1,132 @@
1
+ # Writing a Loom adapter or bridge
2
+
3
+ Loom's integration model has **two tiers**, and picking the right one is the first
4
+ decision:
5
+
6
+ | | Adapter | Bridge |
7
+ |---|---|---|
8
+ | Can hold the baton (write lock) | ✅ | ❌ never |
9
+ | Must implement | `send`, live events, `injectMemory`, `interrupt`, `diff` | events (best-effort), `injectMemory` |
10
+ | For | agents with a real API / headless mode | GUI agents without a stable control surface |
11
+ | Examples | Claude Code, OpenCode | Antigravity |
12
+
13
+ This split is deliberate (see [ARCHITECTURE.md](../ARCHITECTURE.md)): the baton
14
+ authorizes *edits to the user's working tree*, so it can only be held by an agent Loom
15
+ can reliably start, stream, interrupt, and account for. If your target tool only exposes
16
+ a debug port and pixels, build a bridge; if it exposes an API, build an adapter. A
17
+ bridge can graduate later.
18
+
19
+ ## The contract
20
+
21
+ Everything lives in `threadloom/sdk`:
22
+
23
+ ```ts
24
+ import {
25
+ AdapterBase, // extend for adapters
26
+ BridgeBase, // extend for bridges
27
+ registerAgentKind,
28
+ type SendInput, // { text, briefing? }
29
+ type AdapterEvent // { kind, payload }
30
+ } from "threadloom/sdk";
31
+ ```
32
+
33
+ ### Events you can emit
34
+
35
+ | kind | payload | when |
36
+ |---|---|---|
37
+ | `message` | `{ text }` | your agent produced prose |
38
+ | `tool_call` | `{ tool, summary }` | it used a tool |
39
+ | `file_edit` | `{ path }` | it changed a file |
40
+ | `needs_input` | `{ question }` | it is blocked on the human → triggers a notification |
41
+ | `run_complete` | `{ durationMs?, costUsd? }` | the turn finished → notification |
42
+ | `status` | `{ state, … }` | lifecycle (ready / interrupted / …) |
43
+ | `error` | `{ message }` | something broke |
44
+
45
+ Every event you emit is appended to the project's event log — which is exactly what gets
46
+ projected into the *next* agent's memory on handoff. Emit honestly and your agent's work
47
+ becomes context for everyone else.
48
+
49
+ ### A complete adapter
50
+
51
+ ```ts
52
+ import { AdapterBase, registerAgentKind, type SendInput } from "threadloom/sdk";
53
+
54
+ export class MyAgentAdapter extends AdapterBase {
55
+ // available(): is the underlying tool installed/reachable?
56
+ async available(): Promise<boolean> {
57
+ return true;
58
+ }
59
+
60
+ async start(): Promise<void> {
61
+ // boot your server / warm your session; emit a status event
62
+ this.emit({ kind: "status", payload: { state: "ready" } });
63
+ }
64
+
65
+ async stop(): Promise<void> {}
66
+
67
+ async send(input: SendInput): Promise<void> {
68
+ if (this._busy) throw new Error(`${this.id} is busy`);
69
+ this._busy = true;
70
+ try {
71
+ // input.briefing is the one-shot handoff context — inject it however
72
+ // your agent accepts system/context input. Claude Code uses
73
+ // --append-system-prompt; OpenCode gets a delimited preamble.
74
+ const reply = await callMyAgent(input.text, input.briefing);
75
+ this.emit({ kind: "message", payload: { text: reply } });
76
+ this.emit({ kind: "run_complete", payload: {} });
77
+ } finally {
78
+ this._busy = false;
79
+ }
80
+ }
81
+
82
+ async interrupt(): Promise<void> {
83
+ // stop the in-flight turn (kill the process, call the abort route, …)
84
+ }
85
+
86
+ // diff() has a default implementation (git status --porcelain).
87
+ // injectMemory() has a default implementation writing the namespaced
88
+ // .loom/memory/<id>.md file — override only if your agent has a richer
89
+ // native memory surface. NEVER write into user-authored files.
90
+ }
91
+
92
+ registerAgentKind("my-agent", (cfg, dir) => new MyAgentAdapter(cfg.id, "my-agent", dir));
93
+ ```
94
+
95
+ Users then declare it in `.loom/config.json`:
96
+
97
+ ```json
98
+ { "id": "my-agent", "kind": "my-agent", "role": "executor", "options": {} }
99
+ ```
100
+
101
+ `cfg.options` arrives in your constructor if you define one — see
102
+ `src/adapters/claude-code.ts` and `src/adapters/opencode.ts` for real examples,
103
+ including session persistence via project state.
104
+
105
+ ### A bridge
106
+
107
+ ```ts
108
+ import { BridgeBase, registerAgentKind } from "threadloom/sdk";
109
+
110
+ export class MyGuiBridge extends BridgeBase {
111
+ async available() { return true; }
112
+ async start() {
113
+ this.emit({ kind: "status", payload: { state: "observed" } });
114
+ }
115
+ async stop() {}
116
+ // injectMemory() default keeps .loom/memory/<id>.md fresh — the human
117
+ // driving the GUI agent points it at that file.
118
+ }
119
+
120
+ registerAgentKind("my-gui", (cfg, dir) => new MyGuiBridge(cfg.id, "my-gui", dir));
121
+ ```
122
+
123
+ ## Ground rules
124
+
125
+ 1. **Never block `send()` on user interaction** — emit `needs_input` and finish the turn.
126
+ 2. **Interrupt must be safe** — a confirmed handoff interrupts you; leave the tree in a
127
+ sane state (that's why baton holders are adapters only).
128
+ 3. **Emit `run_complete` exactly once per turn** — notifications and suggestions key off it.
129
+ 4. **Respect namespacing** — your memory surface is `.loom/memory/<your-id>.md` and
130
+ whatever *additive* mechanism your agent offers. User files are off-limits.
131
+ 5. **Persist session state** via project state (see `readProjectState` /
132
+ `writeProjectState`) so `--resume`-style continuity survives daemon restarts.