clay-server 4.1.0-beta.5 → 4.1.0-beta.7

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 (155) hide show
  1. package/LICENSE +661 -21
  2. package/LICENSES/MIT-legacy.txt +21 -0
  3. package/LICENSES/README.md +7 -0
  4. package/README.md +2 -2
  5. package/bin/cli.js +0 -2
  6. package/lib/autonomous-run-record.js +32 -0
  7. package/lib/autonomous-run-scheduler.js +92 -0
  8. package/lib/daemon.js +0 -2
  9. package/lib/default-ai-runtime.js +117 -0
  10. package/lib/durable-scheduler-record.js +61 -0
  11. package/lib/durable-scheduler-store.js +185 -0
  12. package/lib/durable-scheduler.js +495 -0
  13. package/lib/git-cli.js +1 -0
  14. package/lib/issue-work-session.js +10 -0
  15. package/lib/issues-comments.js +40 -0
  16. package/lib/issues-commit-verification.js +30 -0
  17. package/lib/issues-mcp-server.js +27 -0
  18. package/lib/issues-schema.js +119 -0
  19. package/lib/issues-service.js +339 -0
  20. package/lib/issues-store.js +210 -0
  21. package/lib/loop-cron.js +47 -0
  22. package/lib/loop-durable-schedule.js +159 -0
  23. package/lib/loop-guidance.js +35 -0
  24. package/lib/loop-registry-occurrence.js +76 -0
  25. package/lib/model-entry.js +15 -0
  26. package/lib/project-autonomous-run.js +498 -0
  27. package/lib/project-capsule-turn.js +1 -0
  28. package/lib/project-connection.js +6 -1
  29. package/lib/project-delegated-session.js +1 -0
  30. package/lib/project-filesystem.js +10 -10
  31. package/lib/project-full-access.js +86 -0
  32. package/lib/project-http.js +3 -1
  33. package/lib/project-issue-launch.js +154 -0
  34. package/lib/project-issues.js +58 -0
  35. package/lib/project-logs-mcp-server.js +8 -12
  36. package/lib/project-loop-interview.js +163 -0
  37. package/lib/project-loop.js +150 -64
  38. package/lib/project-message-delivery.js +4 -2
  39. package/lib/project-models.js +71 -3
  40. package/lib/project-pair-autonomous-stop.js +16 -0
  41. package/lib/project-pair-owned-stop.js +18 -0
  42. package/lib/project-pair-task-control.js +14 -1
  43. package/lib/project-pending-message-queue.js +264 -0
  44. package/lib/project-scheduled-messages.js +316 -0
  45. package/lib/project-scheduled-task-execution.js +236 -0
  46. package/lib/project-scheduled-tasks.js +481 -0
  47. package/lib/project-session-pair.js +16 -19
  48. package/lib/project-sessions.js +114 -31
  49. package/lib/project-tool-llm-runtime.js +48 -0
  50. package/lib/project-user-message.js +202 -60
  51. package/lib/project-workspace-query.js +25 -0
  52. package/lib/project.js +361 -69
  53. package/lib/public/app.js +85 -22
  54. package/lib/public/css/default-ai.css +33 -0
  55. package/lib/public/css/home-hub.css +15 -2
  56. package/lib/public/css/home-sidebar.css +50 -2
  57. package/lib/public/css/input.css +160 -5
  58. package/lib/public/css/issues.css +101 -0
  59. package/lib/public/css/messages.css +19 -0
  60. package/lib/public/css/pane.css +58 -7
  61. package/lib/public/css/pending-message-queue.css +187 -0
  62. package/lib/public/css/rewind.css +11 -2
  63. package/lib/public/css/scheduled-tasks.css +122 -0
  64. package/lib/public/index.html +42 -150
  65. package/lib/public/modules/app-connection.js +3 -0
  66. package/lib/public/modules/app-cursors.js +29 -12
  67. package/lib/public/modules/app-loop-ui.js +4 -8
  68. package/lib/public/modules/app-message-router.js +8 -0
  69. package/lib/public/modules/app-messages.js +34 -9
  70. package/lib/public/modules/app-projects.js +2 -0
  71. package/lib/public/modules/app-rate-limit.js +8 -5
  72. package/lib/public/modules/autonomous-run.js +285 -0
  73. package/lib/public/modules/clay-file-links.js +111 -0
  74. package/lib/public/modules/clay-issue-links.js +131 -0
  75. package/lib/public/modules/clay-log-links.js +2 -0
  76. package/lib/public/modules/cursor-sharing-preference.js +38 -0
  77. package/lib/public/modules/default-ai.js +377 -0
  78. package/lib/public/modules/filebrowser.js +43 -2
  79. package/lib/public/modules/home-sidebar.js +16 -8
  80. package/lib/public/modules/home-surface.js +5 -0
  81. package/lib/public/modules/input.js +11 -20
  82. package/lib/public/modules/issues-context.js +117 -0
  83. package/lib/public/modules/issues-render.js +29 -0
  84. package/lib/public/modules/issues.js +270 -0
  85. package/lib/public/modules/loop-interview.js +151 -0
  86. package/lib/public/modules/markdown.js +4 -0
  87. package/lib/public/modules/message-delivery.js +5 -3
  88. package/lib/public/modules/pending-message-image-editor.js +143 -0
  89. package/lib/public/modules/pending-message-queue-model.js +22 -0
  90. package/lib/public/modules/pending-message-queue.js +449 -0
  91. package/lib/public/modules/project-logs.js +2 -2
  92. package/lib/public/modules/scheduled-message-state.js +46 -0
  93. package/lib/public/modules/scheduled-task-edit-state.js +7 -0
  94. package/lib/public/modules/scheduled-task-engine.js +48 -0
  95. package/lib/public/modules/scheduled-task-form.js +122 -0
  96. package/lib/public/modules/scheduled-task-recurrence.js +46 -0
  97. package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
  98. package/lib/public/modules/scheduled-tasks.js +459 -0
  99. package/lib/public/modules/scheduler-config.js +6 -52
  100. package/lib/public/modules/scheduler.js +9 -21
  101. package/lib/public/modules/sidebar-mobile.js +3 -0
  102. package/lib/public/modules/sidebar.js +0 -11
  103. package/lib/public/modules/split-view.js +2 -0
  104. package/lib/public/modules/sticky-note-markdown.js +1 -0
  105. package/lib/public/modules/terminal.js +2 -0
  106. package/lib/public/modules/tool-palette-order.js +3 -3
  107. package/lib/public/modules/tools.js +5 -2
  108. package/lib/public/style.css +3 -0
  109. package/lib/record-revision.js +11 -0
  110. package/lib/schedule-rule.js +151 -0
  111. package/lib/schedule-validation.js +124 -0
  112. package/lib/scheduled-run-state.js +87 -0
  113. package/lib/scheduled-task-records.js +127 -0
  114. package/lib/scheduler.js +179 -196
  115. package/lib/sdk-bridge.js +157 -22
  116. package/lib/sdk-message-processor.js +3 -0
  117. package/lib/server-cursor-sharing.js +65 -0
  118. package/lib/server-default-ai.js +265 -0
  119. package/lib/server-durable-scheduler.js +26 -0
  120. package/lib/server-home-chat.js +29 -9
  121. package/lib/server-home-clay-entry.js +4 -1
  122. package/lib/server-home-debate-planning.js +32 -14
  123. package/lib/server-home-mate-creation.js +30 -11
  124. package/lib/server-home-models.js +14 -6
  125. package/lib/server-issue-links.js +18 -0
  126. package/lib/server.js +63 -0
  127. package/lib/session-notes-mcp-server.js +1 -1
  128. package/lib/session-pair-factory.js +42 -4
  129. package/lib/session-pair-query-start.js +22 -0
  130. package/lib/session-query-tool-bridge.js +52 -0
  131. package/lib/session-search.js +4 -1
  132. package/lib/session-split-groups.js +35 -1
  133. package/lib/sessions.js +51 -3
  134. package/lib/tool-llm.js +1 -0
  135. package/lib/users-default-ai-preferences.js +80 -0
  136. package/lib/users-home-surface-preferences.js +2 -1
  137. package/lib/users-preferences.js +35 -0
  138. package/lib/users-scheduled-task-preferences.js +44 -0
  139. package/lib/users.js +16 -0
  140. package/lib/workspace-query-projection.js +29 -0
  141. package/lib/workspace-query-service.js +19 -29
  142. package/lib/ws-schema.js +61 -1
  143. package/lib/yoke/adapters/antigravity.js +1 -0
  144. package/lib/yoke/adapters/claude-worker.js +11 -6
  145. package/lib/yoke/adapters/claude.js +35 -1
  146. package/lib/yoke/adapters/codex.js +3 -0
  147. package/lib/yoke/claude-user-input.js +2 -0
  148. package/lib/yoke/index.js +2 -0
  149. package/lib/yoke/mcp-bridge-server.js +7 -1
  150. package/lib/yoke/package.json +1 -1
  151. package/lib/yoke/session-tool-transport.js +53 -0
  152. package/lib/yoke/user-input.js +21 -7
  153. package/package.json +5 -6
  154. package/bin/claude-relay.js +0 -6
  155. package/lib/public/modules/app-loop-wizard.js +0 -439
