clay-server 4.1.0-beta.6 → 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 (90) 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/daemon.js +0 -2
  7. package/lib/default-ai-runtime.js +117 -0
  8. package/lib/git-cli.js +1 -0
  9. package/lib/issue-work-session.js +10 -0
  10. package/lib/issues-comments.js +40 -0
  11. package/lib/issues-commit-verification.js +30 -0
  12. package/lib/issues-mcp-server.js +27 -0
  13. package/lib/issues-schema.js +119 -0
  14. package/lib/issues-service.js +339 -0
  15. package/lib/issues-store.js +210 -0
  16. package/lib/model-entry.js +15 -0
  17. package/lib/project-capsule-turn.js +1 -0
  18. package/lib/project-connection.js +2 -0
  19. package/lib/project-delegated-session.js +1 -0
  20. package/lib/project-filesystem.js +10 -10
  21. package/lib/project-issue-launch.js +154 -0
  22. package/lib/project-issues.js +58 -0
  23. package/lib/project-logs-mcp-server.js +8 -12
  24. package/lib/project-message-delivery.js +4 -2
  25. package/lib/project-models.js +71 -3
  26. package/lib/project-pending-message-queue.js +264 -0
  27. package/lib/project-scheduled-tasks.js +2 -4
  28. package/lib/project-sessions.js +82 -31
  29. package/lib/project-tool-llm-runtime.js +48 -0
  30. package/lib/project-user-message.js +178 -42
  31. package/lib/project-workspace-query.js +25 -0
  32. package/lib/project.js +102 -18
  33. package/lib/public/app.js +38 -4
  34. package/lib/public/css/default-ai.css +33 -0
  35. package/lib/public/css/input.css +89 -5
  36. package/lib/public/css/issues.css +101 -0
  37. package/lib/public/css/messages.css +19 -0
  38. package/lib/public/css/pending-message-queue.css +187 -0
  39. package/lib/public/css/scheduled-tasks.css +3 -4
  40. package/lib/public/index.html +25 -17
  41. package/lib/public/modules/app-connection.js +3 -0
  42. package/lib/public/modules/app-cursors.js +29 -12
  43. package/lib/public/modules/app-message-router.js +8 -0
  44. package/lib/public/modules/app-messages.js +12 -3
  45. package/lib/public/modules/clay-file-links.js +111 -0
  46. package/lib/public/modules/clay-issue-links.js +131 -0
  47. package/lib/public/modules/clay-log-links.js +2 -0
  48. package/lib/public/modules/cursor-sharing-preference.js +38 -0
  49. package/lib/public/modules/default-ai.js +377 -0
  50. package/lib/public/modules/filebrowser.js +43 -2
  51. package/lib/public/modules/input.js +7 -17
  52. package/lib/public/modules/issues-context.js +117 -0
  53. package/lib/public/modules/issues-render.js +29 -0
  54. package/lib/public/modules/issues.js +270 -0
  55. package/lib/public/modules/markdown.js +4 -0
  56. package/lib/public/modules/message-delivery.js +5 -3
  57. package/lib/public/modules/pending-message-image-editor.js +143 -0
  58. package/lib/public/modules/pending-message-queue-model.js +22 -0
  59. package/lib/public/modules/pending-message-queue.js +449 -0
  60. package/lib/public/modules/scheduled-task-engine.js +0 -50
  61. package/lib/public/modules/scheduled-tasks.js +7 -16
  62. package/lib/public/modules/sidebar-mobile.js +3 -0
  63. package/lib/public/modules/sticky-note-markdown.js +1 -0
  64. package/lib/public/modules/terminal.js +2 -0
  65. package/lib/public/modules/tool-palette-order.js +1 -0
  66. package/lib/public/style.css +2 -0
  67. package/lib/sdk-bridge.js +34 -11
  68. package/lib/server-cursor-sharing.js +65 -0
  69. package/lib/server-default-ai.js +265 -0
  70. package/lib/server-home-chat.js +29 -9
  71. package/lib/server-home-clay-entry.js +4 -1
  72. package/lib/server-home-debate-planning.js +32 -14
  73. package/lib/server-home-mate-creation.js +30 -11
  74. package/lib/server-home-models.js +14 -6
  75. package/lib/server-issue-links.js +18 -0
  76. package/lib/server.js +41 -0
  77. package/lib/session-notes-mcp-server.js +1 -1
  78. package/lib/session-search.js +4 -1
  79. package/lib/tool-llm.js +1 -0
  80. package/lib/users-default-ai-preferences.js +80 -0
  81. package/lib/users-preferences.js +35 -0
  82. package/lib/users.js +10 -0
  83. package/lib/workspace-query-projection.js +29 -0
  84. package/lib/workspace-query-service.js +19 -29
  85. package/lib/ws-schema.js +36 -1
  86. package/lib/yoke/adapters/claude-worker.js +4 -2
  87. package/lib/yoke/adapters/claude.js +5 -1
  88. package/lib/yoke/package.json +1 -1
  89. package/package.json +5 -6
  90. package/bin/claude-relay.js +0 -6
