@cryptiklemur/lattice 0.0.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 (162) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release.yml +44 -0
  3. package/.impeccable.md +66 -0
  4. package/.releaserc.json +32 -0
  5. package/.serena/project.yml +138 -0
  6. package/CLAUDE.md +35 -0
  7. package/CONTRIBUTING.md +93 -0
  8. package/LICENSE +21 -0
  9. package/README.md +83 -0
  10. package/bun.lock +1459 -0
  11. package/bunfig.toml +2 -0
  12. package/client/index.html +32 -0
  13. package/client/package.json +37 -0
  14. package/client/public/icons/icon-192.svg +11 -0
  15. package/client/public/icons/icon-512.svg +11 -0
  16. package/client/public/manifest.json +24 -0
  17. package/client/public/sw.js +61 -0
  18. package/client/src/App.tsx +28 -0
  19. package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
  20. package/client/src/components/chat/ChatInput.tsx +241 -0
  21. package/client/src/components/chat/ChatView.tsx +727 -0
  22. package/client/src/components/chat/Message.tsx +362 -0
  23. package/client/src/components/chat/ModelSelector.tsx +87 -0
  24. package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
  25. package/client/src/components/chat/StatusBar.tsx +50 -0
  26. package/client/src/components/chat/ToolGroup.tsx +129 -0
  27. package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
  28. package/client/src/components/chat/toolSummary.ts +41 -0
  29. package/client/src/components/dashboard/DashboardView.tsx +219 -0
  30. package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
  31. package/client/src/components/mesh/NodeBadge.tsx +24 -0
  32. package/client/src/components/mesh/PairingDialog.tsx +281 -0
  33. package/client/src/components/panels/FileBrowser.tsx +241 -0
  34. package/client/src/components/panels/StickyNotes.tsx +187 -0
  35. package/client/src/components/panels/Terminal.tsx +128 -0
  36. package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
  37. package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
  38. package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
  39. package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
  40. package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
  41. package/client/src/components/project-settings/ProjectRules.tsx +277 -0
  42. package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
  43. package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
  44. package/client/src/components/settings/Appearance.tsx +151 -0
  45. package/client/src/components/settings/ClaudeSettings.tsx +151 -0
  46. package/client/src/components/settings/Environment.tsx +185 -0
  47. package/client/src/components/settings/GlobalMcp.tsx +207 -0
  48. package/client/src/components/settings/GlobalSkills.tsx +125 -0
  49. package/client/src/components/settings/MeshStatus.tsx +145 -0
  50. package/client/src/components/settings/SettingsView.tsx +57 -0
  51. package/client/src/components/settings/SkillMarketplace.tsx +175 -0
  52. package/client/src/components/settings/mcp-shared.tsx +194 -0
  53. package/client/src/components/settings/skill-shared.tsx +177 -0
  54. package/client/src/components/setup/SetupWizard.tsx +750 -0
  55. package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
  56. package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
  57. package/client/src/components/sidebar/ProjectRail.tsx +291 -0
  58. package/client/src/components/sidebar/SearchFilter.tsx +52 -0
  59. package/client/src/components/sidebar/SessionList.tsx +384 -0
  60. package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
  61. package/client/src/components/sidebar/Sidebar.tsx +209 -0
  62. package/client/src/components/sidebar/UserIsland.tsx +59 -0
  63. package/client/src/components/sidebar/UserMenu.tsx +101 -0
  64. package/client/src/components/ui/CommandPalette.tsx +321 -0
  65. package/client/src/components/ui/ErrorBoundary.tsx +56 -0
  66. package/client/src/components/ui/IconPicker.tsx +209 -0
  67. package/client/src/components/ui/LatticeLogomark.tsx +19 -0
  68. package/client/src/components/ui/PopupMenu.tsx +98 -0
  69. package/client/src/components/ui/SaveFooter.tsx +38 -0
  70. package/client/src/components/ui/Toast.tsx +112 -0
  71. package/client/src/hooks/useMesh.ts +89 -0
  72. package/client/src/hooks/useProjectSettings.ts +56 -0
  73. package/client/src/hooks/useProjects.ts +66 -0
  74. package/client/src/hooks/useSaveState.ts +59 -0
  75. package/client/src/hooks/useSession.ts +317 -0
  76. package/client/src/hooks/useSidebar.ts +74 -0
  77. package/client/src/hooks/useSkills.ts +30 -0
  78. package/client/src/hooks/useTheme.ts +114 -0
  79. package/client/src/hooks/useWebSocket.ts +26 -0
  80. package/client/src/main.tsx +10 -0
  81. package/client/src/providers/WebSocketProvider.tsx +146 -0
  82. package/client/src/router.tsx +391 -0
  83. package/client/src/stores/mesh.ts +78 -0
  84. package/client/src/stores/session.ts +322 -0
  85. package/client/src/stores/sidebar.ts +336 -0
  86. package/client/src/stores/theme.ts +44 -0
  87. package/client/src/styles/global.css +167 -0
  88. package/client/src/styles/theme-vars.css +18 -0
  89. package/client/src/themes/index.ts +79 -0
  90. package/client/src/utils/findDuplicateKeys.ts +12 -0
  91. package/client/tsconfig.json +14 -0
  92. package/client/vite.config.ts +20 -0
  93. package/package.json +46 -0
  94. package/server/package.json +22 -0
  95. package/server/src/auth/passphrase.ts +48 -0
  96. package/server/src/config.ts +55 -0
  97. package/server/src/daemon.ts +338 -0
  98. package/server/src/features/ralph-loop.ts +173 -0
  99. package/server/src/features/scheduler.ts +281 -0
  100. package/server/src/features/sticky-notes.ts +102 -0
  101. package/server/src/handlers/chat.ts +194 -0
  102. package/server/src/handlers/fs.ts +84 -0
  103. package/server/src/handlers/loop.ts +37 -0
  104. package/server/src/handlers/mesh.ts +125 -0
  105. package/server/src/handlers/notes.ts +45 -0
  106. package/server/src/handlers/project-settings.ts +174 -0
  107. package/server/src/handlers/scheduler.ts +47 -0
  108. package/server/src/handlers/session.ts +159 -0
  109. package/server/src/handlers/settings.ts +109 -0
  110. package/server/src/handlers/skills.ts +380 -0
  111. package/server/src/handlers/terminal.ts +70 -0
  112. package/server/src/identity.ts +26 -0
  113. package/server/src/index.ts +190 -0
  114. package/server/src/mesh/connector.ts +209 -0
  115. package/server/src/mesh/discovery.ts +123 -0
  116. package/server/src/mesh/pairing.ts +94 -0
  117. package/server/src/mesh/peers.ts +52 -0
  118. package/server/src/mesh/proxy.ts +103 -0
  119. package/server/src/mesh/session-sync.ts +107 -0
  120. package/server/src/project/context-breakdown.ts +289 -0
  121. package/server/src/project/file-browser.ts +106 -0
  122. package/server/src/project/project-files.ts +267 -0
  123. package/server/src/project/registry.ts +57 -0
  124. package/server/src/project/sdk-bridge.ts +566 -0
  125. package/server/src/project/session.ts +432 -0
  126. package/server/src/project/terminal.ts +69 -0
  127. package/server/src/tls.ts +51 -0
  128. package/server/src/ws/broadcast.ts +31 -0
  129. package/server/src/ws/router.ts +104 -0
  130. package/server/src/ws/server.ts +2 -0
  131. package/server/tsconfig.json +16 -0
  132. package/shared/package.json +11 -0
  133. package/shared/src/constants.ts +7 -0
  134. package/shared/src/index.ts +4 -0
  135. package/shared/src/messages.ts +638 -0
  136. package/shared/src/models.ts +136 -0
  137. package/shared/src/project-settings.ts +45 -0
  138. package/shared/tsconfig.json +11 -0
  139. package/themes/amoled.json +20 -0
  140. package/themes/ayu-light.json +9 -0
  141. package/themes/catppuccin-latte.json +9 -0
  142. package/themes/catppuccin-mocha.json +9 -0
  143. package/themes/clay-light.json +10 -0
  144. package/themes/clay.json +10 -0
  145. package/themes/dracula.json +9 -0
  146. package/themes/everforest-light.json +9 -0
  147. package/themes/everforest.json +9 -0
  148. package/themes/github-light.json +9 -0
  149. package/themes/gruvbox-dark.json +9 -0
  150. package/themes/gruvbox-light.json +9 -0
  151. package/themes/monokai.json +9 -0
  152. package/themes/nord-light.json +9 -0
  153. package/themes/nord.json +9 -0
  154. package/themes/one-dark.json +9 -0
  155. package/themes/one-light.json +9 -0
  156. package/themes/rose-pine-dawn.json +9 -0
  157. package/themes/rose-pine.json +9 -0
  158. package/themes/solarized-dark.json +9 -0
  159. package/themes/solarized-light.json +9 -0
  160. package/themes/tokyo-night-light.json +9 -0
  161. package/themes/tokyo-night.json +9 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,338 @@
