clay-server 2.27.0-beta.8 → 2.27.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/README.md +10 -0
- package/lib/daemon-projects.js +164 -0
- package/lib/daemon.js +13 -126
- package/lib/mates-identity.js +132 -0
- package/lib/mates-knowledge.js +113 -0
- package/lib/mates-prompts.js +398 -0
- package/lib/mates.js +40 -599
- package/lib/project-connection.js +2 -0
- package/lib/project-debate.js +19 -12
- package/lib/project-http.js +4 -2
- package/lib/project-loop.js +110 -48
- package/lib/project-mate-interaction.js +4 -0
- package/lib/project-notifications.js +210 -0
- package/lib/project-sessions.js +5 -2
- package/lib/project-user-message.js +2 -1
- package/lib/project.js +26 -2
- package/lib/public/app.js +1193 -8521
- package/lib/public/css/command-palette.css +14 -0
- package/lib/public/css/loop.css +301 -0
- package/lib/public/css/notifications-center.css +190 -0
- package/lib/public/css/rewind.css +6 -0
- package/lib/public/index.html +89 -35
- package/lib/public/modules/app-connection.js +160 -0
- package/lib/public/modules/app-cursors.js +473 -0
- package/lib/public/modules/app-debate-ui.js +389 -0
- package/lib/public/modules/app-dm.js +627 -0
- package/lib/public/modules/app-favicon.js +212 -0
- package/lib/public/modules/app-header.js +229 -0
- package/lib/public/modules/app-home-hub.js +600 -0
- package/lib/public/modules/app-loop-ui.js +589 -0
- package/lib/public/modules/app-loop-wizard.js +439 -0
- package/lib/public/modules/app-messages.js +1560 -0
- package/lib/public/modules/app-misc.js +299 -0
- package/lib/public/modules/app-notifications.js +372 -0
- package/lib/public/modules/app-panels.js +888 -0
- package/lib/public/modules/app-projects.js +798 -0
- package/lib/public/modules/app-rate-limit.js +451 -0
- package/lib/public/modules/app-rendering.js +597 -0
- package/lib/public/modules/app-skills-install.js +234 -0
- package/lib/public/modules/command-palette.js +27 -4
- package/lib/public/modules/input.js +31 -20
- package/lib/public/modules/scheduler-config.js +1532 -0
- package/lib/public/modules/scheduler-history.js +79 -0
- package/lib/public/modules/scheduler.js +33 -1554
- package/lib/public/modules/session-search.js +13 -1
- package/lib/public/modules/sidebar-mates.js +812 -0
- package/lib/public/modules/sidebar-mobile.js +1269 -0
- package/lib/public/modules/sidebar-projects.js +1449 -0
- package/lib/public/modules/sidebar-sessions.js +986 -0
- package/lib/public/modules/sidebar.js +232 -4591
- package/lib/public/modules/store.js +27 -0
- package/lib/public/modules/ws-ref.js +7 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +96 -717
- package/lib/sdk-message-processor.js +587 -0
- package/lib/sdk-message-queue.js +42 -0
- package/lib/sdk-skill-discovery.js +131 -0
- package/lib/server-admin.js +712 -0
- package/lib/server-auth.js +737 -0
- package/lib/server-dm.js +221 -0
- package/lib/server-mates.js +281 -0
- package/lib/server-palette.js +110 -0
- package/lib/server-settings.js +479 -0
- package/lib/server-skills.js +280 -0
- package/lib/server.js +246 -2755
- package/lib/sessions.js +11 -4
- package/lib/users-auth.js +146 -0
- package/lib/users-permissions.js +118 -0
- package/lib/users-preferences.js +210 -0
- package/lib/users.js +48 -398
- package/lib/ws-schema.js +498 -0
- package/package.json +1 -1
package/lib/project-debate.js
CHANGED
|
@@ -721,6 +721,13 @@ function attachDebate(ctx) {
|
|
|
721
721
|
ctx.send({ type: "mention_processing", mateId: mateId, active: active });
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
+
// Persist a debate message to session history and send to clients
|
|
725
|
+
function debateSendAndRecord(session, msg) {
|
|
726
|
+
session.history.push(msg);
|
|
727
|
+
ctx.sm.appendToSessionFile(session, msg);
|
|
728
|
+
ctx.sendToSession(session.localId, msg);
|
|
729
|
+
}
|
|
730
|
+
|
|
724
731
|
// --- Live debate ---
|
|
725
732
|
|
|
726
733
|
function startDebateLive(session) {
|
|
@@ -771,7 +778,7 @@ function attachDebate(ctx) {
|
|
|
771
778
|
|
|
772
779
|
// Signal moderator's first turn
|
|
773
780
|
debateMateProcessing(debate.moderatorId, true);
|
|
774
|
-
|
|
781
|
+
debateSendAndRecord(debateSession, {
|
|
775
782
|
type: "debate_turn",
|
|
776
783
|
mateId: debate.moderatorId,
|
|
777
784
|
mateName: moderatorProfile.name,
|
|
@@ -798,7 +805,7 @@ function attachDebate(ctx) {
|
|
|
798
805
|
},
|
|
799
806
|
onDelta: function (delta) {
|
|
800
807
|
if (debateSession._debate && debateSession._debate.phase !== "ended") {
|
|
801
|
-
|
|
808
|
+
debateSendAndRecord(debateSession, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
802
809
|
}
|
|
803
810
|
},
|
|
804
811
|
onDone: function (fullText) {
|
|
@@ -906,7 +913,7 @@ function attachDebate(ctx) {
|
|
|
906
913
|
|
|
907
914
|
// Notify clients of new turn
|
|
908
915
|
debateMateProcessing(mateId, true);
|
|
909
|
-
|
|
916
|
+
debateSendAndRecord(session, {
|
|
910
917
|
type: "debate_turn",
|
|
911
918
|
mateId: mateId,
|
|
912
919
|
mateName: profile.name,
|
|
@@ -926,7 +933,7 @@ function attachDebate(ctx) {
|
|
|
926
933
|
onDelta: function (delta) {
|
|
927
934
|
if (session._debate && session._debate.phase !== "ended") {
|
|
928
935
|
debate._currentTurnText += delta;
|
|
929
|
-
|
|
936
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: mateId, mateName: profile.name, delta: delta });
|
|
930
937
|
}
|
|
931
938
|
},
|
|
932
939
|
onDone: function (fullText) {
|
|
@@ -1071,7 +1078,7 @@ function attachDebate(ctx) {
|
|
|
1071
1078
|
|
|
1072
1079
|
// Notify clients of moderator turn
|
|
1073
1080
|
debateMateProcessing(debate.moderatorId, true);
|
|
1074
|
-
|
|
1081
|
+
debateSendAndRecord(session, {
|
|
1075
1082
|
type: "debate_turn",
|
|
1076
1083
|
mateId: debate.moderatorId,
|
|
1077
1084
|
mateName: moderatorProfile.name,
|
|
@@ -1101,7 +1108,7 @@ function attachDebate(ctx) {
|
|
|
1101
1108
|
},
|
|
1102
1109
|
onDelta: function (delta) {
|
|
1103
1110
|
if (session._debate && session._debate.phase !== "ended") {
|
|
1104
|
-
|
|
1111
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1105
1112
|
}
|
|
1106
1113
|
},
|
|
1107
1114
|
onDone: function (fullText) {
|
|
@@ -1168,7 +1175,7 @@ function attachDebate(ctx) {
|
|
|
1168
1175
|
debate.turnInProgress = true;
|
|
1169
1176
|
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1170
1177
|
|
|
1171
|
-
|
|
1178
|
+
debateSendAndRecord(session, {
|
|
1172
1179
|
type: "debate_turn",
|
|
1173
1180
|
mateId: debate.moderatorId,
|
|
1174
1181
|
mateName: moderatorProfile.name,
|
|
@@ -1207,7 +1214,7 @@ function attachDebate(ctx) {
|
|
|
1207
1214
|
debate.turnInProgress = true;
|
|
1208
1215
|
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1209
1216
|
|
|
1210
|
-
|
|
1217
|
+
debateSendAndRecord(session, {
|
|
1211
1218
|
type: "debate_turn",
|
|
1212
1219
|
mateId: debate.moderatorId,
|
|
1213
1220
|
mateName: moderatorProfile.name,
|
|
@@ -1238,7 +1245,7 @@ function attachDebate(ctx) {
|
|
|
1238
1245
|
},
|
|
1239
1246
|
onDelta: function (delta) {
|
|
1240
1247
|
if (session._debate && session._debate.phase !== "ended") {
|
|
1241
|
-
|
|
1248
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1242
1249
|
}
|
|
1243
1250
|
},
|
|
1244
1251
|
onDone: function (fullText) {
|
|
@@ -1287,7 +1294,7 @@ function attachDebate(ctx) {
|
|
|
1287
1294
|
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1288
1295
|
|
|
1289
1296
|
debateMateProcessing(debate.moderatorId, true);
|
|
1290
|
-
|
|
1297
|
+
debateSendAndRecord(session, {
|
|
1291
1298
|
type: "debate_turn",
|
|
1292
1299
|
mateId: debate.moderatorId,
|
|
1293
1300
|
mateName: moderatorProfile.name,
|
|
@@ -1477,7 +1484,7 @@ function attachDebate(ctx) {
|
|
|
1477
1484
|
|
|
1478
1485
|
debate.turnInProgress = true;
|
|
1479
1486
|
debateMateProcessing(debate.moderatorId, true);
|
|
1480
|
-
|
|
1487
|
+
debateSendAndRecord(session, {
|
|
1481
1488
|
type: "debate_turn",
|
|
1482
1489
|
mateId: debate.moderatorId,
|
|
1483
1490
|
mateName: moderatorProfile.name,
|
|
@@ -1525,7 +1532,7 @@ function attachDebate(ctx) {
|
|
|
1525
1532
|
},
|
|
1526
1533
|
onDelta: function (delta) {
|
|
1527
1534
|
if (session._debate && session._debate.phase !== "ended") {
|
|
1528
|
-
|
|
1535
|
+
debateSendAndRecord(session, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1529
1536
|
}
|
|
1530
1537
|
},
|
|
1531
1538
|
onDone: function (fullText) {
|
package/lib/project-http.js
CHANGED
|
@@ -629,11 +629,13 @@ function attachHTTP(ctx) {
|
|
|
629
629
|
var execSync = require("child_process").execSync;
|
|
630
630
|
try {
|
|
631
631
|
var out = execSync("git status --porcelain", { cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
632
|
-
var
|
|
632
|
+
var lines = out.trim().split("\n").filter(function (line) {
|
|
633
633
|
return line.trim().length > 0 && !line.startsWith("??");
|
|
634
634
|
});
|
|
635
|
+
var dirty = lines.length > 0;
|
|
636
|
+
var files = lines.map(function (line) { return line.trim(); });
|
|
635
637
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
636
|
-
res.end(JSON.stringify({ dirty: dirty }));
|
|
638
|
+
res.end(JSON.stringify({ dirty: dirty, files: files }));
|
|
637
639
|
} catch (e) {
|
|
638
640
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
639
641
|
res.end(JSON.stringify({ dirty: false }));
|
package/lib/project-loop.js
CHANGED
|
@@ -21,6 +21,7 @@ function attachLoop(ctx) {
|
|
|
21
21
|
var sendTo = ctx.sendTo;
|
|
22
22
|
var sendToSession = ctx.sendToSession;
|
|
23
23
|
var pushModule = ctx.pushModule;
|
|
24
|
+
var notificationsModule = ctx.notificationsModule;
|
|
24
25
|
var getHubSchedules = ctx.getHubSchedules;
|
|
25
26
|
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
26
27
|
var onProcessingChanged = ctx.onProcessingChanged;
|
|
@@ -134,13 +135,16 @@ function attachLoop(ctx) {
|
|
|
134
135
|
var _ld = path.join(_loopsBase, _loopDirs[_li]);
|
|
135
136
|
try {
|
|
136
137
|
fs.accessSync(path.join(_ld, "PROMPT.md"));
|
|
137
|
-
fs.accessSync(path.join(_ld, "JUDGE.md"));
|
|
138
138
|
fs.accessSync(path.join(_ld, "LOOP.json"));
|
|
139
|
+
var _loopCfg = JSON.parse(fs.readFileSync(path.join(_ld, "LOOP.json"), "utf8"));
|
|
140
|
+
var _isSimple = _loopCfg.loopMode === "simple";
|
|
141
|
+
if (!_isSimple) fs.accessSync(path.join(_ld, "JUDGE.md"));
|
|
139
142
|
// Found a completed loop — recover to approval phase
|
|
140
143
|
loopState.loopId = _loopDirs[_li];
|
|
141
144
|
loopState.phase = "approval";
|
|
142
|
-
var _loopCfg = JSON.parse(fs.readFileSync(path.join(_ld, "LOOP.json"), "utf8"));
|
|
143
145
|
loopState.maxIterations = _loopCfg.maxIterations || 20;
|
|
146
|
+
if (!loopState.wizardData) loopState.wizardData = {};
|
|
147
|
+
loopState.wizardData.loopMode = _loopCfg.loopMode || "judge";
|
|
144
148
|
saveLoopState();
|
|
145
149
|
console.log("[ralph-loop] Recovered orphaned loop: " + _loopDirs[_li]);
|
|
146
150
|
break;
|
|
@@ -177,7 +181,8 @@ function attachLoop(ctx) {
|
|
|
177
181
|
var hasJudge = false;
|
|
178
182
|
try { fs.accessSync(path.join(dir, "PROMPT.md")); hasPrompt = true; } catch (e) {}
|
|
179
183
|
try { fs.accessSync(path.join(dir, "JUDGE.md")); hasJudge = true; } catch (e) {}
|
|
180
|
-
|
|
184
|
+
var isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
185
|
+
return isSimple ? hasPrompt : (hasPrompt && hasJudge);
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
// .claude/ directory watcher for PROMPT.md / JUDGE.md
|
|
@@ -223,16 +228,18 @@ function attachLoop(ctx) {
|
|
|
223
228
|
try { fs.accessSync(path.join(dir, "JUDGE.md")); hasJudge = true; } catch (e) {}
|
|
224
229
|
try { fs.accessSync(path.join(dir, "LOOP.json")); hasLoopJson = true; } catch (e) {}
|
|
225
230
|
}
|
|
231
|
+
var isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
232
|
+
var bothReady = isSimple ? hasPrompt : (hasPrompt && hasJudge);
|
|
226
233
|
send({
|
|
227
234
|
type: "ralph_files_status",
|
|
228
235
|
promptReady: hasPrompt,
|
|
229
236
|
judgeReady: hasJudge,
|
|
230
237
|
loopJsonReady: hasLoopJson,
|
|
231
|
-
bothReady:
|
|
238
|
+
bothReady: bothReady,
|
|
232
239
|
taskId: loopState.loopId,
|
|
233
240
|
});
|
|
234
|
-
// Auto-transition to approval phase when
|
|
235
|
-
if (
|
|
241
|
+
// Auto-transition to approval phase when files are ready
|
|
242
|
+
if (bothReady && loopState.phase === "crafting") {
|
|
236
243
|
loopState.phase = "approval";
|
|
237
244
|
saveLoopState();
|
|
238
245
|
|
|
@@ -298,7 +305,10 @@ function attachLoop(ctx) {
|
|
|
298
305
|
// Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
|
|
299
306
|
loopState.loopId = record.id;
|
|
300
307
|
loopState.loopFilesId = loopFilesId;
|
|
301
|
-
|
|
308
|
+
// Restore loopMode from LOOP.json so simple loops work correctly on trigger
|
|
309
|
+
var _triggerCfg = {};
|
|
310
|
+
try { _triggerCfg = JSON.parse(fs.readFileSync(path.join(recDir, "LOOP.json"), "utf8")); } catch (e) {}
|
|
311
|
+
loopState.wizardData = { loopMode: _triggerCfg.loopMode || "judge" };
|
|
302
312
|
activeRegistryId = record.id;
|
|
303
313
|
console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopState.loopId + ")");
|
|
304
314
|
send({ type: "schedule_run_started", recordId: record.id });
|
|
@@ -356,12 +366,17 @@ function attachLoop(ctx) {
|
|
|
356
366
|
loopConfig = JSON.parse(fs.readFileSync(path.join(dir, "LOOP.json"), "utf8"));
|
|
357
367
|
} catch (e) {}
|
|
358
368
|
|
|
369
|
+
var isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
359
370
|
loopState.active = true;
|
|
360
371
|
loopState.phase = "executing";
|
|
361
372
|
loopState.promptText = promptText;
|
|
362
|
-
loopState.judgeText = judgeText;
|
|
373
|
+
loopState.judgeText = isSimple ? null : judgeText;
|
|
363
374
|
loopState.iteration = 0;
|
|
364
|
-
|
|
375
|
+
if (isSimple) {
|
|
376
|
+
loopState.maxIterations = (loopOpts.maxIterations >= 1 ? loopOpts.maxIterations : null) || loopConfig.maxIterations || 5;
|
|
377
|
+
} else {
|
|
378
|
+
loopState.maxIterations = judgeText ? ((loopOpts.maxIterations >= 1 ? loopOpts.maxIterations : null) || loopConfig.maxIterations || 20) : 1;
|
|
379
|
+
}
|
|
365
380
|
loopState.baseCommit = baseCommit;
|
|
366
381
|
loopState.currentSessionId = null;
|
|
367
382
|
loopState.judgeSessionId = null;
|
|
@@ -440,7 +455,15 @@ function attachLoop(ctx) {
|
|
|
440
455
|
setTimeout(function() { runNextIteration(); }, 2000);
|
|
441
456
|
return;
|
|
442
457
|
}
|
|
443
|
-
|
|
458
|
+
var _isSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
459
|
+
if (_isSimple) {
|
|
460
|
+
// Simple mode: no judge, proceed to next iteration or finish
|
|
461
|
+
if (loopState.iteration >= loopState.maxIterations) {
|
|
462
|
+
finishLoop("complete");
|
|
463
|
+
} else {
|
|
464
|
+
setTimeout(function() { runNextIteration(); }, 1000);
|
|
465
|
+
}
|
|
466
|
+
} else if (loopState.judgeText && loopState.maxIterations > 1) {
|
|
444
467
|
runJudge();
|
|
445
468
|
} else {
|
|
446
469
|
finishLoop("pass");
|
|
@@ -648,19 +671,23 @@ function attachLoop(ctx) {
|
|
|
648
671
|
}
|
|
649
672
|
|
|
650
673
|
if (pushModule) {
|
|
651
|
-
var
|
|
652
|
-
? "
|
|
674
|
+
var _finishBody = reason === "pass" || reason === "complete"
|
|
675
|
+
? "Completed after " + loopState.iteration + " iteration(s)"
|
|
653
676
|
: reason === "max_iterations"
|
|
654
677
|
? "Reached max iterations (" + loopState.maxIterations + ")"
|
|
655
|
-
: reason === "stopped"
|
|
656
|
-
? "Loop stopped by user"
|
|
657
|
-
: "Loop ended due to error";
|
|
678
|
+
: reason === "stopped" ? "Stopped by user" : "Ended due to error";
|
|
658
679
|
pushModule.sendPush({
|
|
659
|
-
type: "done",
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
680
|
+
type: "done", slug: slug, title: "Loop Complete", body: _finishBody, tag: "ralph-loop-done",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (notificationsModule) {
|
|
685
|
+
notificationsModule.notify("loop_complete", {
|
|
686
|
+
reason: reason,
|
|
687
|
+
name: loopState.name,
|
|
688
|
+
iterations: loopState.iteration,
|
|
689
|
+
maxIterations: loopState.maxIterations,
|
|
690
|
+
sessionId: loopState.currentSessionId,
|
|
664
691
|
});
|
|
665
692
|
}
|
|
666
693
|
}
|
|
@@ -683,14 +710,19 @@ function attachLoop(ctx) {
|
|
|
683
710
|
saveLoopState();
|
|
684
711
|
return;
|
|
685
712
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
713
|
+
var _isSimpleResume = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
714
|
+
if (!_isSimpleResume) {
|
|
715
|
+
try {
|
|
716
|
+
loopState.judgeText = fs.readFileSync(path.join(dir, "JUDGE.md"), "utf8");
|
|
717
|
+
} catch (e) {
|
|
718
|
+
console.error("[ralph-loop] Cannot resume: missing JUDGE.md");
|
|
719
|
+
loopState.active = false;
|
|
720
|
+
loopState.phase = "idle";
|
|
721
|
+
saveLoopState();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
loopState.judgeText = null;
|
|
694
726
|
}
|
|
695
727
|
// Retry the interrupted iteration (runNextIteration will increment)
|
|
696
728
|
if (loopState.iteration > 0) {
|
|
@@ -742,7 +774,7 @@ function attachLoop(ctx) {
|
|
|
742
774
|
send({ type: "loop_scheduled", recordId: loopState.loopId, cron: loopState.wizardData.cron });
|
|
743
775
|
return true;
|
|
744
776
|
}
|
|
745
|
-
startLoop();
|
|
777
|
+
startLoop({ maxIterations: msg.maxIterations });
|
|
746
778
|
return true;
|
|
747
779
|
}
|
|
748
780
|
|
|
@@ -753,7 +785,7 @@ function attachLoop(ctx) {
|
|
|
753
785
|
|
|
754
786
|
if (msg.type === "ralph_wizard_complete") {
|
|
755
787
|
var wData = msg.data || {};
|
|
756
|
-
var maxIter = wData.maxIterations ||
|
|
788
|
+
var maxIter = wData.maxIterations || null;
|
|
757
789
|
var wizardCron = wData.cron || null;
|
|
758
790
|
var newLoopId = generateLoopId();
|
|
759
791
|
loopState.loopId = newLoopId;
|
|
@@ -762,6 +794,9 @@ function attachLoop(ctx) {
|
|
|
762
794
|
task: wData.task || "",
|
|
763
795
|
maxIterations: maxIter,
|
|
764
796
|
cron: wizardCron,
|
|
797
|
+
loopMode: wData.loopMode || "judge",
|
|
798
|
+
promptAuthor: wData.promptAuthor || "clay",
|
|
799
|
+
judgeAuthor: wData.judgeAuthor || null,
|
|
765
800
|
};
|
|
766
801
|
loopState.phase = "crafting";
|
|
767
802
|
loopState.startedAt = Date.now();
|
|
@@ -784,7 +819,7 @@ function attachLoop(ctx) {
|
|
|
784
819
|
try { fs.mkdirSync(lDir, { recursive: true }); } catch (e) {}
|
|
785
820
|
var loopJsonPath = path.join(lDir, "LOOP.json");
|
|
786
821
|
var tmpLoopJson = loopJsonPath + ".tmp";
|
|
787
|
-
fs.writeFileSync(tmpLoopJson, JSON.stringify({ maxIterations: maxIter }, null, 2));
|
|
822
|
+
fs.writeFileSync(tmpLoopJson, JSON.stringify({ maxIterations: maxIter, loopMode: wData.loopMode || "judge" }, null, 2));
|
|
788
823
|
fs.renameSync(tmpLoopJson, loopJsonPath);
|
|
789
824
|
|
|
790
825
|
var craftName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
@@ -804,19 +839,15 @@ function attachLoop(ctx) {
|
|
|
804
839
|
var tmpJudge = judgePath + ".tmp";
|
|
805
840
|
fs.writeFileSync(tmpJudge, wData.judgeText);
|
|
806
841
|
fs.renameSync(tmpJudge, judgePath);
|
|
807
|
-
} else if (!recordSource) {
|
|
808
|
-
//
|
|
809
|
-
var singleJson = loopJsonPath + ".tmp2";
|
|
810
|
-
fs.writeFileSync(singleJson, JSON.stringify({ maxIterations: 1 }, null, 2));
|
|
811
|
-
fs.renameSync(singleJson, loopJsonPath);
|
|
812
|
-
|
|
842
|
+
} else if (wData.loopMode === "simple" || !recordSource) {
|
|
843
|
+
// Simple loop or scheduled task with no judge: go straight to approval
|
|
813
844
|
loopState.phase = "approval";
|
|
814
845
|
saveLoopState();
|
|
815
846
|
send({ type: "ralph_phase", phase: "approval", source: recordSource, wizardData: loopState.wizardData });
|
|
816
847
|
send({ type: "ralph_files_status", promptReady: true, judgeReady: false, bothReady: true });
|
|
817
848
|
return true;
|
|
818
849
|
} else {
|
|
819
|
-
// Ralph with no judge: start a crafting session to create JUDGE.md
|
|
850
|
+
// Ralph with judge mode but no judge provided: start a crafting session to create JUDGE.md
|
|
820
851
|
loopState.phase = "crafting";
|
|
821
852
|
saveLoopState();
|
|
822
853
|
|
|
@@ -863,13 +894,39 @@ function attachLoop(ctx) {
|
|
|
863
894
|
return true;
|
|
864
895
|
}
|
|
865
896
|
|
|
866
|
-
// Default: "draft" mode — Clay crafts
|
|
867
|
-
var
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
"
|
|
871
|
-
|
|
872
|
-
|
|
897
|
+
// Default: "draft" mode — Clay crafts files via the clay-ralph skill
|
|
898
|
+
var _draftIsSimple = wData.loopMode === "simple";
|
|
899
|
+
var craftingPrompt;
|
|
900
|
+
if (_draftIsSimple) {
|
|
901
|
+
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
902
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
903
|
+
"This is a Simple Loop (no judge). Your job is to create ONLY a PROMPT.md file " +
|
|
904
|
+
"that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
|
|
905
|
+
"## Task\n" + (wData.task || "") +
|
|
906
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
907
|
+
} else if (wData.judgeAuthor === "me") {
|
|
908
|
+
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
909
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
910
|
+
"The user will provide their own JUDGE.md, so create ONLY a PROMPT.md file " +
|
|
911
|
+
"that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
|
|
912
|
+
"## Task\n" + (wData.task || "") +
|
|
913
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
914
|
+
} else {
|
|
915
|
+
craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
|
|
916
|
+
"You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
|
|
917
|
+
"Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
|
|
918
|
+
"that a future autonomous session will execute.\n\n" +
|
|
919
|
+
"## Task\n" + (wData.task || "") +
|
|
920
|
+
"\n\n## Loop Directory\n" + lDir;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Pre-write user-provided files before crafting starts
|
|
924
|
+
if (wData.judgeText && wData.judgeAuthor === "me") {
|
|
925
|
+
var _judgePathDraft = path.join(lDir, "JUDGE.md");
|
|
926
|
+
var _tmpJudgeDraft = _judgePathDraft + ".tmp";
|
|
927
|
+
fs.writeFileSync(_tmpJudgeDraft, wData.judgeText);
|
|
928
|
+
fs.renameSync(_tmpJudgeDraft, _judgePathDraft);
|
|
929
|
+
}
|
|
873
930
|
|
|
874
931
|
// Create a new session for crafting
|
|
875
932
|
var craftingSession = sm.createSession();
|
|
@@ -1088,10 +1145,11 @@ function attachLoop(ctx) {
|
|
|
1088
1145
|
// --- Connection state: send loop state to newly connected client ---
|
|
1089
1146
|
function sendConnectionState(ws) {
|
|
1090
1147
|
// Ralph Loop availability
|
|
1148
|
+
var _connIsSimple = loopState.wizardData && loopState.wizardData.loopMode === "simple";
|
|
1091
1149
|
var hasLoopFiles = false;
|
|
1092
1150
|
try {
|
|
1093
1151
|
fs.accessSync(path.join(cwd, ".claude", "PROMPT.md"));
|
|
1094
|
-
fs.accessSync(path.join(cwd, ".claude", "JUDGE.md"));
|
|
1152
|
+
if (!_connIsSimple) fs.accessSync(path.join(cwd, ".claude", "JUDGE.md"));
|
|
1095
1153
|
hasLoopFiles = true;
|
|
1096
1154
|
} catch (e) {}
|
|
1097
1155
|
// Also check loop directory files
|
|
@@ -1100,7 +1158,7 @@ function attachLoop(ctx) {
|
|
|
1100
1158
|
if (_avDir) {
|
|
1101
1159
|
try {
|
|
1102
1160
|
fs.accessSync(path.join(_avDir, "PROMPT.md"));
|
|
1103
|
-
fs.accessSync(path.join(_avDir, "JUDGE.md"));
|
|
1161
|
+
if (!_connIsSimple) fs.accessSync(path.join(_avDir, "JUDGE.md"));
|
|
1104
1162
|
hasLoopFiles = true;
|
|
1105
1163
|
} catch (e) {}
|
|
1106
1164
|
}
|
|
@@ -1115,11 +1173,14 @@ function attachLoop(ctx) {
|
|
|
1115
1173
|
});
|
|
1116
1174
|
|
|
1117
1175
|
// Ralph phase state
|
|
1176
|
+
// Derive source from wizardData for reconnect (so client can distinguish ralph vs task)
|
|
1177
|
+
var _connSource = (loopState.wizardData && loopState.wizardData.source === "task") ? null : "ralph";
|
|
1118
1178
|
sendTo(ws, {
|
|
1119
1179
|
type: "ralph_phase",
|
|
1120
1180
|
phase: loopState.phase,
|
|
1121
1181
|
wizardData: loopState.wizardData,
|
|
1122
1182
|
craftingSessionId: loopState.craftingSessionId || null,
|
|
1183
|
+
source: _connSource,
|
|
1123
1184
|
});
|
|
1124
1185
|
if (loopState.phase === "crafting" || loopState.phase === "approval") {
|
|
1125
1186
|
var _hasPrompt = false;
|
|
@@ -1129,11 +1190,12 @@ function attachLoop(ctx) {
|
|
|
1129
1190
|
try { fs.accessSync(path.join(_lDir, "PROMPT.md")); _hasPrompt = true; } catch (e) {}
|
|
1130
1191
|
try { fs.accessSync(path.join(_lDir, "JUDGE.md")); _hasJudge = true; } catch (e) {}
|
|
1131
1192
|
}
|
|
1193
|
+
var _connBothReady = _connIsSimple ? _hasPrompt : (_hasPrompt && _hasJudge);
|
|
1132
1194
|
sendTo(ws, {
|
|
1133
1195
|
type: "ralph_files_status",
|
|
1134
1196
|
promptReady: _hasPrompt,
|
|
1135
1197
|
judgeReady: _hasJudge,
|
|
1136
|
-
bothReady:
|
|
1198
|
+
bothReady: _connBothReady,
|
|
1137
1199
|
taskId: loopState.loopId,
|
|
1138
1200
|
});
|
|
1139
1201
|
}
|
|
@@ -569,6 +569,10 @@ function attachMateInteraction(ctx) {
|
|
|
569
569
|
onDone: mentionCallbacks.onDone,
|
|
570
570
|
onError: mentionCallbacks.onError,
|
|
571
571
|
canUseTool: function (toolName, input, toolOpts) {
|
|
572
|
+
// Full-auto mode: auto-approve everything except AskUserQuestion
|
|
573
|
+
if (sm.currentPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
|
|
574
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
575
|
+
}
|
|
572
576
|
// Use the shared whitelist from sdk-bridge (read-only tools + safe bash commands)
|
|
573
577
|
var whitelisted = sdk.checkToolWhitelist(toolName, input);
|
|
574
578
|
if (whitelisted) return Promise.resolve(whitelisted);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// project-notifications.js - Global notification queue, event-based creation
|
|
2
|
+
// Single instance shared by all projects. All formatting logic lives here.
|
|
3
|
+
// Follows attachXxx(ctx) pattern per MODULE_MAP.md
|
|
4
|
+
|
|
5
|
+
var fs = require("fs");
|
|
6
|
+
var path = require("path");
|
|
7
|
+
var config = require("./config");
|
|
8
|
+
|
|
9
|
+
var NOTIF_FILE = path.join(config.CONFIG_DIR, "notifications.json");
|
|
10
|
+
var REMINDER_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
11
|
+
|
|
12
|
+
function generateId() {
|
|
13
|
+
var rand = Math.random().toString(36).substring(2, 8);
|
|
14
|
+
return "n_" + Date.now() + "_" + rand;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ========================================================
|
|
18
|
+
// Event -> notification formatters
|
|
19
|
+
// ========================================================
|
|
20
|
+
|
|
21
|
+
var formatters = {
|
|
22
|
+
loop_complete: function (data) {
|
|
23
|
+
var reason = data.reason || "complete";
|
|
24
|
+
var body = (reason === "pass" || reason === "complete")
|
|
25
|
+
? "Completed after " + (data.iterations || 0) + " iteration(s)"
|
|
26
|
+
: reason === "max_iterations"
|
|
27
|
+
? "Reached max iterations (" + (data.maxIterations || "?") + ")"
|
|
28
|
+
: reason === "stopped"
|
|
29
|
+
? "Stopped by user"
|
|
30
|
+
: "Ended due to error";
|
|
31
|
+
return {
|
|
32
|
+
type: reason === "error" ? "loop_error" : "loop_complete",
|
|
33
|
+
title: (data.name || "Loop") + " " + (reason === "error" ? "failed" : "complete"),
|
|
34
|
+
body: body,
|
|
35
|
+
meta: { reason: reason, iterations: data.iterations },
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
response_done: function (data) {
|
|
40
|
+
return {
|
|
41
|
+
type: "response_done",
|
|
42
|
+
title: data.title || "Response ready",
|
|
43
|
+
body: data.preview || "",
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
permission_request: function (data) {
|
|
48
|
+
return {
|
|
49
|
+
type: "permission_request",
|
|
50
|
+
title: data.title || "Permission requested",
|
|
51
|
+
body: data.body || "",
|
|
52
|
+
meta: { requestId: data.requestId, toolName: data.toolName, toolInput: data.toolInput || null },
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
mate_dm: function (data) {
|
|
57
|
+
return {
|
|
58
|
+
type: "mate_dm",
|
|
59
|
+
title: data.senderName || "Message",
|
|
60
|
+
body: data.preview || "Sent you a message",
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ========================================================
|
|
66
|
+
// Module (global singleton)
|
|
67
|
+
// ========================================================
|
|
68
|
+
|
|
69
|
+
function attachNotifications(ctx) {
|
|
70
|
+
var broadcastAll = ctx.broadcastAll;
|
|
71
|
+
var pushModule = ctx.pushModule;
|
|
72
|
+
|
|
73
|
+
var notifications = loadNotifications();
|
|
74
|
+
var reminderTimer = null;
|
|
75
|
+
|
|
76
|
+
function loadNotifications() {
|
|
77
|
+
try {
|
|
78
|
+
var raw = fs.readFileSync(NOTIF_FILE, "utf8");
|
|
79
|
+
var parsed = JSON.parse(raw);
|
|
80
|
+
if (Array.isArray(parsed)) return parsed;
|
|
81
|
+
} catch (e) {}
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function saveNotifications() {
|
|
86
|
+
var tmp = NOTIF_FILE + ".tmp";
|
|
87
|
+
try {
|
|
88
|
+
fs.writeFileSync(tmp, JSON.stringify(notifications, null, 2));
|
|
89
|
+
fs.renameSync(tmp, NOTIF_FILE);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error("[notifications] Failed to save:", e.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getUnreadCount() {
|
|
96
|
+
return notifications.length;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Core API ---
|
|
100
|
+
|
|
101
|
+
function notify(event, data) {
|
|
102
|
+
var formatter = formatters[event];
|
|
103
|
+
if (!formatter) {
|
|
104
|
+
console.warn("[notifications] Unknown event: " + event);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
var formatted = formatter(data || {});
|
|
108
|
+
if (!formatted) return null;
|
|
109
|
+
|
|
110
|
+
var notif = {
|
|
111
|
+
id: generateId(),
|
|
112
|
+
type: formatted.type || event,
|
|
113
|
+
title: formatted.title || "",
|
|
114
|
+
body: formatted.body || "",
|
|
115
|
+
slug: data.slug || "",
|
|
116
|
+
sessionId: data.sessionId || null,
|
|
117
|
+
mateId: data.mateId || null,
|
|
118
|
+
ownerId: data.ownerId || null,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
meta: formatted.meta || {},
|
|
121
|
+
};
|
|
122
|
+
notifications.unshift(notif);
|
|
123
|
+
saveNotifications();
|
|
124
|
+
broadcastAll({
|
|
125
|
+
type: "notification_created",
|
|
126
|
+
notification: notif,
|
|
127
|
+
unreadCount: getUnreadCount(),
|
|
128
|
+
});
|
|
129
|
+
return notif;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function dismiss(ids) {
|
|
133
|
+
var before = notifications.length;
|
|
134
|
+
notifications = notifications.filter(function(n) { return ids.indexOf(n.id) === -1; });
|
|
135
|
+
if (notifications.length !== before) {
|
|
136
|
+
saveNotifications();
|
|
137
|
+
broadcastAll({
|
|
138
|
+
type: "notification_dismissed",
|
|
139
|
+
ids: ids,
|
|
140
|
+
unreadCount: getUnreadCount(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function dismissAll() {
|
|
146
|
+
if (notifications.length > 0) {
|
|
147
|
+
notifications = [];
|
|
148
|
+
saveNotifications();
|
|
149
|
+
broadcastAll({ type: "notification_dismissed_all", unreadCount: 0 });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Periodic reminder ---
|
|
154
|
+
|
|
155
|
+
function startReminder() {
|
|
156
|
+
if (reminderTimer) return;
|
|
157
|
+
reminderTimer = setInterval(function () {
|
|
158
|
+
var count = getUnreadCount();
|
|
159
|
+
if (count > 0 && pushModule) {
|
|
160
|
+
pushModule.sendPush({
|
|
161
|
+
type: "reminder",
|
|
162
|
+
title: "Clay",
|
|
163
|
+
body: count + " unread notification" + (count > 1 ? "s" : ""),
|
|
164
|
+
tag: "clay-notif-reminder",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}, REMINDER_INTERVAL);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function stopReminder() {
|
|
171
|
+
if (reminderTimer) { clearInterval(reminderTimer); reminderTimer = null; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
startReminder();
|
|
175
|
+
|
|
176
|
+
// --- Connection state (called per-client with sendTo) ---
|
|
177
|
+
|
|
178
|
+
function sendConnectionState(ws, sendTo) {
|
|
179
|
+
if (!sendTo) return;
|
|
180
|
+
sendTo(ws, {
|
|
181
|
+
type: "notifications_state",
|
|
182
|
+
notifications: notifications,
|
|
183
|
+
unreadCount: getUnreadCount(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- Message handler ---
|
|
188
|
+
|
|
189
|
+
function handleNotificationMessage(ws, msg) {
|
|
190
|
+
if (msg.type === "notification_dismiss") {
|
|
191
|
+
dismiss(msg.ids || []);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (msg.type === "notification_dismiss_all") {
|
|
195
|
+
dismissAll();
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
notify: notify,
|
|
203
|
+
getUnreadCount: getUnreadCount,
|
|
204
|
+
sendConnectionState: sendConnectionState,
|
|
205
|
+
handleNotificationMessage: handleNotificationMessage,
|
|
206
|
+
stopReminder: stopReminder,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { attachNotifications: attachNotifications };
|