@@ -0,0 +1,339 @@
1
+ // Authorization and capability bindings for the Phase 1 Issues backend.
2
+
3
+ var issuesStore = require("./issues-store");
4
+ var schema = require("./issues-schema");
5
+ var provenance = require("./session-provenance");
6
+ var commitVerification = require("./issues-commit-verification");
7
+ var accessScope = require("./project-logs-access-scope").attachProjectLogsAccessScope;
8
+
9
+ var PUBLIC_FIELDS = { title: true, summary: true, body: true, resolutionSummary: true,
10
+ type: true, priority: true, status: true, closeReason: true };
11
+
12
+ function clone(value) { return value == null ? value : JSON.parse(JSON.stringify(value)); }
13
+
14
+ function attachIssuesService(ctx) {
15
+ var context = ctx || {};
16
+ var projects = context.getProjects;
17
+ var multiUser = context.isMultiUser || function () { return false; };
18
+ var findUserById = context.findUserById || function () { return null; };
19
+ var stores = new Map();
20
+ var verify = commitVerification.injectedVerifier(context.verifyCommit);
21
+ var scope;
22
+
23
+ function projectFor(slug) {
24
+ var list = typeof projects === "function" ? projects() : null;
25
+ return typeof slug === "string" && list && typeof list.get === "function" ? list.get(slug) || null : null;
26
+ }
27
+
28
+ function statusFor(project) {
29
+ var status;
30
+ if (!project || typeof project.getStatus !== "function") return null;
31
+ try { status = project.getStatus(); } catch (e) { return null; }
32
+ if (!status || typeof status !== "object" || Array.isArray(status)) return null;
33
+ if (typeof status.slug !== "string" || !status.slug || typeof status.path !== "string" || !status.path) return null;
34
+ if (typeof status.projectKnowledgeId !== "string" || !status.projectKnowledgeId) return null;
35
+ if (status.projectOwnerId != null && typeof status.projectOwnerId !== "string") return null;
36
+ if (status.isWorktree === true && (typeof status.changeSetId !== "string" || !status.changeSetId || typeof status.parentSlug !== "string" || !status.parentSlug)) return null;
37
+ return status;
38
+ }
39
+
40
+ function governingStatus(status) {
41
+ if (!status || status.isWorktree !== true) return status;
42
+ return statusFor(projectFor(status.parentSlug));
43
+ }
44
+
45
+ function ownerKnown(status) {
46
+ return !multiUser() || !!status.projectOwnerId && !!findUserById(status.projectOwnerId);
47
+ }
48
+
49
+ function openStore(status) {
50
+ var governing = governingStatus(status);
51
+ if (!governing || !governing.path || !governing.projectKnowledgeId) throw new Error("Issues are unavailable for this project.");
52
+ var key = governing.projectKnowledgeId;
53
+ if (!stores.has(key)) stores.set(key, issuesStore.createIssuesStore({ projectKnowledgeId: key, baseDir: context.baseDir }));
54
+ return stores.get(key);
55
+ }
56
+
57
+ function issueContext(status) {
58
+ if (!status || status.isWorktree !== true || !status.changeSetId) return null;
59
+ return { kind: "worktree", changeSetId: status.changeSetId, branch: status.branch || null,
60
+ baseCommit: status.baseCommit || null, headCommit: status.headCommit || null };
61
+ }
62
+
63
+ function author(user, type, session) {
64
+ return { type: type, userId: user ? user.id || null : null,
65
+ displayName: user && (user.displayName || user.username) || null,
66
+ sessionKey: session ? session.cliSessionId || "local:" + session.localId : null,
67
+ sessionOriginId: session ? session.sessionOriginId || null : null,
68
+ vendor: session ? session.vendor || null : null, at: Date.now() };
69
+ }
70
+
71
+ function cleanPublic(input, controls, allowCloseReason) {
72
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Issue arguments are required.");
73
+ var keys = Object.keys(input);
74
+ var result = {};
75
+ for (var i = 0; i < keys.length; i++) {
76
+ var key = keys[i];
77
+ if (Object.prototype.hasOwnProperty.call(controls, key)) continue;
78
+ if (!Object.prototype.hasOwnProperty.call(PUBLIC_FIELDS, key) || (key === "closeReason" && !allowCloseReason)) throw new Error("Unknown or server-owned issue field: " + key + ".");
79
+ result[key] = input[key];
80
+ }
81
+ return result;
82
+ }
83
+
84
+ function osUsersEnabled() {
85
+ return typeof context.osUsersEnabled === "function" ? context.osUsersEnabled() === true : context.osUsersEnabled === true;
86
+ }
87
+
88
+ function osIdentity(state) {
89
+ if (!osUsersEnabled()) return null;
90
+ var identity = typeof context.getOsUserInfoForActor === "function" ? context.getOsUserInfoForActor(state.user) : null;
91
+ if (!identity || typeof identity !== "object") throw new Error("An OS identity is required for commit verification.");
92
+ return clone(identity);
93
+ }
94
+
95
+ scope = accessScope({ projectFor: projectFor, isMultiUser: multiUser,
96
+ canAccessProject: context.canAccessProject, hasFullProjectAccess: context.hasFullProjectAccess,
97
+ openStore: function () { return null; } });
98
+
99
+ function bindingState(project, session, userId, originId) {
100
+ var status = statusFor(project);
101
+ if (!status || status.isMate === true) return null;
102
+ var governing = governingStatus(status);
103
+ if (!governing || !ownerKnown(governing)) return null;
104
+ var user = null;
105
+ if (multiUser()) {
106
+ if (!userId || !(user = findUserById(userId))) return null;
107
+ } else {
108
+ user = userId ? findUserById(userId) : null;
109
+ if (!user) user = { id: userId || status.projectOwnerId || null };
110
+ }
111
+ if (session) {
112
+ var manager = project.getSessionManager ? project.getSessionManager() : project.sm;
113
+ if (!manager || !manager.sessions || manager.sessions.get(session.localId) !== session) return null;
114
+ if ((session.ownerId || null) !== (userId || null) || (session.sessionOriginId || null) !== originId) return null;
115
+ if (multiUser() && !session.ownerId) return null;
116
+ if (provenance.isWorker(session) || session.hidden === true || session.delegated === true || session._pairDelegation || session._delegatedBy) return null;
117
+ }
118
+ var principal = multiUser() ? { userId: user.id, singleUserOwnerId: null }
119
+ : { userId: null, singleUserOwnerId: user.id || status.projectOwnerId || null };
120
+ var authorization;
121
+ try { authorization = scope.projectAuthorization(principal, status); } catch (e) { return null; }
122
+ if (!authorization.allowed || (authorization.worktreeOnly && !status.changeSetId)) return null;
123
+ principal.isProjectOwner = scope.ownsProject(principal, governing);
124
+ return { project: project, status: status, session: session, user: user,
125
+ principal: principal, worktreeOnly: authorization.worktreeOnly };
126
+ }
127
+
128
+ function bind(source, isSession) {
129
+ if (!source || typeof source.projectSlug !== "string") return null;
130
+ var initialSlug = source.projectSlug;
131
+ var initialProject = projectFor(initialSlug);
132
+ var initialSession = isSession ? source.session : null;
133
+ if (!initialProject || (isSession && !initialSession)) return null;
134
+ if (initialSession) provenance.ensureOrigin(initialSession);
135
+ var initialUserId = initialSession ? initialSession.ownerId || null : source.userId || null;
136
+ var initialOrigin = initialSession ? initialSession.sessionOriginId || null : null;
137
+ var initial = bindingState(initialProject, initialSession, initialUserId, initialOrigin);
138
+ if (!initial) return null;
139
+
140
+ function current() {
141
+ if (projectFor(initialSlug) !== initialProject) throw new Error("This Issues binding is no longer valid.");
142
+ var live = bindingState(initialProject, initialSession, initialUserId, initialOrigin);
143
+ if (!live) throw new Error("This Issues binding is no longer valid.");
144
+ return live;
145
+ }
146
+
147
+ function visibleIssue(store, ref, state) {
148
+ var result = store.read(ref);
149
+ if (state.worktreeOnly && (!result.context || result.context.changeSetId !== state.status.changeSetId)) throw new Error("Issue not found.");
150
+ return result;
151
+ }
152
+ function scopedIssue(store, ref, state) {
153
+ var result = store.read(ref, true);
154
+ if (state.worktreeOnly && (!result.context || result.context.changeSetId !== state.status.changeSetId)) throw new Error("Issue not found.");
155
+ return result;
156
+ }
157
+
158
+ function scopedOptions(state, args) {
159
+ var result = Object.assign({}, args || {});
160
+ if (state.worktreeOnly) {
161
+ if (!state.status.changeSetId) throw new Error("Issues are unavailable without a change set.");
162
+ result.changeSetId = state.status.changeSetId;
163
+ }
164
+ return result;
165
+ }
166
+
167
+ function operation(state, needsIdentity) {
168
+ var governing = governingStatus(state.status);
169
+ return { project: state.project, session: state.session, userId: state.user.id || null,
170
+ path: state.status.path, knowledgeId: governing.projectKnowledgeId,
171
+ changeSetId: state.status.changeSetId || null, worktreeOnly: state.worktreeOnly,
172
+ recordContext: clone(issueContext(state.status)),
173
+ actor: clone(author(state.user, state.session ? "session" : "user", state.session)),
174
+ osIdentity: needsIdentity ? osIdentity(state) : null };
175
+ }
176
+
177
+ function recheck(before, needsIdentity) {
178
+ var fresh = current();
179
+ var governing = governingStatus(fresh.status);
180
+ var liveIdentity = needsIdentity ? osIdentity(fresh) : null;
181
+ if (fresh.project !== before.project || fresh.session !== before.session || (fresh.user.id || null) !== before.userId ||
182
+ fresh.status.path !== before.path || !governing || governing.projectKnowledgeId !== before.knowledgeId ||
183
+ (fresh.status.changeSetId || null) !== before.changeSetId || fresh.worktreeOnly !== before.worktreeOnly ||
184
+ JSON.stringify(liveIdentity) !== JSON.stringify(before.osIdentity)) throw new Error("Issue authorization changed during the operation.");
185
+ return fresh;
186
+ }
187
+
188
+ function evidence(before, sha) {
189
+ return verify(before.path, sha, before.knowledgeId, before.osIdentity).then(function (proof) {
190
+ var normalized = schema.normalizeVerifiedCommit(proof);
191
+ if (normalized.repository !== before.knowledgeId) throw new Error("Verified commit evidence belongs to a different repository.");
192
+ return normalized;
193
+ });
194
+ }
195
+
196
+ return {
197
+ linkWorkSession: function (args, session) {
198
+ var state = current();
199
+ var manager = state.project.getSessionManager();
200
+ if (!args || !Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required.");
201
+ if (!session || manager.sessions.get(session.localId) !== session || provenance.isWorker(session) || session.hidden === true ||
202
+ session.delegated === true || session._pairDelegation || session._delegatedBy ||
203
+ (session.ownerId || null) !== (state.user.id || null)) throw new Error("Invalid work session.");
204
+ provenance.ensureOrigin(session);
205
+ var target = openStore(state.status);
206
+ var entry = visibleIssue(target, args.ref, state);
207
+ if (entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict.");
208
+ if (entry.status === "resolved" || entry.status === "closed") throw new Error("Reopen this issue before starting work.");
209
+ var links = entry.linkedWorkSessions || [];
210
+ if (links.some(function (link) { return link.sessionId === session.sessionOriginId; })) return entry;
211
+ var link = { sessionId: session.sessionOriginId, role: "driver", issueRevision: args.expectedRevision };
212
+ if (args.requestId !== undefined) link.requestId = schema.text(args.requestId, "work request id", 200, true);
213
+ return target.update(args.ref, { linkedWorkSessions: links.concat([link]), status: "in_progress" }, args.expectedRevision, author(state.user, "user", null));
214
+ },
215
+ resolveWorkSession: function (args) {
216
+ var state = current();
217
+ var entry = visibleIssue(openStore(state.status), args.ref, state);
218
+ if (!(entry.linkedWorkSessions || []).some(function (link) { return link.sessionId === args.sessionOriginId; })) throw new Error("Work session unavailable.");
219
+ var found = null;
220
+ state.project.getSessionManager().sessions.forEach(function (session) {
221
+ if (session.sessionOriginId === args.sessionOriginId && (session.ownerId || null) === (state.user.id || null) &&
222
+ !provenance.isWorker(session) && session.hidden !== true && session.delegated !== true && !session._pairDelegation && !session._delegatedBy) found = session;
223
+ });
224
+ if (!found) throw new Error("Work session unavailable.");
225
+ return found;
226
+ },
227
+ scopeId: "project/" + governingStatus(initial.status).projectKnowledgeId + "/issues",
228
+ canComment: true,
229
+ canDelete: initial.principal.isProjectOwner === true,
230
+ listIssues: function (args) { var state = current(); return openStore(state.status).list(scopedOptions(state, args)); },
231
+ searchIssues: function (args) { var state = current(); return openStore(state.status).search(scopedOptions(state, args)); },
232
+ readIssue: function (args) { var state = current(); return visibleIssue(openStore(state.status), args && args.ref, state); },
233
+ readIssueTombstone: function (args) { var state = current(); return scopedIssue(openStore(state.status), args && args.ref, state); },
234
+ issueHistory: function (args) { var state = current(); var store = openStore(state.status); var history = store.history(args && args.ref); if (state.worktreeOnly) { var historical = scopedIssue(store, args && args.ref, state); if (!historical.context || historical.context.changeSetId !== state.status.changeSetId) throw new Error("Issue not found."); } return history; },
235
+ readIssueRevision: function (args) { var state = current(); var store = openStore(state.status); visibleIssue(store, args && args.ref, state); return store.readRevision(args.ref, args.revision); },
236
+ commentIssue: function (args) { var state = current(); var target = openStore(state.status); visibleIssue(target, args && args.ref, state); return target.comment(args.ref, args.body, author(state.user, "user", null)); },
237
+ listIssueFeedback: function (args) {
238
+ if (!isSession) throw new Error("Only the Project Driver can review issue comments.");
239
+ var state = current(); var target = openStore(state.status); var out = []; var feedbackLimit = args && args.limit === undefined ? 200 : Number(args.limit);
240
+ if (!Number.isInteger(feedbackLimit) || feedbackLimit < 1 || feedbackLimit > 200) throw new Error("Feedback limit must be between 1 and 200.");
241
+ var position = { issueCursor: null, issueIndex: 0, commentOffset: 0 };
242
+ if (args && args.cursor) {
243
+ try {
244
+ if (/^\d+$/.test(String(args.cursor))) position.issueCursor = String(args.cursor);
245
+ else position = JSON.parse(Buffer.from(String(args.cursor), "base64url").toString("utf8"));
246
+ } catch (e) { throw new Error("Invalid issue feedback cursor."); }
247
+ if (!position || (position.issueCursor !== null && typeof position.issueCursor !== "string") || !Number.isInteger(position.issueIndex) || position.issueIndex < 0 || !Number.isInteger(position.commentOffset) || position.commentOffset < 0) throw new Error("Invalid issue feedback cursor.");
248
+ }
249
+ var nextCursor = null; var scanned = 0;
250
+ while (out.length < feedbackLimit && scanned < 500) {
251
+ var result = target.list(scopedOptions(state, { limit: 50, cursor: position.issueCursor }));
252
+ if (!result.issues.length) break;
253
+ scanned += result.issues.length;
254
+ var firstIssueIndex = position.issueIndex;
255
+ if (firstIssueIndex >= result.issues.length) throw new Error("Invalid issue feedback cursor.");
256
+ for (var i = firstIssueIndex; i < result.issues.length && out.length < feedbackLimit; i++) {
257
+ var entry = visibleIssue(target, result.issues[i].ref, state);
258
+ var waiting = (entry.comments || []).filter(function (item) { return item.status === "pending"; });
259
+ var start = i === position.issueIndex ? position.commentOffset : 0;
260
+ var j = start;
261
+ for (; j < waiting.length && out.length < feedbackLimit; j++) out.push({ ref: entry.ref, title: entry.title, commentId: waiting[j].id, body: waiting[j].body, author: waiting[j].author, at: waiting[j].at });
262
+ if (out.length >= feedbackLimit) {
263
+ if (j < waiting.length) nextCursor = Buffer.from(JSON.stringify({ issueCursor: position.issueCursor, issueIndex: i, commentOffset: j })).toString("base64url");
264
+ else if (i + 1 < result.issues.length) nextCursor = Buffer.from(JSON.stringify({ issueCursor: position.issueCursor, issueIndex: i + 1, commentOffset: 0 })).toString("base64url");
265
+ else if (result.nextCursor) nextCursor = Buffer.from(JSON.stringify({ issueCursor: result.nextCursor, issueIndex: 0, commentOffset: 0 })).toString("base64url");
266
+ }
267
+ }
268
+ if (nextCursor || !result.nextCursor) break;
269
+ if (scanned >= 500) {
270
+ nextCursor = Buffer.from(JSON.stringify({ issueCursor: result.nextCursor, issueIndex: 0, commentOffset: 0 })).toString("base64url");
271
+ break;
272
+ }
273
+ position.issueCursor = result.nextCursor; position.issueIndex = 0; position.commentOffset = 0;
274
+ }
275
+ return { feedback: out, total: out.length, nextCursor: nextCursor, truncated: !!nextCursor };
276
+ },
277
+ reviewIssueComment: function (args) {
278
+ if (!isSession) throw new Error("Only the Project Driver can review issue comments.");
279
+ var state = current(); var target = openStore(state.status); var entry = visibleIssue(target, args && args.ref, state);
280
+ if (args.action === "incorporate") {
281
+ if (!Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required to incorporate a comment.");
282
+ var data = cleanPublic(args, { ref: true, expectedRevision: true, commentId: true, action: true, response: true, commitSha: true }, true);
283
+ if (data.status === "resolved") throw new Error("Use update_issue with verified commit evidence before resolving this Issue.");
284
+ return target.incorporate(entry.ref, args.commentId, data, args.expectedRevision, args.response, author(state.user, "session", state.session));
285
+ }
286
+ return target.review(entry.ref, args.commentId, args.action, args.response, author(state.user, "session", state.session));
287
+ },
288
+ removeIssue: function (args) {
289
+ var state = current();
290
+ if (!state.principal.isProjectOwner) throw new Error("Only the project owner can delete Issues.");
291
+ var target = openStore(state.status); var entry = visibleIssue(target, args && args.ref, state);
292
+ if (!Number.isInteger(args && args.expectedRevision) || entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict.");
293
+ return target.remove(entry.ref, author(state.user, "user", null), args.expectedRevision);
294
+ },
295
+ createIssue: function (args) {
296
+ if (!isSession) throw new Error("People cannot create issues directly. Ask the Project Driver to create one.");
297
+ var data = cleanPublic(args, { commitSha: true }, true);
298
+ var status = data.status === undefined ? "open" : data.status;
299
+ if (schema.STATUSES.indexOf(status) === -1) throw new Error("Invalid status.");
300
+ var state = current();
301
+ if (status !== "resolved") return Promise.resolve(openStore(state.status).create(data,
302
+ author(state.user, state.session ? "session" : "user", state.session), issueContext(state.status)));
303
+ if (typeof data.resolutionSummary !== "string" || !data.resolutionSummary.trim()) throw new Error("resolutionSummary is required when resolving an issue.");
304
+ var before = operation(state, true);
305
+ return evidence(before, args.commitSha).then(function (proof) {
306
+ var after = recheck(before, true);
307
+ return openStore(after.status).create(data, before.actor, before.recordContext, proof);
308
+ });
309
+ },
310
+ updateIssue: function (args) {
311
+ var data = cleanPublic(args, { ref: true, expectedRevision: true, commitSha: true }, true);
312
+ if (!Number.isInteger(args.expectedRevision)) throw new Error("expectedRevision is required.");
313
+ if (data.status !== undefined && schema.STATUSES.indexOf(data.status) === -1) throw new Error("Invalid status.");
314
+ var ref = args.ref;
315
+ var expectedRevision = args.expectedRevision;
316
+ var state = current();
317
+ var target = openStore(state.status);
318
+ var existing = visibleIssue(target, ref, state);
319
+ if (existing.revision !== expectedRevision) throw new Error("Issue revision conflict.");
320
+ if (data.status !== "resolved") return Promise.resolve(target.update(ref, data, expectedRevision,
321
+ author(state.user, state.session ? "session" : "user", state.session)));
322
+ if (typeof data.resolutionSummary !== "string" || !data.resolutionSummary.trim()) throw new Error("resolutionSummary is required when resolving an issue.");
323
+ var before = operation(state, true);
324
+ return evidence(before, args.commitSha).then(function (proof) {
325
+ var after = recheck(before, true);
326
+ var afterStore = openStore(after.status);
327
+ visibleIssue(afterStore, ref, after);
328
+ return afterStore.update(ref, data, expectedRevision, before.actor, null, proof);
329
+ });
330
+ },
331
+ };
332
+ }
333
+
334
+ return { bindUser: function (source) { return bind(source, false); },
335
+ bindProjectSession: function (source) { return bind(source, true); },
336
+ issueScopeIdForKnowledgeId: issuesStore.issueScopeIdForKnowledgeId };
337
+ }
338
+
339
+ module.exports = { attachIssuesService: attachIssuesService };
@@ -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,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 };
@@ -41,6 +41,7 @@ function attachCapsuleTurn(ctx) {
41
41
  ownerId: principal.userId || null,
42
42
  vendor: selection.vendor,
43
43
  model: selection.model,
44
+ effort: selection.effort || null,
44
45
  });
45
46
  session.title = (options.toolName || options.toolId) + " game";
46
47
  session.capsuleGame = { toolId: options.toolId };
@@ -96,6 +96,7 @@ function attachConnection(ctx) {
96
96
  var getProject = ctx.getProject;
97
97
  var warmup = ctx.warmup;
98
98
  var scheduledMessages = ctx.scheduledMessages;
99
+ var sendCursorSharingState = ctx.sendCursorSharingState;
99
100
 
100
101
  // Adapters are initialized lazily: the first websocket connection into
101
102
  // this project triggers warmup. Without this guard we would either keep
@@ -178,6 +179,7 @@ function attachConnection(ctx) {
178
179
  }
179
180
  var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
180
181
  sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked, vendors: yoke.VENDOR_REGISTRY });
182
+ if (typeof sendCursorSharingState === "function") sendCursorSharingState(ws);
181
183
  // Update notifications are pushed on a scheduled interval (see
182
184
  // scheduleUpdateBroadcast). We no longer push on connect to avoid
183
185
  // re-triggering the banner on every page refresh.
@@ -15,6 +15,7 @@ function attachDelegatedSession(ctx) {
15
15
  sessionVisibility: "private",
16
16
  vendor: selection.vendor,
17
17
  model: selection.model,
18
+ effort: selection.effort || null,
18
19
  });
19
20
  session.title = metadata.title;
20
21
  session.assignment = {