@@ -3,6 +3,10 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var { execFileSync } = require("child_process");
5
5
  var { createLoopRegistry } = require("./scheduler");
6
+ var loopGuidance = require("./loop-guidance");
7
+ var validateCron = require("./schedule-validation").validateCron;
8
+ var validateScheduleRule = require("./schedule-rule").validateScheduleRule;
9
+ var attachLoopDurableSchedule = require("./loop-durable-schedule").attachLoopDurableSchedule;
6
10
 
7
11
  /**
8
12
  * Attach loop engine to a project context.
@@ -62,7 +66,7 @@ function attachLoop(ctx) {
62
66
  var _loopUtils = require("./utils");
63
67
  var _loopDir = path.join(_loopConfig.CONFIG_DIR, "loops");
64
68
  var _loopEncodedCwd = _loopUtils.resolveEncodedFile(_loopDir, cwd, ".json");
65
- var _loopStatePath = path.join(_loopDir, _loopEncodedCwd + ".json");
69
+ var _loopStatePath = ctx.loopStatePath || path.join(_loopDir, _loopEncodedCwd + ".json");
66
70
 
67
71
  function saveLoopState() {
68
72
  try {
@@ -269,18 +273,43 @@ function attachLoop(ctx) {
269
273
 
270
274
  // --- Loop Registry (unified one-off + scheduled) ---
271
275
  var activeRegistryId = null; // track which registry record triggered current loop
272
- var pendingTriggers = []; // queue for deferred triggers when skipIfRunning=false
276
+ var durableSchedule = null;
277
+ function isMultiUser() { return typeof ctx.isMultiUser === "function" ? ctx.isMultiUser() : !!ctx.isMultiUser; }
278
+
279
+ function legacyRuntimeFor(session) {
280
+ var record = loopRegistry.getById(loopState.loopId);
281
+ var linuxUser = getLinuxUserForSession(session);
282
+ if (!isMultiUser()) return { ok: true, linuxUser: linuxUser };
283
+ if (!record || !record.ownerId || session.ownerId !== record.ownerId) return { ok: false, error: "The scheduled Loop owner changed before execution." };
284
+ if (ctx.authorizeScheduledRun && !ctx.authorizeScheduledRun(record)) return { ok: false, error: "The scheduled Loop owner is no longer authorized for this project." };
285
+ if (ctx.requiresLinuxUser && ctx.requiresLinuxUser() && !linuxUser) return { ok: false, error: "The scheduled Loop owner no longer has a valid OS identity." };
286
+ return { ok: true, linuxUser: linuxUser };
287
+ }
288
+
289
+ function rejectLegacyRuntime(session, error) {
290
+ var message = error || "The scheduled Loop execution identity is no longer valid.";
291
+ var entry = { type: "error", text: message };
292
+ session.isProcessing = false; session.singleTurn = false; if (session.loop) session.loop.active = false;
293
+ session.history.push(entry); sm.appendToSessionFile(session, entry); sm.saveSessionFile(session);
294
+ sendToSession(session.localId, entry); sendToSession(session.localId, { type: "status", status: "idle" }); send({ type: "loop_error", text: message }); finishLoop("error");
295
+ }
273
296
 
274
297
  function triggerFromQueue(record) {
275
- // For schedule records, resolve the linked task to get loop files
298
+ if (record.createdViaScheduledTasks && ctx.authorizeScheduledRun && !ctx.authorizeScheduledRun(record)) {
299
+ console.error("[loop-registry] Scheduled task owner is no longer authorized: " + record.name);
300
+ return;
301
+ }
302
+ // Linked calendar schedules use their task's files. Standalone Scheduled Tasks own theirs.
276
303
  var loopFilesId = record.id;
277
304
  if (record.source === "schedule") {
278
- if (!record.linkedTaskId) {
279
- console.error("[loop-registry] Schedule has no linked task: " + record.name);
280
- return;
305
+ var standaloneScheduledTask = record.createdViaScheduledTasks && record.mode === "simple" && !record.linkedTaskId;
306
+ if (!record.linkedTaskId && !standaloneScheduledTask) {
307
+ failScheduledDispatch("Schedule has no linked task: " + record.name);
308
+ }
309
+ if (record.linkedTaskId) {
310
+ loopFilesId = record.linkedTaskId;
311
+ console.log("[loop-registry] Schedule triggered: " + record.name + " -> linked task " + loopFilesId);
281
312
  }
282
- loopFilesId = record.linkedTaskId;
283
- console.log("[loop-registry] Schedule triggered: " + record.name + " -> linked task " + loopFilesId);
284
313
  }
285
314
 
286
315
  // Verify the loop directory and PROMPT.md exist
@@ -288,8 +317,7 @@ function attachLoop(ctx) {
288
317
  try {
289
318
  fs.accessSync(path.join(recDir, "PROMPT.md"));
290
319
  } catch (e) {
291
- console.error("[loop-registry] PROMPT.md missing for " + loopFilesId);
292
- return;
320
+ failScheduledDispatch("PROMPT.md is missing for " + loopFilesId + ".");
293
321
  }
294
322
  // Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
295
323
  loopState.loopId = record.id;
@@ -304,28 +332,65 @@ function attachLoop(ctx) {
304
332
  startLoop({ maxIterations: record.maxIterations, name: record.name });
305
333
  }
306
334
 
335
+ function failScheduledDispatch(message) {
336
+ console.error("[loop-registry] " + message);
337
+ send({ type: "loop_error", text: message });
338
+ var error = new Error(message); error.code = "SCHEDULE_DISPATCH_FAILED"; throw error;
339
+ }
340
+
341
+ function usesPairExecution(record) { return !!(record && (record.createdViaScheduledTasks === true || record.source === "schedule")); }
342
+
307
343
  var loopRegistry = createLoopRegistry({
308
344
  cwd: cwd,
309
- onTrigger: function (record) {
310
- // Skip or queue trigger if a loop is already active
345
+ registryPath: ctx.loopRegistryPath,
346
+ canTrigger: function (record) {
347
+ if (!usesPairExecution(record)) return true;
348
+ return ctx.canExecuteScheduledTask ? ctx.canExecuteScheduledTask(record) : false;
349
+ },
350
+ prepareTrigger: function (record) {
351
+ if (!usesPairExecution(record)) return null;
352
+ return ctx.prepareScheduledTask ? ctx.prepareScheduledTask(record, "schedule") : { ok: false };
353
+ },
354
+ onTrigger: function (record, prepared) {
355
+ if (usesPairExecution(record)) {
356
+ if (prepared && (prepared.skipped || prepared.queued)) return;
357
+ var scheduledResult = ctx.executeScheduledTask && ctx.executeScheduledTask(record, "schedule", prepared);
358
+ if (!scheduledResult || !scheduledResult.ok) failScheduledDispatch("Pair execution did not start: " + (scheduledResult && scheduledResult.error || record.name));
359
+ return;
360
+ }
361
+ // A busy non-skipping occurrence remains queued in the durable scheduler.
311
362
  if (loopState.active || loopState.phase === "executing") {
312
363
  if (record.skipIfRunning !== false) {
313
364
  console.log("[loop-registry] Skipping trigger for " + record.name + " — loop already active (skipIfRunning)");
314
365
  return;
315
366
  }
316
- console.log("[loop-registry] Loop active, queuing trigger for " + record.name);
317
- pendingTriggers.push(record);
318
367
  return;
319
368
  }
320
369
 
321
370
  triggerFromQueue(record);
322
371
  },
323
372
  onChange: function () {
373
+ if (durableSchedule) durableSchedule.sync();
324
374
  send({ type: "loop_registry_updated", records: getHubSchedules() });
325
375
  },
326
376
  });
327
377
  loopRegistry.load();
328
- loopRegistry.startTimer();
378
+ if (ctx.durableScheduler) {
379
+ durableSchedule = attachLoopDurableSchedule({
380
+ scheduler: ctx.durableScheduler,
381
+ registry: loopRegistry,
382
+ projectId: slug,
383
+ isMultiUser: ctx.isMultiUser,
384
+ authorize: function (record) {
385
+ if (!isMultiUser() && !usesPairExecution(record)) return true;
386
+ return !ctx.authorizeScheduledRun || ctx.authorizeScheduledRun(record);
387
+ },
388
+ canRun: function (record) {
389
+ if (usesPairExecution(record)) return ctx.canExecuteScheduledTask ? ctx.canExecuteScheduledTask(record) : false;
390
+ return !(record.skipIfRunning === false && (loopState.active || loopState.phase === "executing"));
391
+ },
392
+ });
393
+ } else loopRegistry.startTimer();
329
394
 
330
395
  // Wire loop info resolution for session list broadcasts
331
396
  sm.setResolveLoopInfo(function (loopId) {
@@ -412,8 +477,8 @@ function attachLoop(ctx) {
412
477
  return;
413
478
  }
414
479
 
415
- var session = sm.createSession();
416
480
  var loopSource = loopRegistry.getById(loopState.loopId);
481
+ var session = sm.createSession({ ownerId: loopSource && loopSource.ownerId || null });
417
482
  var loopName = (loopState.wizardData && loopState.wizardData.name) || (loopSource && loopSource.name) || "";
418
483
  var loopSourceTag = (loopSource && loopSource.source) || null;
419
484
  var isRalphLoop = loopSourceTag === "ralph";
@@ -430,6 +495,8 @@ function attachLoop(ctx) {
430
495
  maxIterations: loopState.maxIterations,
431
496
  sessionId: session.localId,
432
497
  });
498
+ var coderRuntime = legacyRuntimeFor(session);
499
+ if (!coderRuntime.ok) { rejectLegacyRuntime(session, coderRuntime.error); return; }
433
500
 
434
501
  var coderCompleted = false;
435
502
  session.onQueryComplete = function(completedSession) {
@@ -508,7 +575,7 @@ function attachLoop(ctx) {
508
575
  session.acceptEditsAfterStart = true;
509
576
  session.singleTurn = true;
510
577
  if (loopState.settings) session.loopSettings = loopState.settings;
511
- sdk.startQuery(session, loopState.promptText, undefined, getLinuxUserForSession(session));
578
+ sdk.startQuery(session, loopState.promptText, undefined, coderRuntime.linuxUser);
512
579
  }
513
580
 
514
581
  function runJudge() {
@@ -549,8 +616,8 @@ function attachLoop(ctx) {
549
616
  "- PASS: [brief explanation]\n" +
550
617
  "- FAIL: [brief explanation of what is still missing]";
551
618
 
552
- var judgeSession = sm.createSession();
553
619
  var judgeSource = loopRegistry.getById(loopState.loopId);
620
+ var judgeSession = sm.createSession({ ownerId: judgeSource && judgeSource.ownerId || null });
554
621
  var judgeName = (loopState.wizardData && loopState.wizardData.name) || (judgeSource && judgeSource.name) || "";
555
622
  var judgeSourceTag = (judgeSource && judgeSource.source) || null;
556
623
  var isRalphJudge = judgeSourceTag === "ralph";
@@ -565,6 +632,8 @@ function attachLoop(ctx) {
565
632
  iteration: loopState.iteration,
566
633
  sessionId: judgeSession.localId,
567
634
  });
635
+ var judgeRuntime = legacyRuntimeFor(judgeSession);
636
+ if (!judgeRuntime.ok) { rejectLegacyRuntime(judgeSession, judgeRuntime.error); return; }
568
637
 
569
638
  var judgeCompleted = false;
570
639
  judgeSession.onQueryComplete = function(completedSession) {
@@ -625,7 +694,7 @@ function attachLoop(ctx) {
625
694
  judgeSession.acceptEditsAfterStart = true;
626
695
  judgeSession.singleTurn = true;
627
696
  if (loopState.settings) judgeSession.loopSettings = loopState.settings;
628
- sdk.startQuery(judgeSession, judgePrompt, undefined, getLinuxUserForSession(judgeSession));
697
+ sdk.startQuery(judgeSession, judgePrompt, undefined, judgeRuntime.linuxUser);
629
698
  }
630
699
 
631
700
  function parseJudgeVerdict(session) {
@@ -710,14 +779,7 @@ function attachLoop(ctx) {
710
779
  });
711
780
  }
712
781
 
713
- // Process next queued trigger if any
714
- if (pendingTriggers.length > 0) {
715
- var next = pendingTriggers.shift();
716
- console.log("[loop-registry] Processing queued trigger: " + next.name);
717
- setTimeout(function () {
718
- triggerFromQueue(next);
719
- }, 1000);
720
- }
782
+ if (durableSchedule) durableSchedule.wake();
721
783
  }
722
784
 
723
785
  function resumeLoop() {
@@ -891,13 +953,7 @@ function attachLoop(ctx) {
891
953
  loopState.phase = "crafting";
892
954
  saveLoopState();
893
955
 
894
- var judgeCraftPrompt = "Use the /clay-ralph skill to design ONLY a JUDGE.md for an existing Ralph Loop. " +
895
- "The user has already provided PROMPT.md, so do NOT create or modify PROMPT.md. " +
896
- "You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
897
- "Your job is to read the existing PROMPT.md and create a JUDGE.md " +
898
- "that will evaluate whether the coder session completed the task successfully.\n\n" +
899
- "## Task\n" + (wData.task || "") +
900
- "\n\n## Loop Directory\n" + lDir;
956
+ var judgeCraftPrompt = loopGuidance.legacyCraftPrompt({ judgeOnly: true, task: wData.task, directory: lDir });
901
957
 
902
958
  var judgeCraftSession = sm.createSession();
903
959
  judgeCraftSession.title = (isRalphCraft ? "Ralph" : "Task") + (craftName ? " " + craftName : "") + " Crafting";
@@ -934,31 +990,9 @@ function attachLoop(ctx) {
934
990
  return true;
935
991
  }
936
992
 
937
- // Default: "draft" mode — Clay crafts files via the clay-ralph skill
993
+ // Default: "draft" mode — Clay crafts legacy PROMPT.md/JUDGE.md files
938
994
  var _draftIsSimple = wData.loopMode === "simple";
939
- var craftingPrompt;
940
- if (_draftIsSimple) {
941
- craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
942
- "You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
943
- "This is a Simple Loop (no judge). Your job is to create ONLY a PROMPT.md file " +
944
- "that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
945
- "## Task\n" + (wData.task || "") +
946
- "\n\n## Loop Directory\n" + lDir;
947
- } else if (wData.judgeAuthor === "me") {
948
- craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
949
- "You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
950
- "The user will provide their own JUDGE.md, so create ONLY a PROMPT.md file " +
951
- "that a future autonomous session will execute. Do NOT create a JUDGE.md file.\n\n" +
952
- "## Task\n" + (wData.task || "") +
953
- "\n\n## Loop Directory\n" + lDir;
954
- } else {
955
- craftingPrompt = "Use the /clay-ralph skill to design a Ralph Loop for the following task. " +
956
- "You MUST invoke the clay-ralph skill — do NOT execute the task yourself. " +
957
- "Your job is to interview me, then create PROMPT.md and JUDGE.md files " +
958
- "that a future autonomous session will execute.\n\n" +
959
- "## Task\n" + (wData.task || "") +
960
- "\n\n## Loop Directory\n" + lDir;
961
- }
995
+ var craftingPrompt = loopGuidance.legacyCraftPrompt({ simple: _draftIsSimple, userJudge: wData.judgeAuthor === "me", task: wData.task, directory: lDir });
962
996
 
963
997
  // Pre-write user-provided files before crafting starts
964
998
  if (wData.judgeText && wData.judgeAuthor === "me") {
@@ -1106,6 +1140,27 @@ function attachLoop(ctx) {
1106
1140
  // --- Schedule create (from calendar click) ---
1107
1141
  if (msg.type === "schedule_create") {
1108
1142
  var sData = msg.data || {};
1143
+ var scheduleActor = ctx.getScheduleActor ? ctx.getScheduleActor(ws) : { ownerId: null, ownerName: null, sessionId: null };
1144
+ if (!scheduleActor) {
1145
+ sendTo(ws, { type: "loop_registry_error", text: "The schedule owner could not be verified." });
1146
+ return true;
1147
+ }
1148
+ if (sData.cron) {
1149
+ var scheduleCreateCron = validateCron(sData.cron);
1150
+ if (!scheduleCreateCron.ok) {
1151
+ sendTo(ws, { type: "loop_registry_error", text: scheduleCreateCron.error });
1152
+ return true;
1153
+ }
1154
+ sData.cron = scheduleCreateCron.cron;
1155
+ }
1156
+ if (sData.scheduleRule) {
1157
+ var scheduleCreateRule = validateScheduleRule(sData.scheduleRule);
1158
+ if (!scheduleCreateRule.ok) {
1159
+ sendTo(ws, { type: "loop_registry_error", text: scheduleCreateRule.error });
1160
+ return true;
1161
+ }
1162
+ sData.scheduleRule = scheduleCreateRule.value;
1163
+ }
1109
1164
  loopRegistry.register({
1110
1165
  name: sData.name || "Untitled",
1111
1166
  task: sData.name || "",
@@ -1122,6 +1177,11 @@ function attachLoop(ctx) {
1122
1177
  recurrenceEnd: sData.recurrenceEnd || null,
1123
1178
  skipIfRunning: sData.skipIfRunning !== undefined ? sData.skipIfRunning : true,
1124
1179
  intervalEnd: sData.intervalEnd || null,
1180
+ scheduleRule: sData.scheduleRule || null,
1181
+ ownerId: scheduleActor.ownerId,
1182
+ ownerName: scheduleActor.ownerName,
1183
+ sessionId: scheduleActor.sessionId,
1184
+ createdViaScheduledTasks: true,
1125
1185
  });
1126
1186
  return true;
1127
1187
  }
@@ -1139,7 +1199,28 @@ function attachLoop(ctx) {
1139
1199
  }
1140
1200
 
1141
1201
  if (msg.type === "loop_registry_update") {
1142
- var updatedRec = loopRegistry.update(msg.id, msg.data || {});
1202
+ if (msg.data && msg.data.cron !== undefined && msg.data.cron !== null) {
1203
+ var scheduleUpdateCron = validateCron(msg.data.cron);
1204
+ if (!scheduleUpdateCron.ok) {
1205
+ sendTo(ws, { type: "loop_registry_error", text: scheduleUpdateCron.error });
1206
+ return true;
1207
+ }
1208
+ msg.data.cron = scheduleUpdateCron.cron;
1209
+ }
1210
+ if (msg.data && msg.data.scheduleRule) {
1211
+ var scheduleUpdateRule = validateScheduleRule(msg.data.scheduleRule);
1212
+ if (!scheduleUpdateRule.ok) {
1213
+ sendTo(ws, { type: "loop_registry_error", text: scheduleUpdateRule.error });
1214
+ return true;
1215
+ }
1216
+ msg.data.scheduleRule = scheduleUpdateRule.value;
1217
+ }
1218
+ var registryUpdateData = Object.assign({}, msg.data || {});
1219
+ delete registryUpdateData.ownerId;
1220
+ delete registryUpdateData.ownerName;
1221
+ delete registryUpdateData.sessionId;
1222
+ delete registryUpdateData.createdViaScheduledTasks;
1223
+ var updatedRec = loopRegistry.update(msg.id, registryUpdateData);
1143
1224
  if (!updatedRec) {
1144
1225
  sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
1145
1226
  }
@@ -1197,15 +1278,20 @@ function attachLoop(ctx) {
1197
1278
 
1198
1279
  if (msg.type === "loop_registry_rerun") {
1199
1280
  // Re-run an existing job (one-off from library)
1200
- if (loopState.active || loopState.phase === "executing") {
1201
- sendTo(ws, { type: "loop_registry_error", text: "A loop is already running" });
1202
- return true;
1203
- }
1204
1281
  var rerunRec = loopRegistry.getById(msg.id);
1205
1282
  if (!rerunRec) {
1206
1283
  sendTo(ws, { type: "loop_registry_error", text: "Record not found" });
1207
1284
  return true;
1208
1285
  }
1286
+ if (usesPairExecution(rerunRec)) {
1287
+ var pairRun = ctx.executeScheduledTask && ctx.executeScheduledTask(rerunRec, "manual");
1288
+ if (!pairRun || !pairRun.ok) sendTo(ws, { type: "loop_registry_error", text: pairRun && pairRun.error || "The scheduled pair could not start." });
1289
+ return true;
1290
+ }
1291
+ if (loopState.active || loopState.phase === "executing") {
1292
+ sendTo(ws, { type: "loop_registry_error", text: "A loop is already running" });
1293
+ return true;
1294
+ }
1209
1295
  var rerunDir = path.join(cwd, ".claude", "loops", rerunRec.id);
1210
1296
  try {
1211
1297
  fs.accessSync(path.join(rerunDir, "PROMPT.md"));
@@ -1297,7 +1383,7 @@ function attachLoop(ctx) {
1297
1383
  getSchedules: function () { return loopRegistry.getAll(); },
1298
1384
  importSchedule: function (data) { return loopRegistry.register(data); },
1299
1385
  removeSchedule: function (id) { return loopRegistry.remove(id); },
1300
- stopTimer: function () { loopRegistry.stopTimer(); },
1386
+ stopTimer: function () { if (durableSchedule) durableSchedule.shutdown(); else loopRegistry.stopTimer(); },
1301
1387
  };
1302
1388
  }
1303
1389
 
@@ -29,7 +29,7 @@ function ensureRecordedMessages(session) {
29
29
  return recorded;
30
30
  }
31
31
 
32
- function createProjectMessageDelivery(sendTo) {
32
+ function createProjectMessageDelivery(sendTo, projectSlug) {
33
33
  function inspect(ws, session, msg) {
34
34
  var clientMessageId = normalizeClientMessageId(msg && msg.clientMessageId);
35
35
  if (!clientMessageId) return { clientMessageId: null, ownerId: ownerKey(ws), duplicate: false };
@@ -48,11 +48,13 @@ function createProjectMessageDelivery(sendTo) {
48
48
  ensureRecordedMessages(session).set(deliveryKey(receipt.ownerId, receipt.clientMessageId), message);
49
49
  }
50
50
 
51
- function acknowledge(ws, receipt, message) {
51
+ function acknowledge(ws, receipt, message, session) {
52
52
  if (!receipt || !receipt.clientMessageId) return;
53
53
  sendTo(ws, {
54
54
  type: "message_ack",
55
55
  clientMessageId: receipt.clientMessageId,
56
+ projectSlug: projectSlug || null,
57
+ sessionId: session ? session.localId : null,
56
58
  message: message || receipt.recordedMessage || null,
57
59
  });
58
60
  }
@@ -1,4 +1,5 @@
1
1
  var yoke = require("./yoke");
2
+ var defaultAiRuntime = require("./default-ai-runtime");
2
3
 
3
4
  function modelEntryValue(entry) {
4
5
  if (!entry) return "";
@@ -12,6 +13,17 @@ function modelEntryMatches(entry, value) {
12
13
  return entry.value === value || entry.id === value || entry.resolvedModel === value;
13
14
  }
14
15
 
16
+ function modelsWithEffortMetadata(models, vendorInfo) {
17
+ var fallback = vendorInfo && Array.isArray(vendorInfo.effortLevels) ? vendorInfo.effortLevels : [];
18
+ return (models || []).map(function (model) {
19
+ if (typeof model === "string") {
20
+ return { value: model, displayName: model, supportedEffortLevels: fallback.slice() };
21
+ }
22
+ if (!model || Array.isArray(model.supportedEffortLevels)) return model;
23
+ return Object.assign({}, model, { supportedEffortLevels: fallback.slice() });
24
+ });
25
+ }
26
+
15
27
  function normalizeCatalogModels(models) {
16
28
  var normalized = [];
17
29
  models = models || [];
@@ -64,8 +76,12 @@ function attachModels(ctx) {
64
76
  var serverPort = ctx.serverPort;
65
77
  var serverTls = ctx.serverTls;
66
78
  var serverAuthToken = ctx.serverAuthToken;
79
+ var resolveDefaultAi = ctx.resolveDefaultAi;
67
80
 
68
- function getVendorAvailability() {
81
+ function getVendorAvailability(ws) {
82
+ if (sm.installedVendors === undefined && sdk && typeof sdk.refreshInstalledVendors === "function") {
83
+ sdk.refreshInstalledVendors(getLinuxUserForWs(ws));
84
+ }
69
85
  var available = (sm.availableVendors || []).slice();
70
86
  var installed = (sm.installedVendors || []).slice();
71
87
  var ids = available.slice();
@@ -160,8 +176,8 @@ function attachModels(ctx) {
160
176
  console.error("[project-models] model loading failed for " + vendor + ":", e.message || e);
161
177
  }
162
178
 
163
- var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
164
179
  var vendorInfo = yoke.getVendorInfo(vendor);
180
+ var vendorModels = modelsWithEffortMetadata((sm.modelsByVendor && sm.modelsByVendor[vendor]) || [], vendorInfo);
165
181
  var displayName = vendorInfo ? vendorInfo.displayName : vendor;
166
182
  var status = vendorModels.length > 0 ? "ready" : (loadError ? "error" : "empty");
167
183
  var defaultModel = "";
@@ -191,11 +207,62 @@ function attachModels(ctx) {
191
207
  installedVendors: sm.installedVendors || [],
192
208
  status: status,
193
209
  error: errorText,
210
+ effortLevels: vendorInfo && Array.isArray(vendorInfo.effortLevels) ? vendorInfo.effortLevels.slice() : [],
194
211
  };
195
212
  }
196
213
 
197
214
  async function resolveConfiguredModel(ws, alias) {
198
- var availability = getVendorAvailability();
215
+ if (typeof resolveDefaultAi === "function") {
216
+ var defaultSelection = await resolveDefaultAi(ws);
217
+ var normalizedAlias = alias === "fast" || alias === "deep" ? alias : "standard";
218
+ if (!defaultSelection || !defaultSelection.ready) {
219
+ return {
220
+ status: "error",
221
+ vendor: defaultSelection && defaultSelection.vendor || "",
222
+ vendorName: defaultSelection && defaultSelection.vendor || "",
223
+ model: "",
224
+ modelName: "",
225
+ alias: normalizedAlias,
226
+ effort: "",
227
+ error: defaultSelection && defaultSelection.error || "Default AI is unavailable.",
228
+ };
229
+ }
230
+ var selectedModel = defaultSelection.model;
231
+ var selectedName = defaultSelection.model;
232
+ if (normalizedAlias !== "standard") {
233
+ var selectedCatalog = await getVendorCatalog(ws, defaultSelection.vendor);
234
+ var aliasEntry = selectCatalogModel(selectedCatalog.models || [], defaultSelection.model, normalizedAlias);
235
+ if (!aliasEntry || selectedCatalog.status !== "ready") {
236
+ return {
237
+ status: "error",
238
+ vendor: defaultSelection.vendor,
239
+ vendorName: defaultSelection.vendor,
240
+ model: "",
241
+ modelName: "",
242
+ alias: normalizedAlias,
243
+ effort: defaultSelection.effort || "",
244
+ error: selectedCatalog.error || "The selected Default AI provider has no available model.",
245
+ };
246
+ }
247
+ selectedModel = aliasEntry.value;
248
+ selectedName = aliasEntry.displayName || selectedModel;
249
+ var aliasEffort = defaultAiRuntime.resolveEffort({ effort: defaultSelection.effort || "" }, aliasEntry.source, selectedCatalog);
250
+ if (!aliasEffort.ok) aliasEffort = defaultAiRuntime.resolveEffort(null, aliasEntry.source, selectedCatalog);
251
+ defaultSelection = Object.assign({}, defaultSelection, { effort: aliasEffort.effort || "" });
252
+ }
253
+ var selectedInfo = yoke.getVendorInfo(defaultSelection.vendor);
254
+ return {
255
+ status: "ready",
256
+ vendor: defaultSelection.vendor,
257
+ vendorName: selectedInfo && selectedInfo.displayName ? selectedInfo.displayName : defaultSelection.vendor,
258
+ alias: normalizedAlias,
259
+ model: selectedModel,
260
+ modelName: selectedName,
261
+ effort: defaultSelection.effort || "",
262
+ error: "",
263
+ };
264
+ }
265
+ var availability = getVendorAvailability(ws);
199
266
  var candidates = [];
200
267
  var preferredVendor = sm.defaultVendor || "";
201
268
  if (preferredVendor) candidates.push(preferredVendor);
@@ -341,5 +408,6 @@ module.exports = {
341
408
  modelEntryMatches: modelEntryMatches,
342
409
  modelEntryValue: modelEntryValue,
343
410
  normalizeCatalogModels: normalizeCatalogModels,
411
+ modelsWithEffortMetadata: modelsWithEffortMetadata,
344
412
  selectCatalogModel: selectCatalogModel,
345
413
  };
@@ -0,0 +1,16 @@
1
+ function stopAutonomousWork(session, roles, turnControl, taskControl, workerPermission) {
2
+ if (!roles) return false;
3
+ var runId = session.autonomousRun && session.autonomousRun.id;
4
+ var cancelled = taskControl.cancelAll(roles.worker, runId);
5
+ var owned = roles.worker._pairDelegation && roles.worker._pairDelegation.autonomousRunId === runId;
6
+ if (!owned && !cancelled) return true;
7
+ turnControl.markHumanStop(session);
8
+ if (!owned) return true;
9
+ taskControl.markInterruption(roles.worker, "user", "The Until complete run was stopped.");
10
+ workerPermission.cancelForSession(roles.worker, "The Until complete run was stopped.");
11
+ roles.worker.taskStopRequested = true;
12
+ if (roles.worker.abortController) roles.worker.abortController.abort();
13
+ return true;
14
+ }
15
+
16
+ module.exports = { stopAutonomousWork: stopAutonomousWork };
@@ -0,0 +1,18 @@
1
+ function stopOwnedPair(session, roles, turnControl, taskControl, workerPermission, reason) {
2
+ if (!roles) return false;
3
+ turnControl.markHumanStop(session);
4
+ taskControl.cancelAll(roles.worker);
5
+ taskControl.markInterruption(roles.worker, "user", reason || "The human stopped this scheduled run.");
6
+ workerPermission.cancelForSession(roles.worker, reason || "The human stopped this scheduled run.");
7
+ var members = [roles.driver, roles.worker];
8
+ for (var i = 0; i < members.length; i++) {
9
+ members[i].taskStopRequested = true;
10
+ if (members[i].queryInstance && typeof members[i].queryInstance.close === "function") {
11
+ try { members[i].queryInstance.close(); } catch (e) {}
12
+ }
13
+ if (members[i].abortController) { try { members[i].abortController.abort(); } catch (e) {} }
14
+ }
15
+ return true;
16
+ }
17
+
18
+ module.exports = { stopOwnedPair: stopOwnedPair };
@@ -67,6 +67,7 @@ function attachPairTaskControl(ctx) {
67
67
  workerSessionId: worker.localId,
68
68
  driverSessionId: caller.localId,
69
69
  message: message,
70
+ autonomousRunId: caller.autonomousRun && caller.autonomousRun.id || null,
70
71
  createdAt: Date.now(),
71
72
  status: "running",
72
73
  };
@@ -118,6 +119,18 @@ function attachPairTaskControl(ctx) {
118
119
  return result({ status: "cancelled", taskId: task.taskId });
119
120
  } catch (err) { return result({ status: "rejected", taskId: args.taskId || null, reason: err.message || String(err) }); }
120
121
  }
122
+ function cancelAll(worker, autonomousRunId) {
123
+ var queue = queueFor(worker);
124
+ var cancelled = 0;
125
+ for (var i = 0; i < queue.length; i++) {
126
+ if (queue[i].status !== "queued" && queue[i].status !== "waiting_for_interrupt") continue;
127
+ if (autonomousRunId && queue[i].autonomousRunId !== autonomousRunId) continue;
128
+ queue[i].status = "cancelled";
129
+ queue[i].completedAt = Date.now();
130
+ cancelled++;
131
+ }
132
+ return cancelled;
133
+ }
121
134
  function markInterruption(worker, by, reason, targetTaskId) {
122
135
  var active = current(worker);
123
136
  worker._pairInterruption = {
@@ -258,7 +271,7 @@ function attachPairTaskControl(ctx) {
258
271
  function workerToolDefs(worker) {
259
272
  return [{ name: "report_partner_outcome", description: "Report structured, Worker-observed task outcome data. This does not claim Driver verification.", inputSchema: buildShape({ taskId: { type: "string" }, outcome: { type: "string" }, summary: { type: "string" }, changedFiles: { type: "array", items: { type: "string" } }, verifications: { type: "array", items: { type: "object" } }, unverified: { type: "array", items: { type: "string" } }, processes: { type: "array", items: { type: "string" } }, nextAction: { type: "string" } }, ["taskId", "outcome"]), handler: function (args) { return report(args || {}, worker); } }];
260
273
  }
261
- return { begin: begin, cancel: cancel, complete: complete, drain: drain, driverToolDefs: driverToolDefs, ensureSystemInterruption: ensureSystemInterruption, inspect: inspect, markInterruption: markInterruption, queue: queue, replaceTask: replaceTask, resumeTask: resumeTask, status: status, workerToolDefs: workerToolDefs };
274
+ return { begin: begin, cancel: cancel, cancelAll: cancelAll, complete: complete, drain: drain, driverToolDefs: driverToolDefs, ensureSystemInterruption: ensureSystemInterruption, inspect: inspect, markInterruption: markInterruption, queue: queue, replaceTask: replaceTask, resumeTask: resumeTask, status: status, workerToolDefs: workerToolDefs };
262
275
  }
263
276
 
264
277
  module.exports = { attachPairTaskControl: attachPairTaskControl };