1
+ import { join, resolve } from "node:path";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import type { ServerWebSocket } from "bun";
4
+ import { getLatticeHome, loadConfig } from "./config";
5
+ import { loadOrCreateIdentity } from "./identity";
6
+ import { addClient, removeClient, routeMessage } from "./ws/server";
7
+ import { broadcast, sendTo } from "./ws/broadcast";
8
+ import { buildNodesMessage } from "./handlers/mesh";
9
+ import { startDiscovery } from "./mesh/discovery";
10
+ import { startMeshConnections, onPeerConnected, onPeerDisconnected, onPeerMessage } from "./mesh/connector";
11
+ import { handleProxyRequest, handleProxyResponse } from "./mesh/proxy";
12
+ import { verifyPassphrase, generateSessionToken, addSession, isValidSession } from "./auth/passphrase";
13
+ import { ensureCerts } from "./tls";
14
+ import type { ClientMessage, MeshMessage } from "@lattice/shared";
15
+ import "./handlers/session";
16
+ import "./handlers/chat";
17
+ import { loadInterruptedSessions } from "./project/sdk-bridge";
18
+ import "./handlers/fs";
19
+ import "./handlers/terminal";
20
+ import "./handlers/settings";
21
+ import "./handlers/project-settings";
22
+ import "./handlers/mesh";
23
+ import "./handlers/loop";
24
+ import "./handlers/scheduler";
25
+ import "./handlers/notes";
26
+ import "./handlers/skills";
27
+ import { startScheduler } from "./features/scheduler";
28
+ import { loadNotes } from "./features/sticky-notes";
29
+ import { cleanupClientTerminals } from "./handlers/terminal";
30
+
31
+ interface WsData {
32
+ id: string;
33
+ }
34
+
35
+ function parseCookies(cookieHeader: string): Map<string, string> {
36
+ var map = new Map<string, string>();
37
+ var parts = cookieHeader.split(";");
38
+ for (var i = 0; i < parts.length; i++) {
39
+ var part = parts[i].trim();
40
+ var eqIdx = part.indexOf("=");
41
+ if (eqIdx === -1) {
42
+ continue;
43
+ }
44
+ var key = part.slice(0, eqIdx).trim();
45
+ var value = part.slice(eqIdx + 1).trim();
46
+ map.set(key, value);
47
+ }
48
+ return map;
49
+ }
50
+
51
+ function isAuthenticated(req: Request, passphraseHash: string | undefined): boolean {
52
+ if (!passphraseHash) {
53
+ return true;
54
+ }
55
+ var cookieHeader = req.headers.get("cookie") || "";
56
+ var cookies = parseCookies(cookieHeader);
57
+ var token = cookies.get("lattice_auth");
58
+ if (!token) {
59
+ return false;
60
+ }
61
+ return isValidSession(token);
62
+ }
63
+
64
+ function buildLoginPage(): string {
65
+ return `<!DOCTYPE html>
66
+ <html lang="en">
67
+ <head>
68
+ <meta charset="UTF-8" />
69
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
70
+ <title>Lattice — Authenticate</title>
71
+ <style>
72
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
73
+ body {
74
+ font-family: 'Courier New', monospace;
75
+ background: #0d0d0d;
76
+ color: #e0e0e0;
77
+ min-height: 100vh;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ }
82
+ .card {
83
+ background: #161616;
84
+ border: 1px solid #2a2a2a;
85
+ border-radius: 6px;
86
+ padding: 2.5rem;
87
+ width: 100%;
88
+ max-width: 360px;
89
+ }
90
+ h1 {
91
+ font-size: 1.1rem;
92
+ font-weight: 600;
93
+ letter-spacing: 0.08em;
94
+ text-transform: uppercase;
95
+ color: #a0a0a0;
96
+ margin-bottom: 1.75rem;
97
+ }
98
+ label {
99
+ display: block;
100
+ font-size: 0.72rem;
101
+ text-transform: uppercase;
102
+ letter-spacing: 0.1em;
103
+ color: #606060;
104
+ margin-bottom: 0.4rem;
105
+ }
106
+ input[type="password"] {
107
+ width: 100%;
108
+ background: #0d0d0d;
109
+ border: 1px solid #2a2a2a;
110
+ border-radius: 4px;
111
+ color: #e0e0e0;
112
+ font-family: inherit;
113
+ font-size: 0.9rem;
114
+ padding: 0.6rem 0.75rem;
115
+ outline: none;
116
+ transition: border-color 0.15s;
117
+ }
118
+ input[type="password"]:focus { border-color: #4a4a4a; }
119
+ button {
120
+ width: 100%;
121
+ margin-top: 1.25rem;
122
+ background: #e0e0e0;
123
+ color: #0d0d0d;
124
+ border: none;
125
+ border-radius: 4px;
126
+ font-family: inherit;
127
+ font-size: 0.85rem;
128
+ font-weight: 700;
129
+ letter-spacing: 0.06em;
130
+ text-transform: uppercase;
131
+ padding: 0.65rem;
132
+ cursor: pointer;
133
+ transition: background 0.15s;
134
+ }
135
+ button:hover { background: #c0c0c0; }
136
+ .error {
137
+ margin-top: 0.85rem;
138
+ font-size: 0.78rem;
139
+ color: #c07070;
140
+ text-align: center;
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <div class="card">
146
+ <h1>Lattice</h1>
147
+ <form id="form">
148
+ <label for="passphrase">Passphrase</label>
149
+ <input type="password" id="passphrase" name="passphrase" autofocus autocomplete="current-password" />
150
+ <button type="submit">Authenticate</button>
151
+ <div class="error" id="error"></div>
152
+ </form>
153
+ </div>
154
+ <script>
155
+ document.getElementById('form').addEventListener('submit', async function(e) {
156
+ e.preventDefault();
157
+ var passphrase = document.getElementById('passphrase').value;
158
+ try {
159
+ var res = await fetch('/auth', {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify({ passphrase: passphrase })
163
+ });
164
+ if (res.ok) {
165
+ window.location.reload();
166
+ } else {
167
+ document.getElementById('error').textContent = 'Invalid passphrase.';
168
+ }
169
+ } catch {
170
+ document.getElementById('error').textContent = 'Connection error.';
171
+ }
172
+ });
173
+ </script>
174
+ </body>
175
+ </html>`;
176
+ }
177
+
178
+ export async function startDaemon(portOverride?: number | null): Promise<void> {
179
+ var config = loadConfig();
180
+ if (portOverride && !isNaN(portOverride)) {
181
+ config.port = portOverride;
182
+ }
183
+ var identity = loadOrCreateIdentity();
184
+
185
+ console.log(`[lattice] Node: ${config.name} (${identity.id})`);
186
+ console.log(`[lattice] Home: ${getLatticeHome()}`);
187
+
188
+ var clientDir = join(import.meta.dir, "../../client/dist");
189
+
190
+ var tlsOptions: { cert: Buffer; key: Buffer } | undefined;
191
+ if (config.tls) {
192
+ try {
193
+ var certs = ensureCerts();
194
+ tlsOptions = {
195
+ cert: readFileSync(certs.cert),
196
+ key: readFileSync(certs.key),
197
+ };
198
+ console.log("[lattice] TLS enabled");
199
+ } catch (err) {
200
+ console.error("[lattice] Failed to load TLS certs, falling back to HTTP:", err);
201
+ }
202
+ }
203
+
204
+ var protocol = tlsOptions ? "https" : "http";
205
+
206
+ Bun.serve<WsData>({
207
+ port: config.port,
208
+ hostname: "0.0.0.0",
209
+ ...(tlsOptions ? { tls: tlsOptions } : {}),
210
+
211
+ async fetch(req: Request, server: ReturnType<typeof Bun.serve>) {
212
+ var url = new URL(req.url);
213
+
214
+ if (url.pathname === "/auth" && req.method === "POST") {
215
+ try {
216
+ var body = await req.json() as { passphrase?: string };
217
+ var passphrase = body.passphrase || "";
218
+ if (!config.passphraseHash || verifyPassphrase(passphrase, config.passphraseHash)) {
219
+ var token = generateSessionToken();
220
+ addSession(token);
221
+ var headers = new Headers();
222
+ headers.set("Content-Type", "application/json");
223
+ headers.set("Set-Cookie", "lattice_auth=" + token + "; HttpOnly; Path=/; SameSite=Strict");
224
+ return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
225
+ }
226
+ return new Response(JSON.stringify({ ok: false }), { status: 401, headers: { "Content-Type": "application/json" } });
227
+ } catch {
228
+ return new Response(JSON.stringify({ ok: false }), { status: 400, headers: { "Content-Type": "application/json" } });
229
+ }
230
+ }
231
+
232
+ if (!isAuthenticated(req, config.passphraseHash)) {
233
+ return new Response(buildLoginPage(), {
234
+ status: 200,
235
+ headers: { "Content-Type": "text/html; charset=utf-8" },
236
+ });
237
+ }
238
+
239
+ if (url.pathname === "/api/file") {
240
+ var reqFilePath = url.searchParams.get("path");
241
+ if (!reqFilePath) {
242
+ return new Response("Missing path parameter", { status: 400 });
243
+ }
244
+ var resolved = resolve(reqFilePath);
245
+ if (!existsSync(resolved)) {
246
+ for (var pi = 0; pi < config.projects.length; pi++) {
247
+ var projectResolved = join(config.projects[pi].path, reqFilePath);
248
+ if (existsSync(projectResolved)) {
249
+ resolved = projectResolved;
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ if (!existsSync(resolved)) {
255
+ return new Response("File not found", { status: 404 });
256
+ }
257
+ var reqFile = Bun.file(resolved);
258
+ return new Response(reqFile);
259
+ }
260
+
261
+ if (url.pathname === "/ws") {
262
+ var upgraded = server.upgrade(req, {
263
+ data: { id: crypto.randomUUID() },
264
+ });
265
+ if (upgraded) {
266
+ return undefined;
267
+ }
268
+ return new Response("WebSocket upgrade failed", { status: 400 });
269
+ }
270
+
271
+ var staticPath = url.pathname === "/" ? "/index.html" : url.pathname;
272
+ var file = Bun.file(join(clientDir, staticPath));
273
+ if (await file.exists()) {
274
+ return new Response(file);
275
+ }
276
+
277
+ var index = Bun.file(join(clientDir, "index.html"));
278
+ if (await index.exists()) {
279
+ return new Response(index);
280
+ }
281
+
282
+ return new Response("Not found", { status: 404 });
283
+ },
284
+
285
+ websocket: {
286
+ open(ws: ServerWebSocket<WsData>) {
287
+ addClient(ws);
288
+ console.log(`[lattice] Client connected: ${ws.data.id}`);
289
+ sendTo(ws.data.id, { type: "mesh:nodes", nodes: buildNodesMessage() });
290
+ },
291
+ message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
292
+ var text = typeof message === "string" ? message : message.toString();
293
+ try {
294
+ var msg = JSON.parse(text) as ClientMessage;
295
+ routeMessage(ws.data.id, msg);
296
+ } catch (err) {
297
+ console.error("[lattice] Invalid JSON message:", err);
298
+ }
299
+ },
300
+ close(ws: ServerWebSocket<WsData>) {
301
+ removeClient(ws.data.id);
302
+ cleanupClientTerminals(ws.data.id);
303
+ console.log(`[lattice] Client disconnected: ${ws.data.id}`);
304
+ },
305
+ },
306
+ });
307
+
308
+ console.log(`[lattice] Listening on ${protocol}://0.0.0.0:${config.port}`);
309
+
310
+ startDiscovery(identity.id, config.name, config.port);
311
+
312
+ startMeshConnections();
313
+
314
+ startScheduler();
315
+
316
+ loadNotes();
317
+
318
+ loadInterruptedSessions();
319
+
320
+ onPeerConnected(function (nodeId: string) {
321
+ broadcast({ type: "mesh:node_online", nodeId: nodeId });
322
+ });
323
+
324
+ onPeerDisconnected(function (nodeId: string) {
325
+ broadcast({ type: "mesh:node_offline", nodeId: nodeId });
326
+ });
327
+
328
+ onPeerMessage(function (nodeId: string, msg: MeshMessage) {
329
+ if (msg.type === "mesh:proxy_request") {
330
+ handleProxyRequest(nodeId, msg);
331
+ return;
332
+ }
333
+ if (msg.type === "mesh:proxy_response") {
334
+ handleProxyResponse(msg);
335
+ return;
336
+ }
337
+ });
338
+ }
@@ -0,0 +1,173 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomBytes, randomUUID } from "node:crypto";
4
+ import { query } from "@anthropic-ai/claude-agent-sdk";
5
+ import type { SDKMessage, SDKPartialAssistantMessage, SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
6
+ import { broadcast } from "../ws/broadcast";
7
+ import { getProjectBySlug } from "../project/registry";
8
+ import type { LoopStatus } from "@lattice/shared";
9
+
10
+ var activeLoops = new Map<string, LoopStatus>();
11
+
12
+ function readLoopFile(projectPath: string, filename: string): string | null {
13
+ var loopsDir = join(projectPath, ".claude", "loops");
14
+ var filePath = join(loopsDir, filename);
15
+ if (!existsSync(filePath)) {
16
+ return null;
17
+ }
18
+ return readFileSync(filePath, "utf-8");
19
+ }
20
+
21
+ async function runIteration(
22
+ loopId: string,
23
+ prompt: string,
24
+ cwd: string,
25
+ iterationNum: number
26
+ ): Promise<string> {
27
+ var accumulated = "";
28
+
29
+ var stream = query({ prompt, options: { cwd, allowedTools: ["*"], permissionMode: "acceptEdits" } });
30
+
31
+ for await (var msg of stream) {
32
+ var typedMsg = msg as SDKMessage;
33
+ if (typedMsg.type === "stream_event") {
34
+ var partial = typedMsg as SDKPartialAssistantMessage;
35
+ var evt = partial.event;
36
+ if (evt.type === "content_block_delta") {
37
+ var deltaEvt = evt as { delta: { type: string; text?: string } };
38
+ if (deltaEvt.delta.type === "text_delta" && typeof deltaEvt.delta.text === "string") {
39
+ accumulated += deltaEvt.delta.text;
40
+ broadcast({ type: "loop:delta", loopId, iteration: iterationNum, text: deltaEvt.delta.text });
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ return accumulated;
47
+ }
48
+
49
+ async function runJudge(
50
+ judgePrompt: string,
51
+ iterationResult: string,
52
+ cwd: string
53
+ ): Promise<{ pass: boolean; reason: string }> {
54
+ var fullPrompt = `${judgePrompt}\n\n<iteration_result>\n${iterationResult}\n</iteration_result>\n\nRespond with PASS or FAIL on the first line, followed by a brief reason.`;
55
+ var accumulated = "";
56
+
57
+ var stream = query({ prompt: fullPrompt, options: { cwd, allowedTools: [], permissionMode: "acceptEdits" } });
58
+
59
+ for await (var msg of stream) {
60
+ var typedMsg = msg as SDKMessage;
61
+ if (typedMsg.type === "stream_event") {
62
+ var partial = typedMsg as SDKPartialAssistantMessage;
63
+ var evt = partial.event;
64
+ if (evt.type === "content_block_delta") {
65
+ var deltaEvt = evt as { delta: { type: string; text?: string } };
66
+ if (deltaEvt.delta.type === "text_delta" && typeof deltaEvt.delta.text === "string") {
67
+ accumulated += deltaEvt.delta.text;
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ var firstLine = accumulated.trim().split("\n")[0].toUpperCase();
74
+ var pass = firstLine.startsWith("PASS");
75
+ var reason = accumulated.trim().split("\n").slice(1).join("\n").trim() || accumulated.trim();
76
+
77
+ return { pass, reason };
78
+ }
79
+
80
+ export function startLoop(projectSlug: string): LoopStatus | null {
81
+ var project = getProjectBySlug(projectSlug);
82
+ if (!project) {
83
+ return null;
84
+ }
85
+
86
+ var prompt = readLoopFile(project.path, "PROMPT.md");
87
+ if (!prompt) {
88
+ return null;
89
+ }
90
+
91
+ var loopId = "loop_" + Date.now() + "_" + randomBytes(3).toString("hex");
92
+ var loopStatus: LoopStatus = {
93
+ id: loopId,
94
+ projectSlug,
95
+ status: "running",
96
+ iteration: 0,
97
+ maxIterations: 20,
98
+ judgeReason: null,
99
+ startedAt: Date.now(),
100
+ finishedAt: null,
101
+ };
102
+
103
+ activeLoops.set(loopId, loopStatus);
104
+ broadcast({ type: "loop:started", loop: loopStatus });
105
+
106
+ void (async function () {
107
+ var judgePrompt = readLoopFile(project.path, "JUDGE.md");
108
+
109
+ for (var i = 1; i <= loopStatus.maxIterations; i++) {
110
+ var current = activeLoops.get(loopId);
111
+ if (!current || current.status === "stopped") {
112
+ break;
113
+ }
114
+
115
+ current.iteration = i;
116
+ activeLoops.set(loopId, current);
117
+ broadcast({ type: "loop:status_update", loop: { ...current } });
118
+
119
+ try {
120
+ var result = await runIteration(loopId, prompt, project.path, i);
121
+
122
+ if (judgePrompt) {
123
+ var judgment = await runJudge(judgePrompt, result, project.path);
124
+ current.judgeReason = judgment.reason;
125
+ activeLoops.set(loopId, current);
126
+
127
+ if (judgment.pass) {
128
+ current.status = "done";
129
+ current.finishedAt = Date.now();
130
+ activeLoops.set(loopId, current);
131
+ broadcast({ type: "loop:status_update", loop: { ...current } });
132
+ break;
133
+ }
134
+ }
135
+ } catch (err: unknown) {
136
+ var errMsg = err instanceof Error ? err.message : String(err);
137
+ console.error(`[ralph-loop] Iteration ${i} error:`, errMsg);
138
+ current.status = "error";
139
+ current.judgeReason = errMsg;
140
+ current.finishedAt = Date.now();
141
+ activeLoops.set(loopId, current);
142
+ broadcast({ type: "loop:status_update", loop: { ...current } });
143
+ return;
144
+ }
145
+ }
146
+
147
+ var final = activeLoops.get(loopId);
148
+ if (final && final.status === "running") {
149
+ final.status = "done";
150
+ final.finishedAt = Date.now();
151
+ activeLoops.set(loopId, final);
152
+ broadcast({ type: "loop:status_update", loop: { ...final } });
153
+ }
154
+ })();
155
+
156
+ return loopStatus;
157
+ }
158
+
159
+ export function stopLoop(loopId: string): boolean {
160
+ var loop = activeLoops.get(loopId);
161
+ if (!loop || loop.status !== "running") {
162
+ return false;
163
+ }
164
+ loop.status = "stopped";
165
+ loop.finishedAt = Date.now();
166
+ activeLoops.set(loopId, loop);
167
+ broadcast({ type: "loop:status_update", loop: { ...loop } });
168
+ return true;
169
+ }
170
+
171
+ export function getLoopStatus(loopId: string): LoopStatus | null {
172
+ return activeLoops.get(loopId) ?? null;
173
+ }