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
@@ -0,0 +1,210 @@
1
+ // Append-only Issue records, stored beside but separately from Project Logs.
2
+
3
+ var crypto = require("crypto");
4
+ var recordStore = require("./knowledge-record-store");
5
+ var schema = require("./issues-schema");
6
+ var issueComments = require("./issues-comments");
7
+
8
+ var REF = /^issue:[A-Za-z0-9_-]{24}$/;
9
+ var MAX_PAGE = 50;
10
+
11
+ function issueScopeIdForKnowledgeId(id) {
12
+ if (typeof id !== "string" || !/^[A-Za-z0-9_-]{1,120}$/.test(id)) throw new Error("Invalid project knowledge id.");
13
+ return "project/" + id + "/issues";
14
+ }
15
+
16
+ function createIssuesStore(options) {
17
+ var opts = options || {};
18
+ var knowledgeId = opts.projectKnowledgeId;
19
+ var scopeId = issueScopeIdForKnowledgeId(knowledgeId);
20
+ var records = recordStore.createRecordStore({ scopeId: scopeId, baseDir: opts.baseDir });
21
+
22
+ function ref() {
23
+ return "issue:" + crypto.randomBytes(18).toString("base64url").substring(0, 24);
24
+ }
25
+ function chainFor(id) {
26
+ var all = records.all();
27
+ var out = [];
28
+ for (var i = 0; i < all.length; i++) if (all[i].rootId === id) out.push(all[i]);
29
+ return out;
30
+ }
31
+ function snapshotFor(chain, revision) {
32
+ var current = null;
33
+ for (var i = 0; i < chain.length; i++) {
34
+ if ((chain[i].op === "revision" || chain[i].op === "incorporate") && (!revision || chain[i].revision <= revision)) current = chain[i].snapshot;
35
+ }
36
+ return current ? schema.clone(current) : null;
37
+ }
38
+ function project(id, includeBody, includeDeleted) {
39
+ var chain = chainFor(id);
40
+ var revisions = chain.filter(function (record) { return record.op === "revision" || record.op === "incorporate"; });
41
+ if (!revisions.length) return null;
42
+ var latest = revisions[revisions.length - 1];
43
+ var current = schema.clone(latest.snapshot);
44
+ var deleted = chain.some(function (record) { return record.op === "delete"; });
45
+ if (deleted && !includeDeleted) return null;
46
+ var result = {
47
+ ref: id, revision: latest.revision, title: current.title, summary: current.summary,
48
+ resolutionSummary: current.resolutionSummary || "",
49
+ type: current.type, priority: current.priority, status: current.status,
50
+ createdAt: revisions[0].at, updatedAt: latest.at, createdBy: revisions[0].author,
51
+ updatedBy: latest.author, context: current.context, linkedWorkSessions: current.linkedWorkSessions,
52
+ resolutionHistory: current.resolutionHistory,
53
+ comments: issueComments.comments(chain), deleted: deleted,
54
+ };
55
+ if (deleted) { var tombstone = chain.filter(function (record) { return record.op === "delete"; }).pop(); result.deletedAt = tombstone.at; result.deletedBy = tombstone.author; }
56
+ if (current.verifiedCommit) result.verifiedCommit = current.verifiedCommit;
57
+ if (current.closeReason) result.closeReason = current.closeReason;
58
+ if (includeBody) result.body = current.body;
59
+ return result;
60
+ }
61
+ function append(id, operation, snapshot, revision, author) {
62
+ records.append({ id: recordStore.newRecordId(), rootId: id, op: operation, scope: scopeId,
63
+ revision: revision, snapshot: schema.clone(snapshot), author: schema.clone(author), at: Date.now() });
64
+ }
65
+ function create(data, author, context, verifiedCommit) {
66
+ var id = ref();
67
+ var input = Object.assign({}, data || {});
68
+ input.status = input.status === undefined ? "open" : input.status;
69
+ input.context = schema.normalizeContext(context);
70
+ if (input.status === "resolved") {
71
+ if (!verifiedCommit) throw new Error("resolved issues require verified commit evidence.");
72
+ if (typeof input.resolutionSummary !== "string" || !input.resolutionSummary.trim()) throw new Error("resolutionSummary is required when resolving an issue.");
73
+ input.verifiedCommit = verifiedCommit;
74
+ }
75
+ var snap = schema.snapshot(input);
76
+ if (snap.status === "resolved") {
77
+ snap.resolutionHistory = [{ status: "resolved", resolutionSummary: snap.resolutionSummary, verifiedCommit: snap.verifiedCommit, at: Date.now() }];
78
+ }
79
+ append(id, "revision", snap, 1, author);
80
+ return project(id, true);
81
+ }
82
+ function update(id, data, expectedRevision, author, context, verifiedCommit) {
83
+ var current = project(id, true);
84
+ if (!current) throw new Error("Issue not found.");
85
+ if (expectedRevision !== current.revision) throw new Error("Issue revision conflict.");
86
+ var input = Object.assign({}, data || {});
87
+ input.context = schema.clone(current.context || null);
88
+ if (input.status === "resolved") {
89
+ if (!verifiedCommit) throw new Error("resolved issues require verified commit evidence.");
90
+ input.verifiedCommit = verifiedCommit;
91
+ }
92
+ if (input.status === "open" || input.status === "in_progress") {
93
+ input.resolutionHistory = current.resolutionHistory || [];
94
+ delete input.verifiedCommit;
95
+ delete input.resolutionSummary;
96
+ }
97
+ var snap = schema.snapshot(input, current);
98
+ if (input.status === "resolved") {
99
+ snap.resolutionHistory = (current.resolutionHistory || []).concat([{ status: "resolved", resolutionSummary: snap.resolutionSummary, verifiedCommit: snap.verifiedCommit, at: Date.now() }]);
100
+ }
101
+ append(id, "revision", snap, current.revision + 1, author);
102
+ return project(id, true);
103
+ }
104
+ function comment(id, body, author) {
105
+ var located = { rootId: id, chain: chainFor(id) };
106
+ if (!project(id, false)) throw new Error("Issue not found.");
107
+ var value = schema.text(body, "comment", 4000, true);
108
+ var current = project(id, true);
109
+ if ((current.comments || []).length >= 200) throw new Error("This issue already has the maximum of 200 comments.");
110
+ records.append({ id: recordStore.newRecordId(), rootId: located.rootId, op: "comment", scope: scopeId,
111
+ body: value, author: schema.clone(author), at: Date.now() });
112
+ return project(id, true);
113
+ }
114
+ function review(id, commentId, action, response, author, revision) {
115
+ var current = project(id, true);
116
+ if (!current) throw new Error("Issue not found.");
117
+ if (issueComments.ACTIONS.indexOf(action) === -1) throw new Error("Invalid issue comment action.");
118
+ var exists = (current.comments || []).some(function (item) { return item.id === commentId && item.status === issueComments.STATUS_PENDING; });
119
+ if (!exists) throw new Error("Issue comment is no longer awaiting review.");
120
+ records.append({ id: recordStore.newRecordId(), rootId: id, op: "review", scope: scopeId,
121
+ commentId: commentId, action: action, response: schema.text(response || "", "response", 4000, action !== "incorporate"), revision: revision || null,
122
+ author: schema.clone(author), at: Date.now() });
123
+ return project(id, true);
124
+ }
125
+ function incorporate(id, commentId, data, expectedRevision, response, author) {
126
+ var current = project(id, true);
127
+ if (!current) throw new Error("Issue not found.");
128
+ if (current.revision !== expectedRevision) throw new Error("Issue revision conflict.");
129
+ if (issueComments.ACTIONS.indexOf("incorporate") === -1) throw new Error("Invalid issue comment action.");
130
+ if (!(current.comments || []).some(function (item) { return item.id === commentId && item.status === issueComments.STATUS_PENDING; })) throw new Error("Issue comment is no longer awaiting review.");
131
+ var input = Object.assign({}, data || {}); input.context = schema.clone(current.context || null);
132
+ var reviewResponse = schema.text(response || "", "response", 4000, false);
133
+ var snapshot = schema.snapshot(input, current);
134
+ var unchanged = snapshotFor(chainFor(id), current.revision);
135
+ if (JSON.stringify(snapshot) === JSON.stringify(unchanged)) throw new Error("Issue incorporation must change the canonical Issue.");
136
+ records.append({ id: recordStore.newRecordId(), rootId: id, op: "revision", scope: scopeId, snapshot: schema.clone(snapshot),
137
+ review: { commentId: commentId, action: "incorporate", response: reviewResponse, revision: current.revision + 1 }, revision: current.revision + 1,
138
+ author: schema.clone(author), at: Date.now() });
139
+ return project(id, true);
140
+ }
141
+ function remove(id, author, expectedRevision) {
142
+ var current = project(id, false);
143
+ if (!current) throw new Error("Issue not found.");
144
+ if (!Number.isInteger(expectedRevision)) throw new Error("expectedRevision is required.");
145
+ if (current.revision !== expectedRevision) throw new Error("Issue revision conflict.");
146
+ records.append({ id: recordStore.newRecordId(), rootId: id, op: "delete", scope: scopeId, author: schema.clone(author), at: Date.now() });
147
+ return project(id, true, true);
148
+ }
149
+ function visible(options) {
150
+ var opts2 = options || {};
151
+ if (opts2.type !== undefined && schema.TYPES.indexOf(opts2.type) === -1) throw new Error("Invalid issue type filter.");
152
+ if (opts2.status !== undefined && schema.STATUSES.indexOf(opts2.status) === -1) throw new Error("Invalid issue status filter.");
153
+ if (opts2.priority !== undefined && schema.PRIORITIES.indexOf(opts2.priority) === -1) throw new Error("Invalid issue priority filter.");
154
+ var all = records.all();
155
+ var ids = {}; var order = [];
156
+ for (var i = 0; i < all.length; i++) if ((all[i].op === "revision" || all[i].op === "incorporate") && !ids[all[i].rootId]) { ids[all[i].rootId] = true; order.push(all[i].rootId); }
157
+ var out = [];
158
+ for (var j = 0; j < order.length; j++) {
159
+ var item = project(order[j], false);
160
+ if (!item) continue;
161
+ if (opts2.type && item.type !== opts2.type) continue;
162
+ if (opts2.priority && item.priority !== opts2.priority) continue;
163
+ if (opts2.status && item.status !== opts2.status) continue;
164
+ if (opts2.changeSetId && (!item.context || item.context.changeSetId !== opts2.changeSetId)) continue;
165
+ if (opts2.query) {
166
+ var full = project(order[j], true);
167
+ if ((full.title + " " + full.summary + " " + full.body).toLowerCase().indexOf(String(opts2.query).toLowerCase()) === -1) continue;
168
+ }
169
+ out.push(item);
170
+ }
171
+ out.sort(function (a, b) { return b.updatedAt - a.updatedAt || a.ref.localeCompare(b.ref); });
172
+ var limit = Number(opts2.limit);
173
+ if (!Number.isInteger(limit)) limit = 20;
174
+ if (limit < 1 || limit > MAX_PAGE) throw new Error("Issue page limit must be between 1 and 50.");
175
+ var offset = opts2.cursor === undefined || opts2.cursor === null || opts2.cursor === "" ? 0 : Number(opts2.cursor);
176
+ if (!Number.isInteger(offset) || offset < 0) throw new Error("Invalid issue page cursor.");
177
+ return { issues: out.slice(offset, offset + limit), total: out.length, nextCursor: offset + limit < out.length ? String(offset + limit) : null };
178
+ }
179
+ function read(id, includeDeleted) { if (!REF.test(id)) throw new Error("Invalid issue reference."); var result = project(id, true, includeDeleted === true); if (!result) throw new Error("Issue not found."); return result; }
180
+ function history(id) {
181
+ var chain = chainFor(id); if (!chain.length) throw new Error("Issue not found.");
182
+ var previous = null;
183
+ var revisions = chain.filter(function (r) { return r.op === "revision" || r.op === "incorporate"; }).map(function (r) {
184
+ var keys = {};
185
+ Object.keys(previous || {}).concat(Object.keys(r.snapshot || {})).forEach(function (key) { keys[key] = true; });
186
+ var changed = Object.keys(keys).filter(function (key) {
187
+ return JSON.stringify(previous && previous[key]) !== JSON.stringify(r.snapshot && r.snapshot[key]);
188
+ });
189
+ previous = r.snapshot;
190
+ var item = { revision: r.revision, at: r.at, author: schema.clone(r.author), status: r.snapshot.status, changed: changed };
191
+ if (r.review) item.review = schema.clone(r.review);
192
+ return item;
193
+ });
194
+ var deleted = chain.filter(function (r) { return r.op === "delete"; }).pop();
195
+ if (deleted) revisions.push({ revision: revisions.length + 1, op: "delete", at: deleted.at, author: schema.clone(deleted.author), changed: [] });
196
+ return { ref: id, revisions: revisions, deleted: !!deleted };
197
+ }
198
+ function readRevision(id, revision) {
199
+ if (!REF.test(id)) throw new Error("Invalid issue reference.");
200
+ var chain = chainFor(id); var target = Number(revision); var found = null;
201
+ for (var i = 0; i < chain.length; i++) if ((chain[i].op === "revision" || chain[i].op === "incorporate") && chain[i].revision === target) found = chain[i];
202
+ if (!found) throw new Error("Issue revision not found.");
203
+ var result = schema.clone(found.snapshot); result.ref = id; result.revision = target; result.author = schema.clone(found.author); result.at = found.at;
204
+ if (found.review) result.review = schema.clone(found.review);
205
+ return result;
206
+ }
207
+ return { scopeId: scopeId, filePath: records.filePath, create: create, update: update, comment: comment, review: review, incorporate: incorporate, remove: remove, list: visible, search: visible, read: read, history: history, readRevision: readRevision, records: records };
208
+ }
209
+
210
+ module.exports = { issueScopeIdForKnowledgeId: issueScopeIdForKnowledgeId, createIssuesStore: createIssuesStore, REF: REF };
@@ -0,0 +1,47 @@
1
+ var validateCron = require("./schedule-validation").validateCron;
2
+
3
+ function parseField(field, min, max) {
4
+ var values = [];
5
+ var parts = field.split(",");
6
+ for (var i = 0; i < parts.length; i++) {
7
+ var part = parts[i].trim();
8
+ if (part.indexOf("/") !== -1) {
9
+ var slash = part.split("/");
10
+ var step = parseInt(slash[1], 10);
11
+ var range = slash[0] === "*" ? [min, max] : slash[0].split("-").map(function (value) { return parseInt(value, 10); });
12
+ if (range.length === 1) range.push(range[0]);
13
+ for (var stepped = range[0]; stepped <= range[1]; stepped += step) values.push(stepped);
14
+ } else if (part === "*") {
15
+ for (var any = min; any <= max; any++) values.push(any);
16
+ } else if (part.indexOf("-") !== -1) {
17
+ var bounds = part.split("-");
18
+ for (var ranged = parseInt(bounds[0], 10); ranged <= parseInt(bounds[1], 10); ranged++) values.push(ranged);
19
+ } else values.push(parseInt(part, 10));
20
+ }
21
+ return values;
22
+ }
23
+
24
+ function parse(expr) {
25
+ if (!validateCron(expr).ok) return null;
26
+ var fields = expr.trim().split(/\s+/);
27
+ return { minutes: parseField(fields[0], 0, 59), hours: parseField(fields[1], 0, 23), daysOfMonth: parseField(fields[2], 1, 31), months: parseField(fields[3], 1, 12), daysOfWeek: parseField(fields[4], 0, 6) };
28
+ }
29
+
30
+ function matches(parsed, date) {
31
+ return parsed.minutes.indexOf(date.getMinutes()) !== -1 && parsed.hours.indexOf(date.getHours()) !== -1 &&
32
+ parsed.daysOfMonth.indexOf(date.getDate()) !== -1 && parsed.months.indexOf(date.getMonth() + 1) !== -1 && parsed.daysOfWeek.indexOf(date.getDay()) !== -1;
33
+ }
34
+
35
+ function nextRunTime(expr, after) {
36
+ var parsed = parse(expr);
37
+ if (!parsed) return null;
38
+ var date = new Date(after || Date.now());
39
+ date.setSeconds(0, 0); date.setMinutes(date.getMinutes() + 1);
40
+ for (var i = 0; i < 366 * 24 * 60; i++) {
41
+ if (matches(parsed, date)) return date.getTime();
42
+ date.setMinutes(date.getMinutes() + 1);
43
+ }
44
+ return null;
45
+ }
46
+
47
+ module.exports = { nextRunTime: nextRunTime };
@@ -0,0 +1,159 @@
1
+ var crypto = require("crypto");
2
+
3
+ function identity(prefix, projectId, scheduleIdentity, scheduleEpoch, occurrenceAt) {
4
+ var raw = projectId + "\n" + scheduleIdentity + "\n" + scheduleEpoch + "\n" + occurrenceAt;
5
+ return prefix + crypto.createHash("sha256").update(raw).digest("hex");
6
+ }
7
+
8
+ function attachLoopDurableSchedule(ctx) {
9
+ var scheduler = ctx.scheduler;
10
+ var registry = ctx.registry;
11
+ var type = "loop-occurrence:" + ctx.projectId;
12
+ var unregister = null;
13
+ var syncing = false;
14
+
15
+ function isMultiUser() { return typeof ctx.isMultiUser === "function" ? ctx.isMultiUser() : !!ctx.isMultiUser; }
16
+
17
+ function ownerFor(record) {
18
+ if (!isMultiUser()) return "local-user";
19
+ return typeof record.ownerId === "string" && record.ownerId ? record.ownerId : null;
20
+ }
21
+
22
+ function jobsFor(recordId) {
23
+ return scheduler.listJobs().filter(function (job) {
24
+ return job.type === type && job.projectId === ctx.projectId && job.target && job.target.id === recordId;
25
+ });
26
+ }
27
+
28
+ function queuedFor(recordId) {
29
+ var jobs = jobsFor(recordId);
30
+ for (var i = jobs.length - 1; i >= 0; i--) if (jobs[i].state === "queued") return jobs[i];
31
+ return null;
32
+ }
33
+
34
+ function spec(record, ownerId) {
35
+ var dueAt = record.nextRunAt;
36
+ var occurrenceKey = identity("occurrence:", ctx.projectId, record.scheduleIdentity, record.scheduleEpoch, dueAt);
37
+ return {
38
+ idempotencyKey: "loop-" + occurrenceKey,
39
+ type: type,
40
+ ownerId: ownerId,
41
+ projectId: ctx.projectId,
42
+ target: { kind: "loop-record", id: record.id },
43
+ payload: { recordId: record.id, occurrenceAt: dueAt, occurrenceKey: occurrenceKey, scheduleIdentity: record.scheduleIdentity, scheduleEpoch: record.scheduleEpoch, recordRevision: Number(record.updatedAt || 0) },
44
+ runAt: dueAt,
45
+ };
46
+ }
47
+
48
+ function matchesOccurrence(job, record) {
49
+ return job && job.payload && job.payload.scheduleIdentity === record.scheduleIdentity &&
50
+ Number(job.payload.scheduleEpoch) === Number(record.scheduleEpoch) && job.payload.occurrenceAt === record.nextRunAt;
51
+ }
52
+
53
+ function cancelQueued(recordId, reason) {
54
+ var jobs = jobsFor(recordId);
55
+ for (var i = 0; i < jobs.length; i++) {
56
+ if (jobs[i].state === "queued") scheduler.cancel(jobs[i].id, reason);
57
+ }
58
+ }
59
+
60
+ function syncRecord(record) {
61
+ var related = jobsFor(record.id);
62
+ var persistedDueOwned = related.some(function (job) {
63
+ return ["queued", "claimed", "running"].indexOf(job.state) !== -1 && matchesOccurrence(job, record);
64
+ });
65
+ var terminal = null;
66
+ for (var ti = related.length - 1; ti >= 0; ti--) {
67
+ if (["completed", "failed", "interrupted"].indexOf(related[ti].state) !== -1 && matchesOccurrence(related[ti], record)) { terminal = related[ti]; break; }
68
+ }
69
+ if (terminal && record.enabled && Number.isFinite(record.nextRunAt)) {
70
+ if (!registry.reconcileDurableOccurrence(record.id, record.nextRunAt)) throw new Error("The interrupted occurrence could not be reconciled.");
71
+ record = registry.getById(record.id); related = jobsFor(record.id); persistedDueOwned = false;
72
+ }
73
+ if (!persistedDueOwned) {
74
+ record = registry.normalizeDurableOccurrence(record.id, Date.now());
75
+ if (!record) throw new Error("The scheduled occurrence could not be normalized.");
76
+ related = jobsFor(record.id);
77
+ }
78
+ var queued = queuedFor(record.id);
79
+ var ownerId = ownerFor(record);
80
+ if (isMultiUser() && !ownerId) {
81
+ if (queued) scheduler.cancel(queued.id, "record_requires_owner");
82
+ registry.markNeedsOwner(record.id);
83
+ return;
84
+ }
85
+ if (!record.enabled || !Number.isFinite(record.nextRunAt)) {
86
+ if (queued) scheduler.cancel(queued.id, "record_disabled_or_unscheduled");
87
+ return;
88
+ }
89
+ var wanted = spec(record, ownerId);
90
+ if (queued) {
91
+ if (matchesOccurrence(queued, record)) {
92
+ scheduler.replaceQueued(queued.id, wanted);
93
+ return;
94
+ }
95
+ scheduler.cancel(queued.id, "record_schedule_changed");
96
+ }
97
+ for (var i = related.length - 1; i >= 0; i--) {
98
+ if ((related[i].state === "claimed" || related[i].state === "running") && matchesOccurrence(related[i], record)) return;
99
+ if (related[i].state === "cancelled" && matchesOccurrence(related[i], record)) return;
100
+ }
101
+ scheduler.enqueue(wanted);
102
+ }
103
+
104
+ function sync() {
105
+ if (syncing) return;
106
+ syncing = true;
107
+ try {
108
+ var records = registry.getAll();
109
+ var present = Object.create(null);
110
+ for (var i = 0; i < records.length; i++) {
111
+ present[records[i].id] = true;
112
+ syncRecord(records[i]);
113
+ }
114
+ var jobs = scheduler.listJobs();
115
+ for (var ji = 0; ji < jobs.length; ji++) {
116
+ if (jobs[ji].type === type && jobs[ji].state === "queued" && !present[jobs[ji].target.id]) {
117
+ scheduler.cancel(jobs[ji].id, "record_deleted");
118
+ }
119
+ }
120
+ } finally { syncing = false; }
121
+ }
122
+
123
+ function canRun(job) {
124
+ var record = registry.getById(job.target.id);
125
+ if (!record || !record.enabled || !matchesOccurrence(job, record)) return true;
126
+ if (ownerFor(record) !== job.ownerId) return true;
127
+ if (ctx.authorize && !ctx.authorize(record, job.ownerId)) return false;
128
+ return !ctx.canRun || ctx.canRun(record);
129
+ }
130
+
131
+ function dispatch(job, execution) {
132
+ var record = registry.getById(job.target.id);
133
+ if (!record) throw new Error("The scheduled record was deleted.");
134
+ if (!record.enabled || !matchesOccurrence(job, record)) throw new Error("The scheduled occurrence is stale.");
135
+ if (ownerFor(record) !== job.ownerId) throw new Error("The scheduled record owner changed.");
136
+ if (ctx.authorize && !ctx.authorize(record, job.ownerId)) throw new Error("The scheduled record owner is no longer authorized.");
137
+ execution.recordMetadata({ projectId: ctx.projectId, recordId: record.id, occurrenceAt: job.payload.occurrenceAt, recordRevision: Number(record.updatedAt || 0) });
138
+ var prepared = registry.prepareDurableOccurrence(record.id, job.payload.occurrenceAt);
139
+ if (!prepared || prepared.ok === false) throw new Error(prepared && prepared.error || "The scheduled occurrence could not be reserved.");
140
+ execution.recordReceipt({ state: "dispatching", projectId: ctx.projectId, recordId: record.id, occurrenceAt: job.payload.occurrenceAt, recordedAt: Date.now() });
141
+ var triggerError = null;
142
+ try { registry.triggerDurableOccurrence(record.id, prepared.value); }
143
+ catch (error) { triggerError = error; }
144
+ if (!registry.completeDurableOccurrence(record.id, job.payload.occurrenceAt)) throw new Error("The next scheduled occurrence could not be stored after dispatch.");
145
+ if (triggerError) throw triggerError;
146
+ return { dispatched: true, recordId: record.id, occurrenceAt: job.payload.occurrenceAt };
147
+ }
148
+
149
+ unregister = scheduler.registerHandler(type, dispatch, { canRun: canRun });
150
+ sync();
151
+ return {
152
+ sync: sync,
153
+ wake: function () { try { scheduler.tick(); } catch (error) {} },
154
+ shutdown: function () { if (unregister) { unregister(); unregister = null; } },
155
+ type: type,
156
+ };
157
+ }
158
+
159
+ module.exports = { attachLoopDurableSchedule: attachLoopDurableSchedule };
@@ -0,0 +1,35 @@
1
+ var MAX_CONTEXT = 12000;
2
+
3
+ function bounded(value) {
4
+ return typeof value === "string" ? value.substring(0, MAX_CONTEXT) : "";
5
+ }
6
+
7
+ function commonGuidance() {
8
+ return "Begin by clarifying any missing goal, scope, constraints, or target files. Inspect the relevant project code and tests before drafting. Produce a self-contained, goal-oriented brief that names the intended outcome, boundaries, and concrete evidence-based completion checks; every check must identify an observable artifact, behavior, or test result, never vague quality language. Review the brief with the user and refine it before any Start or file write. Do not claim completion without the stated evidence.";
9
+ }
10
+
11
+ function structuredQuestionGuidance() {
12
+ return "When clarification is needed, prefer the available structured question capability: AskUserQuestion, request_user_input, or Clay ask_user_questions (including its namespaced fallback). Ask only the minimum necessary, at most 1-3 concise questions per turn; provide 2-6 choices for bounded decisions and use a freeform question when choices would constrain the answer. Do not repeat information already established. Use plain chat only when no structured question capability is available.";
13
+ }
14
+
15
+ function interviewPrompt() {
16
+ return commonGuidance() + " " + structuredQuestionGuidance() + " Use the current chat and repository context. This is the built-in interactive Loop interview: after review, call propose_loop with the bounded objective, criteria, and limits. Do not execute, arm permissions, invoke /clay-ralph, require skills, use plan mode, create PROMPT.md/JUDGE.md, or start a run; wait for the human Start action.";
17
+ }
18
+
19
+ function legacyCraftPrompt(options) {
20
+ var opts = options || {};
21
+ var task = bounded(opts.task);
22
+ var directory = bounded(opts.directory);
23
+ var filePlan = opts.simple ? "Create only PROMPT.md; this Simple Loop has no judge." : opts.userJudge ? "Create only PROMPT.md; preserve the user's JUDGE.md and do not replace or edit it." : opts.judgeOnly ? "Create only JUDGE.md for the existing PROMPT.md; do not create or modify PROMPT.md." : "Create PROMPT.md and, when this is a judged Loop, JUDGE.md.";
24
+ return commonGuidance() + " " + structuredQuestionGuidance() + " This is legacy scheduled Loop file crafting. " +
25
+ "The files are the durable handoff to a future fresh session: that session starts from the target directory and reads the files from disk, so make each file self-contained and do not rely on this chat's transient context. " +
26
+ "Respect any user-provided files and the target directory; inspect existing files before writing and never overwrite a user-provided file. " + filePlan + " " +
27
+ "A separate judge is applicable only for judged Loops; it evaluates observable evidence from the fresh execution session and must not perform the task itself. Never make an automatic commit.\n\n" +
28
+ "## Task\n" + task + "\n\n## Loop Directory\n" + directory;
29
+ }
30
+
31
+ module.exports = {
32
+ commonGuidance: commonGuidance,
33
+ interviewPrompt: interviewPrompt,
34
+ legacyCraftPrompt: legacyCraftPrompt,
35
+ };
@@ -0,0 +1,76 @@
1
+ var recordRevision = require("./record-revision");
2
+
3
+ function attachLoopRegistryOccurrence(ctx) {
4
+ function normalize(id, now) {
5
+ var rec = ctx.getById(id);
6
+ if (!rec || !rec.enabled || !rec.scheduleRule || !rec.nextRunAt || rec.nextRunAt > now - 60000) return rec;
7
+ var next = ctx.nextRecordRunTime(rec, now - 60000);
8
+ if (Number(next) === Number(rec.nextRunAt)) return rec;
9
+ var previous = JSON.parse(JSON.stringify(rec));
10
+ rec.nextRunAt = next;
11
+ if (!next) rec.enabled = false;
12
+ if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return null; }
13
+ ctx.changed();
14
+ return rec;
15
+ }
16
+
17
+ function markNeedsOwner(id) {
18
+ var rec = ctx.getById(id);
19
+ if (!rec || rec.needsOwner === true) return false;
20
+ var previous = JSON.parse(JSON.stringify(rec));
21
+ rec.needsOwner = true;
22
+ rec.setupRequired = "owner";
23
+ rec.enabled = false;
24
+ rec.nextRunAt = null;
25
+ rec.scheduleEpoch = Math.max(1, Number(rec.scheduleEpoch) || 1) + 1;
26
+ rec.updatedAt = recordRevision.nextRevision(rec);
27
+ if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return false; }
28
+ ctx.changed();
29
+ return true;
30
+ }
31
+
32
+ function prepare(id, occurrenceAt) {
33
+ var rec = ctx.getById(id);
34
+ if (!rec || !rec.enabled || Number(rec.nextRunAt) !== Number(occurrenceAt)) return { ok: false, error: "The scheduled occurrence is stale." };
35
+ var previous = JSON.parse(JSON.stringify(rec));
36
+ var prepared = ctx.prepareTrigger ? ctx.prepareTrigger(rec) : null;
37
+ if (prepared && prepared.ok === false) return prepared;
38
+ ctx.scheduledRuns.prepareRecord(rec, prepared);
39
+ if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return { ok: false, error: ctx.lastSaveError() || "The occurrence reservation could not be stored." }; }
40
+ ctx.changed();
41
+ return { ok: true, value: prepared };
42
+ }
43
+
44
+ function trigger(id, prepared) {
45
+ var rec = ctx.getById(id);
46
+ if (!rec) throw new Error("The scheduled record was deleted.");
47
+ if (ctx.onTrigger) ctx.onTrigger(rec, prepared);
48
+ }
49
+
50
+ function advance(id, occurrenceAt, result) {
51
+ var rec = ctx.getById(id);
52
+ if (!rec || Number(rec.nextRunAt) !== Number(occurrenceAt)) return false;
53
+ var previous = JSON.parse(JSON.stringify(rec));
54
+ var now = Date.now();
55
+ rec.lastRunAt = result === "interrupted" ? occurrenceAt : now;
56
+ if (result) rec.lastRunResult = result;
57
+ if (rec.scheduleRule) ctx.scheduleRule.recordScheduleStartState(rec, occurrenceAt);
58
+ if (rec.cron || rec.scheduleRule) {
59
+ rec.nextRunAt = ctx.nextRecordRunTime(rec, now);
60
+ if (rec.scheduleRule && !rec.nextRunAt) rec.enabled = false;
61
+ } else {
62
+ rec.nextRunAt = null;
63
+ rec.enabled = false;
64
+ }
65
+ if (!ctx.save()) { recordRevision.restoreRecord(rec, previous); return false; }
66
+ ctx.changed();
67
+ return true;
68
+ }
69
+
70
+ function complete(id, occurrenceAt) { return advance(id, occurrenceAt, null); }
71
+ function reconcile(id, occurrenceAt) { return advance(id, occurrenceAt, "interrupted"); }
72
+
73
+ return { normalize: normalize, markNeedsOwner: markNeedsOwner, prepare: prepare, trigger: trigger, complete: complete, reconcile: reconcile };
74
+ }
75
+
76
+ module.exports = { attachLoopRegistryOccurrence: attachLoopRegistryOccurrence };
@@ -0,0 +1,15 @@
1
+ // Small dependency-free model catalog helpers shared by server resolvers.
2
+
3
+ function modelEntryValue(entry) {
4
+ if (!entry) return "";
5
+ if (typeof entry === "string") return entry;
6
+ return entry.value || entry.id || "";
7
+ }
8
+
9
+ function modelEntryMatches(entry, value) {
10
+ if (!entry || !value) return false;
11
+ if (typeof entry === "string") return entry === value;
12
+ return entry.value === value || entry.id === value || entry.resolvedModel === value;
13
+ }
14
+
15
+ module.exports = { modelEntryValue: modelEntryValue, modelEntryMatches: modelEntryMatches };