@cryptiklemur/lattice 5.10.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{angular-html-BDIcxkJq.js → angular-html-BoFzmWT8.js} +1 -1
- package/dist/client/assets/{angular-ts-Bt22ouNH.js → angular-ts-DZnI8rKE.js} +1 -1
- package/dist/client/assets/{apl-p8qkxzEK.js → apl-DstVmncE.js} +1 -1
- package/dist/client/assets/{astro-CIaMc49M.js → astro-DTPCjzEx.js} +1 -1
- package/dist/client/assets/{blade-BR56EAMD.js → blade-6q42Ss3F.js} +1 -1
- package/dist/client/assets/{c-Dli0HzAh.js → c-BQDGJ-nQ.js} +1 -1
- package/dist/client/assets/{cobol-Cad15ECy.js → cobol-Dlh0WvsZ.js} +1 -1
- package/dist/client/assets/{coffee-DpyATEbF.js → coffee-DdQv129j.js} +1 -1
- package/dist/client/assets/{cpp-KN8_NFsf.js → cpp-DhbQJIv4.js} +1 -1
- package/dist/client/assets/{crystal-CuyGv0kh.js → crystal-C22kERUB.js} +1 -1
- package/dist/client/assets/{css-Cm3q4bxn.js → css-n31O5kHj.js} +1 -1
- package/dist/client/assets/{dist-BjxsMc4u.js → dist-D8okl7lw.js} +2 -2
- package/dist/client/assets/{edge-B6S7CSbx.js → edge-Cgwx-o_7.js} +1 -1
- package/dist/client/assets/{elixir-CNUy9H8T.js → elixir-DAGM2WKD.js} +1 -1
- package/dist/client/assets/{elm-CNfcWmb9.js → elm-BLw_7oO9.js} +1 -1
- package/dist/client/assets/{erb-DWebzDaI.js → erb-DCaNhYa7.js} +1 -1
- package/dist/client/assets/{git-rebase-B_Pt2ZBK.js → git-rebase-CNNhb8-g.js} +1 -1
- package/dist/client/assets/{glimmer-js-CVwoOd72.js → glimmer-js-BnZd88Wi.js} +1 -1
- package/dist/client/assets/{glimmer-ts-CjtFSxjz.js → glimmer-ts-DvFNbZu-.js} +1 -1
- package/dist/client/assets/{glsl-CP4rggAA.js → glsl-Dnrk_Jnx.js} +1 -1
- package/dist/client/assets/{graphql-Dbm6sAtp.js → graphql-DlWTPvCG.js} +1 -1
- package/dist/client/assets/{hack-Bj9y3SGf.js → hack-DQg1Ek33.js} +1 -1
- package/dist/client/assets/{haml-DRGrdf3f.js → haml-DSk45qIE.js} +1 -1
- package/dist/client/assets/{handlebars-CFKjcBMg.js → handlebars-DuLvATB2.js} +1 -1
- package/dist/client/assets/{html-Vcd4eHHg.js → html-D4DiUnLg.js} +1 -1
- package/dist/client/assets/{html-derivative-BF0YbD4L.js → html-derivative-CS5MZ6d9.js} +1 -1
- package/dist/client/assets/{http-CGVTa2NT.js → http-CkDncfer.js} +1 -1
- package/dist/client/assets/{hurl-B0GrsGqd.js → hurl-DU39oO3U.js} +1 -1
- package/dist/client/assets/{index-CX1tudsF.js → index-CHPfE1Zl.js} +129 -129
- package/dist/client/assets/index-DHUKmLLC.css +2 -0
- package/dist/client/assets/{java-BJHQqHsm.js → java-lntACKEu.js} +1 -1
- package/dist/client/assets/{javascript-CmuMsKrc.js → javascript-CxkFc6nV.js} +1 -1
- package/dist/client/assets/{jinja-JxCLeq1j.js → jinja-DolO2zO7.js} +1 -1
- package/dist/client/assets/{jison-BdgAUhei.js → jison-Cok5FPev.js} +1 -1
- package/dist/client/assets/{json-DtPissHL.js → json-BebuQPrq.js} +1 -1
- package/dist/client/assets/{jsx-DUAxxDkP.js → jsx-iLBaUyXr.js} +1 -1
- package/dist/client/assets/{julia-DxDlbL6e.js → julia-C5Dsc7cH.js} +1 -1
- package/dist/client/assets/{just-CVmAAx2R.js → just-DJYqq_9R.js} +1 -1
- package/dist/client/assets/{latex-uwxggTWA.js → latex-BTTYiKj1.js} +1 -1
- package/dist/client/assets/{liquid-xsETAJJy.js → liquid-DpAKCrOB.js} +1 -1
- package/dist/client/assets/{lua-B2Hh8PgD.js → lua-BZ6b1hko.js} +1 -1
- package/dist/client/assets/{marko-yDeGxD87.js → marko-D8VK6iGt.js} +1 -1
- package/dist/client/assets/{mdc-QMp4ieYR.js → mdc-Paa3XzwY.js} +1 -1
- package/dist/client/assets/{nginx-7gmRmcqz.js → nginx-C5k9mWtJ.js} +1 -1
- package/dist/client/assets/{nim-CA8SNY_7.js → nim-Dst6YSnE.js} +1 -1
- package/dist/client/assets/{perl-lx5nW4VC.js → perl-XhiCjgBp.js} +1 -1
- package/dist/client/assets/{php-DgHiW953.js → php-BcsPLnLU.js} +1 -1
- package/dist/client/assets/{pug-CbbB1vwb.js → pug-GLH9-eAJ.js} +1 -1
- package/dist/client/assets/{qml-COrzwCIh.js → qml-Cj_lJioE.js} +1 -1
- package/dist/client/assets/{r-Dv7pZJDH.js → r-B70aGYK5.js} +1 -1
- package/dist/client/assets/{razor-D2m8EDP5.js → razor-R3gub_zy.js} +1 -1
- package/dist/client/assets/{regexp-BXLT-jPc.js → regexp-itC0dIUJ.js} +1 -1
- package/dist/client/assets/{rst-_S6rrUYh.js → rst-DdyoV8E2.js} +1 -1
- package/dist/client/assets/{ruby-C3XO7tYY.js → ruby-BYBZsv66.js} +1 -1
- package/dist/client/assets/{sas-DP2k4iuN.js → sas-fqfqXqj1.js} +1 -1
- package/dist/client/assets/{scss-lhLFMXGn.js → scss-B-ELv6mu.js} +1 -1
- package/dist/client/assets/{shellscript-BYlBPHen.js → shellscript-BgB8TNw6.js} +1 -1
- package/dist/client/assets/{shellsession-CbVyQKWZ.js → shellsession-BLK2Dgkm.js} +1 -1
- package/dist/client/assets/{soy-Be8a0lHq.js → soy-C7_RmNrp.js} +1 -1
- package/dist/client/assets/{sql-2KxvU9YS.js → sql-AUgbUJq4.js} +1 -1
- package/dist/client/assets/{stata-BxlWftTS.js → stata-CIVqSIOr.js} +1 -1
- package/dist/client/assets/{surrealql-CJ-q86nR.js → surrealql-BzRQzc5S.js} +1 -1
- package/dist/client/assets/{svelte-Q1ml0OiY.js → svelte-BCIwEwtb.js} +1 -1
- package/dist/client/assets/{templ-BbfPZhtu.js → templ-C1hbwe4u.js} +1 -1
- package/dist/client/assets/{tex-Dcth4Gi6.js → tex-CI4tIsaP.js} +1 -1
- package/dist/client/assets/{ts-tags-BKhSOXI3.js → ts-tags-SUeikhEp.js} +1 -1
- package/dist/client/assets/{tsx-CS6iQ0XH.js → tsx-xkp7aIZs.js} +1 -1
- package/dist/client/assets/{twig-BHp31ZxS.js → twig-CGgBSAyc.js} +1 -1
- package/dist/client/assets/{typescript-16YJBTaO.js → typescript-O2YMTl_s.js} +1 -1
- package/dist/client/assets/{vue-CMKwTi4r.js → vue-DsNRxos1.js} +1 -1
- package/dist/client/assets/{vue-html-Dr8VUA2G.js → vue-html-CuY3t7bs.js} +1 -1
- package/dist/client/assets/{vue-vine-DZUqDerl.js → vue-vine-C6kSCKwY.js} +1 -1
- package/dist/client/assets/{xml-CBbBKKDC.js → xml-DafwzOLY.js} +1 -1
- package/dist/client/assets/{xsl-DWEX6PKX.js → xsl-1SGGZibr.js} +1 -1
- package/dist/client/assets/{yaml-DvKvvh3X.js → yaml-DSVhzmhr.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/analytics/engine.js +241 -241
- package/dist/server/assets.js +4 -4
- package/dist/server/auth/passphrase.js +13 -13
- package/dist/server/config.js +7 -7
- package/dist/server/daemon.js +93 -93
- package/dist/server/features/brainstorm.js +42 -42
- package/dist/server/features/ralph-loop.js +33 -33
- package/dist/server/features/scheduler.js +53 -53
- package/dist/server/features/specs.js +54 -54
- package/dist/server/features/sticky-notes.js +17 -17
- package/dist/server/features/superpowers.js +24 -24
- package/dist/server/handlers/analytics.js +1 -1
- package/dist/server/handlers/attachment.js +32 -32
- package/dist/server/handlers/bookmarks.js +4 -4
- package/dist/server/handlers/brainstorm.js +4 -4
- package/dist/server/handlers/chat.js +54 -54
- package/dist/server/handlers/editor.js +13 -13
- package/dist/server/handlers/fs.js +51 -51
- package/dist/server/handlers/hooks.js +20 -20
- package/dist/server/handlers/loop.js +6 -6
- package/dist/server/handlers/memory.js +44 -44
- package/dist/server/handlers/mesh.js +60 -60
- package/dist/server/handlers/notes.js +7 -7
- package/dist/server/handlers/plugins.js +174 -174
- package/dist/server/handlers/project-settings.js +26 -26
- package/dist/server/handlers/scheduler.js +6 -6
- package/dist/server/handlers/session.js +24 -24
- package/dist/server/handlers/settings.js +21 -21
- package/dist/server/handlers/skills.js +91 -91
- package/dist/server/handlers/specs.js +51 -28
- package/dist/server/handlers/terminal.js +13 -13
- package/dist/server/handlers/themes.js +21 -21
- package/dist/server/handlers/update.js +17 -17
- package/dist/server/hooks/event_forward.sh +34 -0
- package/dist/server/hooks/post_tool_use.sh +26 -0
- package/dist/server/hooks/statusline.sh +26 -0
- package/dist/server/identity.js +6 -6
- package/dist/server/index.js +111 -111
- package/dist/server/logger.js +1 -1
- package/dist/server/mesh/connector.js +78 -78
- package/dist/server/mesh/crypto.js +20 -20
- package/dist/server/mesh/discovery.js +14 -14
- package/dist/server/mesh/pairing.js +30 -30
- package/dist/server/mesh/peers.js +10 -10
- package/dist/server/mesh/proxy.js +14 -14
- package/dist/server/mesh/session-sync.js +23 -23
- package/dist/server/project/bookmarks.js +11 -11
- package/dist/server/project/context-breakdown.js +70 -70
- package/dist/server/project/file-browser.js +17 -17
- package/dist/server/project/project-files.js +68 -68
- package/dist/server/project/registry.js +10 -10
- package/dist/server/project/sdk-bridge.js +157 -157
- package/dist/server/project/session.js +201 -199
- package/dist/server/project/terminal.js +15 -15
- package/dist/server/project/warmup.js +37 -37
- package/dist/server/push.js +11 -11
- package/dist/server/runtime.js +1 -1
- package/dist/server/tls.js +15 -15
- package/dist/server/tui.js +15 -15
- package/dist/server/update-checker.js +21 -21
- package/dist/server/ws/broadcast.js +18 -18
- package/dist/server/ws/router.js +17 -17
- package/dist/shared/constants.js +8 -8
- package/package.json +2 -2
- package/dist/client/assets/index-DlfI20Gn.css +0 -2
|
@@ -3,9 +3,9 @@ import { join } from "node:path";
|
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { broadcast } from "../ws/broadcast.js";
|
|
5
5
|
import { log } from "../logger.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
let activeBrainstorms = {};
|
|
7
|
+
let watchers = [];
|
|
8
|
+
let debounceTimers = {};
|
|
9
9
|
function debounce(key, fn, ms) {
|
|
10
10
|
if (debounceTimers[key]) {
|
|
11
11
|
clearTimeout(debounceTimers[key]);
|
|
@@ -19,13 +19,13 @@ function getMostRecentSessionDir(brainstormDir) {
|
|
|
19
19
|
if (!existsSync(brainstormDir)) {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
const entries = [];
|
|
23
23
|
try {
|
|
24
|
-
|
|
25
|
-
for (
|
|
26
|
-
|
|
24
|
+
const names = readdirSync(brainstormDir);
|
|
25
|
+
for (let i = 0; i < names.length; i++) {
|
|
26
|
+
const fullPath = join(brainstormDir, names[i]);
|
|
27
27
|
try {
|
|
28
|
-
|
|
28
|
+
const stat = statSync(fullPath);
|
|
29
29
|
if (stat.isDirectory()) {
|
|
30
30
|
entries.push({ name: names[i], mtime: stat.mtimeMs });
|
|
31
31
|
}
|
|
@@ -59,11 +59,11 @@ function handleContentFile(projectSlug, contentDir, sessionDir, filename) {
|
|
|
59
59
|
broadcast({ type: "brainstorm:cleared" });
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
const filePath = join(contentDir, filename);
|
|
63
63
|
if (!existsSync(filePath)) {
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
let html;
|
|
67
67
|
try {
|
|
68
68
|
html = readFileSync(filePath, "utf-8");
|
|
69
69
|
}
|
|
@@ -83,32 +83,32 @@ function handleStateFile(projectSlug, filename) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
function watchSessionDir(projectSlug, sessionDir) {
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const contentDir = join(sessionDir, "content");
|
|
87
|
+
const stateDir = join(sessionDir, "state");
|
|
88
88
|
if (!existsSync(contentDir)) {
|
|
89
89
|
mkdirSync(contentDir, { recursive: true });
|
|
90
90
|
}
|
|
91
91
|
if (!existsSync(stateDir)) {
|
|
92
92
|
mkdirSync(stateDir, { recursive: true });
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
const alreadyStopped = existsSync(join(stateDir, "server-stopped"));
|
|
95
95
|
if (!alreadyStopped) {
|
|
96
|
-
|
|
96
|
+
let existingFiles = [];
|
|
97
97
|
try {
|
|
98
98
|
existingFiles = readdirSync(contentDir);
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
101
|
// ok
|
|
102
102
|
}
|
|
103
|
-
for (
|
|
104
|
-
|
|
103
|
+
for (let i = 0; i < existingFiles.length; i++) {
|
|
104
|
+
const fname = existingFiles[i];
|
|
105
105
|
if (fname.endsWith(".html") && !isWaitingFile(fname)) {
|
|
106
106
|
handleContentFile(projectSlug, contentDir, sessionDir, fname);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
try {
|
|
111
|
-
|
|
111
|
+
const contentWatcher = watch(contentDir, function (eventType, filename) {
|
|
112
112
|
if (!filename) {
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
@@ -122,7 +122,7 @@ function watchSessionDir(projectSlug, sessionDir) {
|
|
|
122
122
|
log.server("[brainstorm] failed to watch content dir %s: %s", contentDir, err);
|
|
123
123
|
}
|
|
124
124
|
try {
|
|
125
|
-
|
|
125
|
+
const stateWatcher = watch(stateDir, function (eventType, filename) {
|
|
126
126
|
if (!filename) {
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
@@ -137,19 +137,19 @@ function watchSessionDir(projectSlug, sessionDir) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
function watchBrainstormDir(projectSlug, brainstormDir) {
|
|
140
|
-
|
|
140
|
+
const recentSession = getMostRecentSessionDir(brainstormDir);
|
|
141
141
|
if (recentSession) {
|
|
142
142
|
watchSessionDir(projectSlug, recentSession);
|
|
143
143
|
}
|
|
144
144
|
try {
|
|
145
|
-
|
|
145
|
+
const dirWatcher = watch(brainstormDir, function (eventType, filename) {
|
|
146
146
|
if (!filename) {
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
debounce("brainstorm:" + brainstormDir + ":" + filename, function () {
|
|
150
|
-
|
|
150
|
+
const candidate = join(brainstormDir, filename);
|
|
151
151
|
try {
|
|
152
|
-
|
|
152
|
+
const stat = statSync(candidate);
|
|
153
153
|
if (stat.isDirectory()) {
|
|
154
154
|
log.server("[brainstorm] new session dir detected: %s", candidate);
|
|
155
155
|
watchSessionDir(projectSlug, candidate);
|
|
@@ -167,8 +167,8 @@ function watchBrainstormDir(projectSlug, brainstormDir) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
function watchProjectRoot(projectSlug, projectPath) {
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
const superpowersDir = join(projectPath, ".superpowers");
|
|
171
|
+
const brainstormDir = join(superpowersDir, "brainstorm");
|
|
172
172
|
if (existsSync(brainstormDir)) {
|
|
173
173
|
watchBrainstormDir(projectSlug, brainstormDir);
|
|
174
174
|
return;
|
|
@@ -178,7 +178,7 @@ function watchProjectRoot(projectSlug, projectPath) {
|
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
try {
|
|
181
|
-
|
|
181
|
+
const rootWatcher = watch(projectPath, function (eventType, filename) {
|
|
182
182
|
if (filename !== ".superpowers") {
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
@@ -199,7 +199,7 @@ function watchForBrainstormDir(projectSlug, superpowersDir, brainstormDir) {
|
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
try {
|
|
202
|
-
|
|
202
|
+
const spWatcher = watch(superpowersDir, function (eventType, filename) {
|
|
203
203
|
if (filename !== "brainstorm") {
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
@@ -215,10 +215,10 @@ function watchForBrainstormDir(projectSlug, superpowersDir, brainstormDir) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
export function startBrainstormWatchers() {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (
|
|
221
|
-
|
|
218
|
+
const config = loadConfig();
|
|
219
|
+
const projects = config.projects;
|
|
220
|
+
for (let i = 0; i < projects.length; i++) {
|
|
221
|
+
const project = projects[i];
|
|
222
222
|
if (!project.path || !project.slug) {
|
|
223
223
|
continue;
|
|
224
224
|
}
|
|
@@ -230,7 +230,7 @@ export function startBrainstormWatchers() {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
export function stopBrainstormWatchers() {
|
|
233
|
-
for (
|
|
233
|
+
for (let i = 0; i < watchers.length; i++) {
|
|
234
234
|
try {
|
|
235
235
|
watchers[i].close();
|
|
236
236
|
}
|
|
@@ -240,8 +240,8 @@ export function stopBrainstormWatchers() {
|
|
|
240
240
|
}
|
|
241
241
|
watchers = [];
|
|
242
242
|
activeBrainstorms = {};
|
|
243
|
-
|
|
244
|
-
for (
|
|
243
|
+
const keys = Object.keys(debounceTimers);
|
|
244
|
+
for (let i = 0; i < keys.length; i++) {
|
|
245
245
|
clearTimeout(debounceTimers[keys[i]]);
|
|
246
246
|
}
|
|
247
247
|
debounceTimers = {};
|
|
@@ -250,23 +250,23 @@ export function getActiveBrainstorm(projectSlug) {
|
|
|
250
250
|
return activeBrainstorms[projectSlug] || null;
|
|
251
251
|
}
|
|
252
252
|
export function getAnyActiveBrainstorm() {
|
|
253
|
-
|
|
253
|
+
const keys = Object.keys(activeBrainstorms);
|
|
254
254
|
if (keys.length === 0)
|
|
255
255
|
return null;
|
|
256
256
|
return activeBrainstorms[keys[0]];
|
|
257
257
|
}
|
|
258
258
|
export function stopBrainstorm(projectSlug) {
|
|
259
|
-
|
|
260
|
-
for (
|
|
261
|
-
|
|
262
|
-
|
|
259
|
+
const slugs = projectSlug ? [projectSlug] : Object.keys(activeBrainstorms);
|
|
260
|
+
for (let i = 0; i < slugs.length; i++) {
|
|
261
|
+
const slug = slugs[i];
|
|
262
|
+
const active = activeBrainstorms[slug];
|
|
263
263
|
if (!active)
|
|
264
264
|
continue;
|
|
265
|
-
|
|
265
|
+
const stateDir = join(active.sessionDir, "state");
|
|
266
266
|
if (!existsSync(stateDir)) {
|
|
267
267
|
mkdirSync(stateDir, { recursive: true });
|
|
268
268
|
}
|
|
269
|
-
|
|
269
|
+
const stoppedPath = join(stateDir, "server-stopped");
|
|
270
270
|
if (!existsSync(stoppedPath)) {
|
|
271
271
|
try {
|
|
272
272
|
writeFileSync(stoppedPath, String(Date.now()));
|
|
@@ -281,11 +281,11 @@ export function stopBrainstorm(projectSlug) {
|
|
|
281
281
|
broadcast({ type: "brainstorm:cleared" });
|
|
282
282
|
}
|
|
283
283
|
export function writeBrainstormEvent(sessionDir, event) {
|
|
284
|
-
|
|
284
|
+
const stateDir = join(sessionDir, "state");
|
|
285
285
|
if (!existsSync(stateDir)) {
|
|
286
286
|
mkdirSync(stateDir, { recursive: true });
|
|
287
287
|
}
|
|
288
|
-
|
|
288
|
+
const eventsFile = join(stateDir, "events");
|
|
289
289
|
try {
|
|
290
290
|
appendFileSync(eventsFile, JSON.stringify(event) + "\n", "utf-8");
|
|
291
291
|
}
|
|
@@ -4,25 +4,25 @@ import { randomBytes } from "node:crypto";
|
|
|
4
4
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
5
5
|
import { broadcast } from "../ws/broadcast.js";
|
|
6
6
|
import { getProjectBySlug } from "../project/registry.js";
|
|
7
|
-
|
|
7
|
+
const activeLoops = new Map();
|
|
8
8
|
function readLoopFile(projectPath, filename) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const loopsDir = join(projectPath, ".claude", "loops");
|
|
10
|
+
const filePath = join(loopsDir, filename);
|
|
11
11
|
if (!existsSync(filePath)) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
14
|
return readFileSync(filePath, "utf-8");
|
|
15
15
|
}
|
|
16
16
|
async function runIteration(loopId, prompt, cwd, iterationNum) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
for await (
|
|
20
|
-
|
|
17
|
+
let accumulated = "";
|
|
18
|
+
const stream = query({ prompt, options: { cwd, allowedTools: ["*"], permissionMode: "acceptEdits" } });
|
|
19
|
+
for await (const msg of stream) {
|
|
20
|
+
const typedMsg = msg;
|
|
21
21
|
if (typedMsg.type === "stream_event") {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const partial = typedMsg;
|
|
23
|
+
const evt = partial.event;
|
|
24
24
|
if (evt.type === "content_block_delta") {
|
|
25
|
-
|
|
25
|
+
const deltaEvt = evt;
|
|
26
26
|
if (deltaEvt.delta.type === "text_delta" && typeof deltaEvt.delta.text === "string") {
|
|
27
27
|
accumulated += deltaEvt.delta.text;
|
|
28
28
|
broadcast({ type: "loop:delta", loopId, iteration: iterationNum, text: deltaEvt.delta.text });
|
|
@@ -33,38 +33,38 @@ async function runIteration(loopId, prompt, cwd, iterationNum) {
|
|
|
33
33
|
return accumulated;
|
|
34
34
|
}
|
|
35
35
|
async function runJudge(judgePrompt, iterationResult, cwd) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for await (
|
|
40
|
-
|
|
36
|
+
const 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.`;
|
|
37
|
+
let accumulated = "";
|
|
38
|
+
const stream = query({ prompt: fullPrompt, options: { cwd, allowedTools: [], permissionMode: "acceptEdits" } });
|
|
39
|
+
for await (const msg of stream) {
|
|
40
|
+
const typedMsg = msg;
|
|
41
41
|
if (typedMsg.type === "stream_event") {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const partial = typedMsg;
|
|
43
|
+
const evt = partial.event;
|
|
44
44
|
if (evt.type === "content_block_delta") {
|
|
45
|
-
|
|
45
|
+
const deltaEvt = evt;
|
|
46
46
|
if (deltaEvt.delta.type === "text_delta" && typeof deltaEvt.delta.text === "string") {
|
|
47
47
|
accumulated += deltaEvt.delta.text;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const firstLine = accumulated.trim().split("\n")[0].toUpperCase();
|
|
53
|
+
const pass = firstLine.startsWith("PASS");
|
|
54
|
+
const reason = accumulated.trim().split("\n").slice(1).join("\n").trim() || accumulated.trim();
|
|
55
55
|
return { pass, reason };
|
|
56
56
|
}
|
|
57
57
|
export function startLoop(projectSlug) {
|
|
58
|
-
|
|
58
|
+
const project = getProjectBySlug(projectSlug);
|
|
59
59
|
if (!project) {
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
const prompt = readLoopFile(project.path, "PROMPT.md");
|
|
63
63
|
if (!prompt) {
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const loopId = "loop_" + Date.now() + "_" + randomBytes(3).toString("hex");
|
|
67
|
+
const loopStatus = {
|
|
68
68
|
id: loopId,
|
|
69
69
|
projectSlug,
|
|
70
70
|
status: "running",
|
|
@@ -77,9 +77,9 @@ export function startLoop(projectSlug) {
|
|
|
77
77
|
activeLoops.set(loopId, loopStatus);
|
|
78
78
|
broadcast({ type: "loop:started", loop: loopStatus });
|
|
79
79
|
void (async function () {
|
|
80
|
-
|
|
81
|
-
for (
|
|
82
|
-
|
|
80
|
+
const judgePrompt = readLoopFile(project.path, "JUDGE.md");
|
|
81
|
+
for (let i = 1; i <= loopStatus.maxIterations; i++) {
|
|
82
|
+
const current = activeLoops.get(loopId);
|
|
83
83
|
if (!current || current.status === "stopped") {
|
|
84
84
|
break;
|
|
85
85
|
}
|
|
@@ -87,9 +87,9 @@ export function startLoop(projectSlug) {
|
|
|
87
87
|
activeLoops.set(loopId, current);
|
|
88
88
|
broadcast({ type: "loop:status_update", loop: { ...current } });
|
|
89
89
|
try {
|
|
90
|
-
|
|
90
|
+
const result = await runIteration(loopId, prompt, project.path, i);
|
|
91
91
|
if (judgePrompt) {
|
|
92
|
-
|
|
92
|
+
const judgment = await runJudge(judgePrompt, result, project.path);
|
|
93
93
|
current.judgeReason = judgment.reason;
|
|
94
94
|
activeLoops.set(loopId, current);
|
|
95
95
|
if (judgment.pass) {
|
|
@@ -102,7 +102,7 @@ export function startLoop(projectSlug) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
catch (err) {
|
|
105
|
-
|
|
105
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
106
106
|
console.error(`[ralph-loop] Iteration ${i} error:`, errMsg);
|
|
107
107
|
current.status = "error";
|
|
108
108
|
current.judgeReason = errMsg;
|
|
@@ -112,7 +112,7 @@ export function startLoop(projectSlug) {
|
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
const final = activeLoops.get(loopId);
|
|
116
116
|
if (final && final.status === "running") {
|
|
117
117
|
final.status = "done";
|
|
118
118
|
final.finishedAt = Date.now();
|
|
@@ -123,7 +123,7 @@ export function startLoop(projectSlug) {
|
|
|
123
123
|
return loopStatus;
|
|
124
124
|
}
|
|
125
125
|
export function stopLoop(loopId) {
|
|
126
|
-
|
|
126
|
+
const loop = activeLoops.get(loopId);
|
|
127
127
|
if (!loop || loop.status !== "running") {
|
|
128
128
|
return false;
|
|
129
129
|
}
|
|
@@ -3,11 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { getLatticeHome } from "../config.js";
|
|
5
5
|
import { broadcast } from "../ws/broadcast.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
let schedulesFile = "";
|
|
7
|
+
let tasks = [];
|
|
8
|
+
let timerId = null;
|
|
9
|
+
const lastTriggeredMinute = {};
|
|
10
|
+
const CHECK_INTERVAL = 30 * 1000;
|
|
11
11
|
function getSchedulesPath() {
|
|
12
12
|
if (!schedulesFile) {
|
|
13
13
|
schedulesFile = join(getLatticeHome(), "schedules.json");
|
|
@@ -15,37 +15,37 @@ function getSchedulesPath() {
|
|
|
15
15
|
return schedulesFile;
|
|
16
16
|
}
|
|
17
17
|
function parseCronField(field, min, max) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for (
|
|
21
|
-
|
|
18
|
+
const values = [];
|
|
19
|
+
const parts = field.split(",");
|
|
20
|
+
for (let i = 0; i < parts.length; i++) {
|
|
21
|
+
const part = parts[i].trim();
|
|
22
22
|
if (part.indexOf("/") !== -1) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const slashParts = part.split("/");
|
|
24
|
+
const step = parseInt(slashParts[1], 10);
|
|
25
|
+
const rangeStr = slashParts[0];
|
|
26
|
+
let rangeMin = min;
|
|
27
|
+
let rangeMax = max;
|
|
28
28
|
if (rangeStr !== "*") {
|
|
29
|
-
|
|
29
|
+
const rp = rangeStr.split("-");
|
|
30
30
|
rangeMin = parseInt(rp[0], 10);
|
|
31
31
|
rangeMax = rp.length > 1 ? parseInt(rp[1], 10) : rangeMin;
|
|
32
32
|
}
|
|
33
|
-
for (
|
|
33
|
+
for (let v = rangeMin; v <= rangeMax; v += step) {
|
|
34
34
|
values.push(v);
|
|
35
35
|
}
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
if (part === "*") {
|
|
39
|
-
for (
|
|
39
|
+
for (let v = min; v <= max; v++) {
|
|
40
40
|
values.push(v);
|
|
41
41
|
}
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
44
|
if (part.indexOf("-") !== -1) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for (
|
|
45
|
+
const rangeParts = part.split("-");
|
|
46
|
+
const from = parseInt(rangeParts[0], 10);
|
|
47
|
+
const to = parseInt(rangeParts[1], 10);
|
|
48
|
+
for (let v = from; v <= to; v++) {
|
|
49
49
|
values.push(v);
|
|
50
50
|
}
|
|
51
51
|
continue;
|
|
@@ -55,7 +55,7 @@ function parseCronField(field, min, max) {
|
|
|
55
55
|
return values;
|
|
56
56
|
}
|
|
57
57
|
function parseCron(expr) {
|
|
58
|
-
|
|
58
|
+
const fields = expr.trim().split(/\s+/);
|
|
59
59
|
if (fields.length !== 5) {
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
@@ -75,15 +75,15 @@ function cronMatches(parsed, date) {
|
|
|
75
75
|
parsed.daysOfWeek.indexOf(date.getDay()) !== -1);
|
|
76
76
|
}
|
|
77
77
|
function nextRunTime(cronExpr, after) {
|
|
78
|
-
|
|
78
|
+
const parsed = parseCron(cronExpr);
|
|
79
79
|
if (!parsed) {
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const d = new Date(after || Date.now());
|
|
83
83
|
d.setSeconds(0, 0);
|
|
84
84
|
d.setMinutes(d.getMinutes() + 1);
|
|
85
|
-
|
|
86
|
-
for (
|
|
85
|
+
const limit = 366 * 24 * 60;
|
|
86
|
+
for (let i = 0; i < limit; i++) {
|
|
87
87
|
if (cronMatches(parsed, d)) {
|
|
88
88
|
return d.getTime();
|
|
89
89
|
}
|
|
@@ -92,17 +92,17 @@ function nextRunTime(cronExpr, after) {
|
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
94
|
export function loadSchedules() {
|
|
95
|
-
|
|
95
|
+
const path = getSchedulesPath();
|
|
96
96
|
if (!existsSync(path)) {
|
|
97
97
|
tasks = [];
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const raw = readFileSync(path, "utf-8");
|
|
102
|
+
const parsed = JSON.parse(raw);
|
|
103
103
|
tasks = parsed.tasks || [];
|
|
104
|
-
for (
|
|
105
|
-
|
|
104
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
105
|
+
const task = tasks[i];
|
|
106
106
|
if (task.enabled && task.cron) {
|
|
107
107
|
task.nextRunAt = nextRunTime(task.cron);
|
|
108
108
|
}
|
|
@@ -114,12 +114,12 @@ export function loadSchedules() {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
function saveSchedules() {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const path = getSchedulesPath();
|
|
118
|
+
const dir = join(path, "..");
|
|
119
119
|
if (!existsSync(dir)) {
|
|
120
120
|
mkdirSync(dir, { recursive: true });
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
const tmp = path + ".tmp";
|
|
123
123
|
try {
|
|
124
124
|
writeFileSync(tmp, JSON.stringify({ tasks }, null, 2));
|
|
125
125
|
renameSync(tmp, path);
|
|
@@ -129,25 +129,25 @@ function saveSchedules() {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
function tick() {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for (
|
|
135
|
-
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const nowMinuteKey = Math.floor(now / 60000);
|
|
134
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
135
|
+
const task = tasks[i];
|
|
136
136
|
if (!task.enabled || !task.nextRunAt) {
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
139
|
if (task.nextRunAt > now) {
|
|
140
140
|
continue;
|
|
141
141
|
}
|
|
142
|
-
|
|
142
|
+
const triggerKey = task.id + "_" + nowMinuteKey;
|
|
143
143
|
if (lastTriggeredMinute[triggerKey]) {
|
|
144
144
|
continue;
|
|
145
145
|
}
|
|
146
146
|
lastTriggeredMinute[triggerKey] = true;
|
|
147
|
-
|
|
148
|
-
for (
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
const keys = Object.keys(lastTriggeredMinute);
|
|
148
|
+
for (let k = 0; k < keys.length; k++) {
|
|
149
|
+
const keyParts = keys[k].split("_");
|
|
150
|
+
const keyMinute = parseInt(keyParts[keyParts.length - 1], 10);
|
|
151
151
|
if (keyMinute < nowMinuteKey - 1) {
|
|
152
152
|
delete lastTriggeredMinute[keys[k]];
|
|
153
153
|
}
|
|
@@ -180,12 +180,12 @@ export function listTasks() {
|
|
|
180
180
|
return tasks.slice();
|
|
181
181
|
}
|
|
182
182
|
export function createTask(data) {
|
|
183
|
-
|
|
183
|
+
const parsed = parseCron(data.cron);
|
|
184
184
|
if (!parsed) {
|
|
185
185
|
return null;
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const task = {
|
|
189
189
|
id: "task_" + now + "_" + randomBytes(3).toString("hex"),
|
|
190
190
|
name: data.name,
|
|
191
191
|
prompt: data.prompt,
|
|
@@ -202,8 +202,8 @@ export function createTask(data) {
|
|
|
202
202
|
return task;
|
|
203
203
|
}
|
|
204
204
|
export function updateTask(taskId, data) {
|
|
205
|
-
|
|
206
|
-
for (
|
|
205
|
+
let task = null;
|
|
206
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
207
207
|
if (tasks[i].id === taskId) {
|
|
208
208
|
task = tasks[i];
|
|
209
209
|
break;
|
|
@@ -212,7 +212,7 @@ export function updateTask(taskId, data) {
|
|
|
212
212
|
if (!task)
|
|
213
213
|
return null;
|
|
214
214
|
if (data.cron && data.cron !== task.cron) {
|
|
215
|
-
|
|
215
|
+
const parsed = parseCron(data.cron);
|
|
216
216
|
if (!parsed)
|
|
217
217
|
return null;
|
|
218
218
|
task.cron = data.cron;
|
|
@@ -227,8 +227,8 @@ export function updateTask(taskId, data) {
|
|
|
227
227
|
return task;
|
|
228
228
|
}
|
|
229
229
|
export function deleteTask(taskId) {
|
|
230
|
-
|
|
231
|
-
for (
|
|
230
|
+
let idx = -1;
|
|
231
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
232
232
|
if (tasks[i].id === taskId) {
|
|
233
233
|
idx = i;
|
|
234
234
|
break;
|
|
@@ -242,8 +242,8 @@ export function deleteTask(taskId) {
|
|
|
242
242
|
return true;
|
|
243
243
|
}
|
|
244
244
|
export function toggleTask(taskId) {
|
|
245
|
-
|
|
246
|
-
for (
|
|
245
|
+
let task = null;
|
|
246
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
247
247
|
if (tasks[i].id === taskId) {
|
|
248
248
|
task = tasks[i];
|
|
249
249
|
break;
|