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.
Files changed (72) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
@@ -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
- ctx.sendToSession(debateSession.localId, {
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
- ctx.sendToSession(debateSession.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: mateId, mateName: profile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, {
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
- ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
1535
+ debateSendAndRecord(session, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
1529
1536
  }
1530
1537
  },
1531
1538
  onDone: function (fullText) {
@@ -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 dirty = out.trim().split("\n").some(function (line) {
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 }));
@@ -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
- return hasPrompt && hasJudge;
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: hasPrompt && hasJudge,
238
+ bothReady: bothReady,
232
239
  taskId: loopState.loopId,
233
240
  });
234
- // Auto-transition to approval phase when both files appear
235
- if (hasPrompt && hasJudge && loopState.phase === "crafting") {
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
- loopState.wizardData = null;
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
- loopState.maxIterations = judgeText ? ((loopOpts.maxIterations >= 1 ? loopOpts.maxIterations : null) || loopConfig.maxIterations || 20) : 1;
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
- if (loopState.judgeText && loopState.maxIterations > 1) {
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 body = reason === "pass"
652
- ? "Task completed after " + loopState.iteration + " iteration(s)"
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
- slug: slug,
661
- title: "Ralph Loop Complete",
662
- body: body,
663
- tag: "ralph-loop-done",
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
- try {
687
- loopState.judgeText = fs.readFileSync(path.join(dir, "JUDGE.md"), "utf8");
688
- } catch (e) {
689
- console.error("[ralph-loop] Cannot resume: missing JUDGE.md");
690
- loopState.active = false;
691
- loopState.phase = "idle";
692
- saveLoopState();
693
- return;
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 || 3;
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
- // Scheduled task with no judge: force single iteration and go to approval
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 both PROMPT.md and JUDGE.md
867
- var craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
868
- "You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
869
- "Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
870
- "that a future autonomous session will execute.\n\n" +
871
- "## Task\n" + (wData.task || "") +
872
- "\n\n## Loop Directory\n" + lDir;
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: _hasPrompt && _hasJudge,
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 };