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,86 @@
1
+ // Shared, transactional control for Clay-managed per-session Skip Permissions.
2
+
3
+ function createFullAccessService(ctx) {
4
+ var sm = ctx.sm;
5
+ var transitions = new Map();
6
+
7
+ function serialize(session, action) {
8
+ var prior = transitions.get(session.localId) || Promise.resolve();
9
+ var next = prior.catch(function () {}).then(action);
10
+ transitions.set(session.localId, next);
11
+ return next.finally(function () {
12
+ if (transitions.get(session.localId) === next) transitions.delete(session.localId);
13
+ });
14
+ }
15
+
16
+ function snapshot(session) {
17
+ return {
18
+ permissionMode: session.permissionMode || null,
19
+ permissionModeBeforeFullAccess: session.permissionModeBeforeFullAccess || null,
20
+ enabled: session.permissionMode === "bypassPermissions",
21
+ };
22
+ }
23
+
24
+ function notify(session) {
25
+ if (typeof sm.sendToSession === "function") sm.sendToSession(session, { type: "session_full_access_changed", id: session.localId,
26
+ enabled: session.permissionMode === "bypassPermissions", permissionMode: session.permissionMode });
27
+ }
28
+
29
+ async function applyNativeMode(session, enabled) {
30
+ if (!session.queryInstance || typeof session.queryInstance.setPermissionMode !== "function") return;
31
+ // Clay's common permission callback remains the approval authority.
32
+ await session.queryInstance.setPermissionMode(enabled ? "default" : session.permissionMode);
33
+ }
34
+
35
+ async function applyEnabled(session, enabled) {
36
+ var previous = snapshot(session);
37
+ if (previous.enabled === !!enabled) return previous;
38
+ if (enabled) {
39
+ session.permissionModeBeforeFullAccess = session.permissionMode || sm.currentPermissionMode || "default";
40
+ session.permissionMode = "bypassPermissions";
41
+ } else {
42
+ session.permissionMode = session.permissionModeBeforeFullAccess || "default";
43
+ session.permissionModeBeforeFullAccess = null;
44
+ }
45
+ try {
46
+ await applyNativeMode(session, !!enabled);
47
+ sm.saveSessionFile(session);
48
+ sm.broadcastSessionList();
49
+ notify(session);
50
+ return previous;
51
+ } catch (error) {
52
+ session.permissionMode = previous.permissionMode;
53
+ session.permissionModeBeforeFullAccess = previous.permissionModeBeforeFullAccess;
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ function setEnabled(session, enabled) {
59
+ return serialize(session, function () { return applyEnabled(session, enabled); });
60
+ }
61
+
62
+ async function applyRestore(session, prior) {
63
+ if (!prior) return;
64
+ var current = snapshot(session);
65
+ session.permissionMode = prior.permissionMode;
66
+ session.permissionModeBeforeFullAccess = prior.permissionModeBeforeFullAccess;
67
+ try {
68
+ await applyNativeMode(session, prior.enabled);
69
+ sm.saveSessionFile(session);
70
+ sm.broadcastSessionList();
71
+ notify(session);
72
+ } catch (error) {
73
+ session.permissionMode = current.permissionMode;
74
+ session.permissionModeBeforeFullAccess = current.permissionModeBeforeFullAccess;
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ function restore(session, prior) {
80
+ return serialize(session, function () { return applyRestore(session, prior); });
81
+ }
82
+
83
+ return { restore: restore, setEnabled: setEnabled, snapshot: snapshot };
84
+ }
85
+
86
+ module.exports = { createFullAccessService: createFullAccessService };
@@ -898,7 +898,9 @@ function attachHTTP(ctx) {
898
898
  }
899
899
  var sessionId = Number(body.sessionId);
900
900
  if (!Number.isInteger(sessionId)) sessionId = null;
901
- var handler = getMcpBridgeHandler(sessionId, body.sessionOnly === true);
901
+ var queryGeneration = Number(body.queryGeneration);
902
+ if (!Number.isInteger(queryGeneration)) queryGeneration = null;
903
+ var handler = getMcpBridgeHandler(sessionId, body.sessionOnly === true, queryGeneration);
902
904
  if (!handler) {
903
905
  res.writeHead(500, { "Content-Type": "application/json" });
904
906
  res.end('{"error":"MCP bridge handler unavailable"}');
@@ -0,0 +1,154 @@
1
+ // Creates one visible Driver session for an exact Issue revision.
2
+
3
+ var yoke = require("./yoke");
4
+ var provenance = require("./session-provenance");
5
+
6
+ function attachIssueLaunch(ctx) {
7
+ var requests = new Map();
8
+ var operations = new Map();
9
+
10
+ function osUsersEnabled() {
11
+ return typeof ctx.osUsers === "function" ? ctx.osUsers() === true : ctx.osUsers === true;
12
+ }
13
+
14
+ function requestFingerprint(args) {
15
+ return JSON.stringify({ ref: args.ref, expectedRevision: args.expectedRevision });
16
+ }
17
+
18
+ function trimRequests() {
19
+ if (requests.size <= 200) return;
20
+ for (var pair of requests) {
21
+ if (pair[1].settled) { requests.delete(pair[0]); return; }
22
+ }
23
+ }
24
+
25
+ function record(session, event) {
26
+ if (ctx.sm && typeof ctx.sm.sendAndRecord === "function") ctx.sm.sendAndRecord(session, event);
27
+ else {
28
+ session.history = session.history || [];
29
+ session.history.push(event);
30
+ if (ctx.sm && typeof ctx.sm.appendToSessionFile === "function") ctx.sm.appendToSessionFile(session, event);
31
+ }
32
+ }
33
+
34
+ function sendStatus(session, status) {
35
+ if (ctx.sm && typeof ctx.sm.sendToSession === "function") ctx.sm.sendToSession(session, { type: "status", status: status });
36
+ }
37
+
38
+ function lastLaunchError(session, fromIndex) {
39
+ var history = session.history || [];
40
+ for (var i = history.length - 1; i >= fromIndex; i--) {
41
+ if (history[i] && history[i].type === "error" && history[i].text) return String(history[i].text);
42
+ }
43
+ return "The Driver session could not start its agent runtime.";
44
+ }
45
+
46
+ function failLaunch(session, fromIndex, error) {
47
+ var message = error && error.message ? error.message : lastLaunchError(session, fromIndex);
48
+ if (lastLaunchError(session, fromIndex) === "The Driver session could not start its agent runtime.") {
49
+ record(session, { type: "error", text: "Could not start issue work: " + message, issueRef: session.issueOrigin && session.issueOrigin.ref });
50
+ }
51
+ session.isProcessing = false;
52
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
53
+ sendStatus(session, "idle");
54
+ var launchError = new Error(message || "The Driver session could not start its agent runtime.");
55
+ launchError.sessionId = session.localId;
56
+ return launchError;
57
+ }
58
+
59
+ async function run(ws, actor, actorId, args, bound) {
60
+ var entry = bound.readIssue({ ref: args.ref });
61
+ if (entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict. Reload before starting work.");
62
+ if (entry.status === "resolved" || entry.status === "closed") throw new Error("Reopen this issue before starting work.");
63
+ if ((entry.linkedWorkSessions || []).length >= 40) throw new Error("This issue has reached its work-session limit.");
64
+
65
+ var expectedLinuxUser = typeof ctx.getLinuxUserForSession === "function" ? ctx.getLinuxUserForSession({ ownerId: actorId }) : null;
66
+ if (osUsersEnabled() && !expectedLinuxUser) throw new Error("An OS identity is required to start work.");
67
+ var runtime = await ctx.resolveDefaultAi(ws);
68
+ if (ws.readyState !== 1 || ws._clayUser !== actor || actorId !== (ws._clayUser && ws._clayUser.id || null)) {
69
+ throw new Error("The requesting connection changed.");
70
+ }
71
+ if (typeof ctx.canStartWork === "function" && ctx.canStartWork(ws, actor) !== true) throw new Error("Issue work is no longer allowed from this connection.");
72
+ entry = bound.readIssue({ ref: args.ref });
73
+ if (entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict. Reload before starting work.");
74
+ if (entry.status === "resolved" || entry.status === "closed") throw new Error("Reopen this issue before starting work.");
75
+ if (!runtime || !runtime.ready || !ctx.sm || (ctx.sm.installedVendors || []).indexOf(runtime.vendor) < 0) {
76
+ throw new Error(runtime && runtime.error || "Default AI is unavailable.");
77
+ }
78
+ if (yoke.sessionTools.capability(runtime.vendor).kind === "none") throw new Error("The selected provider does not support Issues tools.");
79
+ var liveLinuxUser = typeof ctx.getLinuxUserForSession === "function" ? ctx.getLinuxUserForSession({ ownerId: actorId }) : null;
80
+ if (osUsersEnabled() && liveLinuxUser !== expectedLinuxUser) throw new Error("The OS identity changed while issue work was being prepared.");
81
+
82
+ var session = ctx.sm.createSessionRaw({ vendor: runtime.vendor, model: runtime.model, effort: runtime.effort || null,
83
+ mode: "gui", ownerId: actor && actor.id || null, sessionVisibility: "private" });
84
+ provenance.ensureOrigin(session);
85
+ if (session.hidden === true || session.delegated === true || provenance.isWorker(session)) {
86
+ ctx.sm.deleteSession(session.localId);
87
+ throw new Error("A visible Driver session is required to start issue work.");
88
+ }
89
+ var linuxUser = typeof ctx.ensureProjectAccessForSession === "function"
90
+ ? ctx.ensureProjectAccessForSession(session) : expectedLinuxUser;
91
+ if (osUsersEnabled() && (!linuxUser || linuxUser !== expectedLinuxUser)) {
92
+ ctx.sm.deleteSession(session.localId);
93
+ throw new Error("The OS identity changed before issue work could start.");
94
+ }
95
+ session.issueOrigin = { ref: entry.ref, revision: args.expectedRevision, requestId: args.requestId, sessionOriginId: session.sessionOriginId };
96
+ try { bound.linkWorkSession(args, session); }
97
+ catch (error) { ctx.sm.deleteSession(session.localId); throw error; }
98
+
99
+ session.title = "Issue · " + entry.title;
100
+ session.titleManuallySet = true;
101
+ ctx.sm.saveSessionFile(session);
102
+ var prompt = "Work on project issue " + entry.ref + ". Read it with read_issue, inspect the repository, implement the requested outcome and verify it. Update this same issue as work progresses. Do not commit, publish, or create a PR unless explicitly authorized. If implementation is verified but no commit is authorized, keep it in_progress and record that state. Resolve only with verified commit evidence. Session completion never resolves the issue automatically.\n\nRequested issue:\n" + JSON.stringify({ title: entry.title, summary: entry.summary, body: entry.body });
103
+ ctx.sm.switchSession(session.localId, ws);
104
+ ws._clayActiveSession = session.localId;
105
+ record(session, { type: "user_message", text: prompt, issueRef: entry.ref, issueRevision: args.expectedRevision });
106
+ session.isProcessing = true;
107
+ sendStatus(session, "processing");
108
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
109
+ ctx.sm.broadcastSessionList();
110
+ var historyStart = (session.history || []).length;
111
+ var sdk = ctx.getSdk && ctx.getSdk();
112
+ if (!sdk || typeof sdk.startQuery !== "function") throw failLaunch(session, historyStart, new Error("The SDK bridge is not ready."));
113
+ try { await sdk.startQuery(session, prompt, undefined, linuxUser); }
114
+ catch (error) { throw failLaunch(session, historyStart, error); }
115
+ if (ctx.sm.sessions.get(session.localId) !== session || !session.queryInstance) {
116
+ throw failLaunch(session, historyStart, null);
117
+ }
118
+ return { sessionId: session.localId, sessionOriginId: session.sessionOriginId, ref: entry.ref };
119
+ }
120
+
121
+ return function startWork(ws, msg, bound) {
122
+ var args = Object.assign({}, msg && msg.args || {}, { requestId: msg && msg.requestId });
123
+ var actor = ws && ws._clayUser || null;
124
+ var actorId = actor && actor.id || null;
125
+ if (!msg || typeof msg.requestId !== "string" || !msg.requestId || msg.requestId.length > 200) {
126
+ return Promise.reject(new Error("A work request identifier is required."));
127
+ }
128
+ if (!bound || typeof args.ref !== "string" || !Number.isInteger(args.expectedRevision)) {
129
+ return Promise.reject(new Error("An exact issue reference and expectedRevision are required."));
130
+ }
131
+ var fingerprint = requestFingerprint(args);
132
+ var requestKey = JSON.stringify([bound.scopeId, actorId, msg.requestId]);
133
+ var prior = requests.get(requestKey);
134
+ if (prior) {
135
+ if (prior.ws !== ws || prior.fingerprint !== fingerprint) return Promise.reject(new Error("Work request identifier was already used with different arguments."));
136
+ return prior.task;
137
+ }
138
+ var operationKey = JSON.stringify([bound.scopeId, actorId, args.ref, args.expectedRevision]);
139
+ var operation = operations.get(operationKey);
140
+ var task = operation ? operation.task : run(ws, actor, actorId, args, bound);
141
+ if (!operation) {
142
+ operations.set(operationKey, { task: task });
143
+ task.then(function () { if (operations.get(operationKey).task === task) operations.delete(operationKey); },
144
+ function () { if (operations.get(operationKey).task === task) operations.delete(operationKey); });
145
+ }
146
+ var request = { ws: ws, fingerprint: fingerprint, task: task, settled: false };
147
+ requests.set(requestKey, request);
148
+ task.then(function () { request.settled = true; trimRequests(); }, function () { request.settled = true; trimRequests(); });
149
+ trimRequests();
150
+ return task;
151
+ };
152
+ }
153
+
154
+ module.exports = { attachIssueLaunch: attachIssueLaunch };
@@ -0,0 +1,58 @@
1
+ var mcp = require("./issues-mcp-server");
2
+ var resolveIssueWorkSession = require("./issue-work-session").resolveIssueWorkSession;
3
+ function attachProjectIssues(ctx) {
4
+ function user(ws) {
5
+ return !ctx.isMate && ctx.service && ctx.service.bindUser({ projectSlug: ctx.projectSlug, userId: ws._clayUser && ws._clayUser.id });
6
+ }
7
+ function notify(entry, type) {
8
+ for (var ws of ctx.getClients()) {
9
+ if (!ws || ws.readyState !== 1 || ws._clayPane) continue;
10
+ try { var bound = user(ws); if (bound) { if (entry.deleted) bound.readIssueTombstone({ ref: entry.ref }); else bound.readIssue({ ref: entry.ref }); ctx.sendTo(ws, Object.assign({ type: type || "issue_updated", projectSlug: ctx.projectSlug, ref: entry.ref, status: entry.status, title: entry.title, revision: entry.revision }, type === "issue_deleted" ? { deleted: true } : {})); } } catch (error) { /* Access may have changed. */ }
11
+ }
12
+ return entry;
13
+ }
14
+ function sessionBound(session) {
15
+ var bound = !ctx.isMate && ctx.service && session && ctx.service.bindProjectSession({ projectSlug: ctx.projectSlug, session: session });
16
+ if (!bound) return null;
17
+ ["createIssue", "updateIssue", "reviewIssueComment"].forEach(function (method) {
18
+ var original = bound[method];
19
+ bound[method] = async function (args) { var result = await original(args); if (method === "reviewIssueComment") return notify(result, "issue_commented"); return notify(result); };
20
+ });
21
+ return bound;
22
+ }
23
+ function defs(session) { return ctx.isMate || !ctx.service || (session && !sessionBound(session)) ? [] : mcp.getToolDefs(sessionBound(session)); }
24
+ function handleMessage(ws, msg) {
25
+ var methods = { issues_list: "listIssues", issue_read: "readIssue", issue_create: "createIssue", issue_update: "updateIssue", issue_history: "issueHistory", issue_revision: "readIssueRevision", issue_comment: "commentIssue", issue_delete: "removeIssue" };
26
+ if (!methods[msg.type] && msg.type !== "issue_start_work" && msg.type !== "issue_open_work") return false;
27
+ Promise.resolve().then(async function () {
28
+ var bound = user(ws);
29
+ if (!bound) throw new Error("Issues are unavailable for this project.");
30
+ var result;
31
+ if (msg.type === "issue_open_work") {
32
+ var session = resolveIssueWorkSession(bound, msg.args || {});
33
+ ctx.sm.switchSession(session.localId, ws); ws._clayActiveSession = session.localId;
34
+ result = { sessionId: session.localId };
35
+ } else if (msg.type === "issue_start_work") { result = await ctx.startWork(ws, msg, bound); notify(bound.readIssue({ ref: msg.args.ref })); }
36
+ else {
37
+ result = await bound[methods[msg.type]](msg.args || {});
38
+ if (msg.type === "issue_create" || msg.type === "issue_update") notify(result);
39
+ else if (msg.type === "issue_comment") notify(result, "issue_commented");
40
+ else if (msg.type === "issue_delete") notify(result, "issue_deleted");
41
+ }
42
+ ctx.sendTo(ws, { type: "issues_result", requestId: msg.requestId, action: msg.type, result: result, canDelete: bound.canDelete === true });
43
+ }).catch(function (error) {
44
+ var result = error && error.sessionId ? { sessionId: error.sessionId } : undefined;
45
+ ctx.sendTo(ws, { type: "issues_result", requestId: msg.requestId, action: msg.type, error: error.message, result: result });
46
+ });
47
+ return true;
48
+ }
49
+ return {
50
+ handleMessage: handleMessage, notify: notify,
51
+ getDynamicToolDefs: function (session) { return defs(session).map(function (tool) { tool.permissionName = "mcp__clay-issues__" + tool.name; return tool; }); },
52
+ createMcpServer: function (adapter, session) { return ctx.isMate || !ctx.service || !adapter.createToolServer ? null : adapter.createToolServer({ name: "clay-issues", version: "1.0.0", tools: defs(session) }); },
53
+ getBridgeTools: function (session, normalize) { return defs(session).map(function (tool) { return { server: "clay-issues", name: tool.name, description: tool.description, inputSchema: normalize(tool.inputSchema) }; }); },
54
+ callBridgeTool: function (session, name, args) { var tool = defs(session).find(function (item) { return item.name === name; }); if (!tool || !session) return Promise.reject(new Error("Issue tool unavailable.")); return tool.handler(args); },
55
+ getSystemPrompt: function (session) { return sessionBound(session) ? "--- Project Issues ---\n" + mcp.CONTRACT : ""; },
56
+ };
57
+ }
58
+ module.exports = { attachProjectIssues: attachProjectIssues };
@@ -52,21 +52,17 @@ var LEARNING_CONTRACT =
52
52
  // history or loses the history when the board is cleared.
53
53
  //
54
54
  // This is guidance for the Driver, not an automatic mirror: notes are also
55
- // written by people and by Mates that hold no Logs authority, so nothing here
56
- // makes creating a note mutate the ledger on its own.
55
+ // written by people and by Mates that hold no Issues or Logs authority, so
56
+ // nothing here makes creating a note mutate either canonical record.
57
57
  var ATTENTION_CONTRACT =
58
58
  "Sticky Notes and Project Logs are two different layers and must not be confused. " +
59
59
  "A Sticky Note is the transient attention layer: an unresolved, actionable commitment or defect that stays on the active board only while it still needs action, and is closed once it no longer does. Closing is reversible and never deletes the note. " +
60
- "A Project Log is the durable project-scoped ledger: discovery, evidence, impact, decision, remediation, and outcome stay permanently, versioned, long after the note has been closed. " +
61
- "When you discover a concrete defect in this project that will remain unresolved past the current work, and you therefore write a Sticky Note about it, also record it in the ledger. The note is the alert; the log entry is the record. " +
62
- "Do both in this order when practical: create or update the log entry first, then write the note and include the entry's opaque log: reference in the note text, so the alert points at the durable record. " +
63
- "A log entry may mention that an attention note exists, but must never depend on a note id for its identity, because the note is a transient alert and the ledger must stand on its own. " +
64
- "If the ledger already has an entry for this defect, revise that entry instead of creating a second one. " +
65
- "Categorise it as defect, security, or incident, or another category this project already uses when one fits better, and set priority separately from category. " +
66
- "When the defect is fixed, revise that same entry with the remediation, how it was verified, and the outcome as a new canonical revision, and only then close the Sticky Note. Close it, never delete it: the note leaves the active board and the entry stays permanent. " +
67
- "If you find and fully fix a defect inside the current task, do not open a Sticky Note for it at all, and write a log entry only when the discovery itself has durable value for the project. " +
68
- "This pairing applies to concrete unresolved defects, not to everything on the board. Never mirror speculation, general cleanup ideas, transient blockers, proposals, or ordinary notes into the ledger. " +
69
- "Notes written by people or by other sessions are not yours to mirror; judge only what you discovered yourself.";
60
+ "Project Issues are the primary record for concrete defects. For a Project Driver session with Issues authority, search, reuse, or create the Issue first, recording observable evidence, affected component, impact, and next action there. Then write only a concise Sticky Note title, one-line actionable cue, and the actual opaque issue: reference; never duplicate the full defect details in the note. " +
61
+ "If you lack Issues authority, route the defect through an eligible Project Driver. Never invent an issue reference, drop the alert, mirror storage automatically, or expand privileges. " +
62
+ "Project Logs remain required for concise work continuity, decisions, and results. Reference the issue in the Log instead of duplicating its full defect report. Logs are durable and versioned; the issue keeps the defect lifecycle and the note remains the transient alert. " +
63
+ "If the Issue already exists, revise that Issue instead of creating a second one. When the defect is fixed, update the Issue with remediation and verification while respecting its real commit-evidence and status rules; only then close a Sticky Note created by this same session. Close it, never delete it. " +
64
+ "If you find and fully fix a defect inside the current task, do not open a Sticky Note for it at all, and keep the required concise Log only when the work instruction or result belongs in the project record. " +
65
+ "This Issue-plus-note flow applies to concrete unresolved defects, not to ordinary notes or proposals. Notes written by people or by other sessions are not yours to mirror; judge authority and evidence rather than mutating records automatically.";
70
66
 
71
67
  var REVIEW_CONTRACT =
72
68
  "People cannot edit the ledger, so a comment is a proposal or a piece of evidence and never an automatic change. Judge each one against the project itself. " +
@@ -0,0 +1,163 @@
1
+ var crypto = require("crypto");
2
+ var driverEligibility = require("./session-driver-eligibility");
3
+ var loopGuidance = require("./loop-guidance");
4
+
5
+ var MAX_OBJECTIVE = 20000;
6
+ var MAX_CRITERIA = 10;
7
+ var MAX_CRITERION = 1000;
8
+
9
+ function text(value, max) { return typeof value === "string" ? value.trim().substring(0, max) : ""; }
10
+ function criteria(value) { return (Array.isArray(value) ? value : []).slice(0, MAX_CRITERIA).map(function (item) { return text(item, MAX_CRITERION); }).filter(Boolean); }
11
+ function activeInterviewId(session) {
12
+ var history = Array.isArray(session && session.history) ? session.history : [];
13
+ var current = null;
14
+ for (var i = 0; i < history.length; i++) {
15
+ var entry = history[i];
16
+ if (entry && entry.source === "loop_interview") current = entry.interviewId || null;
17
+ if (entry && (entry.source === "loop_interview_cancel" || entry.source === "loop_interview_close") && entry.interviewId === current) current = null;
18
+ }
19
+ return current;
20
+ }
21
+ function brief(value) {
22
+ if (!value || typeof value !== "object") return null;
23
+ var objective = text(value.objective, MAX_OBJECTIVE);
24
+ var successCriteria = criteria(value.successCriteria);
25
+ if (!objective || successCriteria.length === 0) return null;
26
+ return { version: 1, id: text(value.id, 200) || crypto.randomUUID(), objective: objective,
27
+ successCriteria: successCriteria, maxContinuations: Math.max(1, Math.min(100, Math.floor(Number(value.maxContinuations) || 10))),
28
+ maxMinutes: Math.max(1, Math.min(1440, Math.floor(Number(value.maxMinutes) || 60))), createdAt: Number(value.createdAt) || Date.now() };
29
+ }
30
+
31
+ function attachLoopInterview(ctx) {
32
+ var sm = ctx.sm;
33
+ var sendTo = ctx.sendTo;
34
+ var autonomousRun = ctx.autonomousRun;
35
+ function eligible(session) { return !!(session && !ctx.isMate && (session.runtimeMode || session.mode) === "gui" && driverEligibility.isEligibleDriverSession(session) && !ctx.isDriverOperatedSession(session)); }
36
+ function runActive(session) { return !!(session && session.autonomousRun && ["armed", "running", "reviewing", "waiting-worker", "waiting-user", "paused"].indexOf(session.autonomousRun.state) !== -1); }
37
+ function hasInterview(session, requestId) {
38
+ var history = Array.isArray(session && session.history) ? session.history : [];
39
+ for (var i = history.length - 1; i >= 0; i--) if (history[i] && history[i].source === "loop_interview" && history[i].requestId === requestId) return true;
40
+ return false;
41
+ }
42
+ function currentInterview(session) {
43
+ return activeInterviewId(session);
44
+ }
45
+ function interviewActive(session) {
46
+ return !!currentInterview(session);
47
+ }
48
+ function startInterview(ws, msg) {
49
+ var session = ctx.getSessionForWs(ws);
50
+ var requestId = text(msg.requestId, 200);
51
+ var result = { type: "loop_interview_start_result", sessionId: session && session.localId || msg.sessionId || null, requestId: requestId, ok: false };
52
+ if (!session || !requestId || Number(msg.sessionId) !== Number(session.localId) || (ctx.canAccess && !ctx.canAccess(ws, session))) result.error = "This session is no longer available.";
53
+ else if (hasInterview(session, requestId)) { result.ok = true; result.duplicate = true; }
54
+ else if (!eligible(session)) result.error = "Loop interviews are available only in a Driver project chat.";
55
+ else if (runActive(session)) result.error = "Finish the active Loop run before starting another interview.";
56
+ else if (session.isProcessing || session._queryStarting) result.error = "Wait for the current task to finish before starting a Loop interview.";
57
+ else {
58
+ session.loopInterviewBrief = null;
59
+ var prompt = loopGuidance.interviewPrompt();
60
+ var interviewId = crypto.randomUUID();
61
+ var entry = { type: "user_message", text: "[Loop interview started]", source: "loop_interview", interviewId: interviewId, requestId: requestId, _ts: Date.now() };
62
+ session.history.push(entry);
63
+ sm.appendToSessionFile(session, entry);
64
+ sm.saveSessionFile(session);
65
+ publish(session);
66
+ ctx.sendToSession(session.localId, entry);
67
+ session.isProcessing = true;
68
+ ctx.onProcessingChanged();
69
+ ctx.sendToSession(session.localId, { type: "status", status: "processing" });
70
+ try {
71
+ var started = ctx.sdk.startQuery(session, prompt, null, ctx.ensureProjectAccessForSession(session));
72
+ ctx.sendTo(ws, Object.assign({}, result, { ok: true }));
73
+ Promise.resolve(started).catch(function (error) {
74
+ var stillCurrent = ctx.getSessionForWs(ws) === session && currentInterview(session) === interviewId && hasInterview(session, requestId);
75
+ if (!stillCurrent) return;
76
+ session.isProcessing = false;
77
+ ctx.onProcessingChanged();
78
+ ctx.sendToSession(session.localId, { type: "status", status: "idle" });
79
+ sm.saveSessionFile(session);
80
+ ctx.sendTo(ws, Object.assign({}, result, { ok: false, error: "Loop interview could not start: " + (error.message || error) }));
81
+ });
82
+ return true;
83
+ } catch (error) {
84
+ session.isProcessing = false;
85
+ ctx.onProcessingChanged();
86
+ ctx.sendToSession(session.localId, { type: "status", status: "idle" });
87
+ sm.saveSessionFile(session);
88
+ result.error = "Loop interview could not start: " + (error.message || error);
89
+ }
90
+ }
91
+ ctx.sendTo(ws, result);
92
+ return true;
93
+ }
94
+ function publish(session) { sm.sendToSession(session, { type: "loop_interview_brief", sessionId: session.localId, brief: session.loopInterviewBrief || null }); }
95
+ function propose(session, interviewId, args) {
96
+ if (!eligible(session) || runActive(session) || !interviewId || currentInterview(session) !== interviewId) return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: "propose_loop is stale or unavailable outside the current Driver Loop interview." }) }] });
97
+ var next = brief(args);
98
+ if (!next) return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: "Provide an objective and at least one success criterion." }) }] });
99
+ next.interviewId = interviewId;
100
+ session.loopInterviewBrief = next;
101
+ sm.saveSessionFile(session);
102
+ publish(session);
103
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ status: "proposed", brief: next }) }] });
104
+ }
105
+ function getToolDefs(session) {
106
+ if (!eligible(session) || runActive(session) || !interviewActive(session) || session.loopInterviewBrief) return [];
107
+ var interviewId = currentInterview(session);
108
+ return [{ name: "propose_loop", description: "Draft a bounded Loop execution brief for human review. This never starts execution or changes permissions.", inputSchema: { type: "object", properties: {
109
+ objective: { type: "string" }, successCriteria: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 10 },
110
+ maxContinuations: { type: "integer", minimum: 1, maximum: 100 }, maxMinutes: { type: "integer", minimum: 1, maximum: 1440 }
111
+ }, required: ["objective", "successCriteria"], additionalProperties: false }, handler: function (args) { return propose(session, interviewId, args || {}); } }];
112
+ }
113
+ function handleMessage(ws, msg) {
114
+ if (msg.type === "loop_interview_state") {
115
+ var stateSession = ctx.getSessionForWs(ws);
116
+ if (stateSession && Number(msg.sessionId) === Number(stateSession.localId) && (!ctx.canAccess || ctx.canAccess(ws, stateSession))) publish(stateSession);
117
+ return true;
118
+ }
119
+ if (msg.type === "loop_interview_start") return startInterview(ws, msg);
120
+ if (["loop_interview_start_run", "loop_interview_edit", "loop_interview_cancel"].indexOf(msg.type) === -1) return false;
121
+ var session = ctx.getSessionForWs(ws);
122
+ if (!session || Number(msg.sessionId) !== Number(session.localId) || (ctx.canAccess && !ctx.canAccess(ws, session)) || !eligible(session)) {
123
+ if (msg.type === "loop_interview_start_run") sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session && session.localId || msg.sessionId || null, requestId: text(msg.requestId, 200), error: "This session is no longer available." });
124
+ return true;
125
+ }
126
+ if (msg.type === "loop_interview_cancel") {
127
+ if (session.loopInterviewBrief && (text(msg.proposalId, 200) !== session.loopInterviewBrief.id || Number(msg.version) !== Number(session.loopInterviewBrief.version))) return true;
128
+ var cancelEntry = { type: "loop_interview_cancel", source: "loop_interview_cancel", interviewId: currentInterview(session), _ts: Date.now() };
129
+ session.history.push(cancelEntry); sm.appendToSessionFile(session, cancelEntry);
130
+ session.loopInterviewBrief = null; sm.saveSessionFile(session); publish(session); return true;
131
+ }
132
+ if (msg.type === "loop_interview_edit") {
133
+ if (session.loopInterviewBrief && text(msg.proposalId, 200) === session.loopInterviewBrief.id && Number(msg.version) === Number(session.loopInterviewBrief.version) && session.loopInterviewBrief.interviewId === currentInterview(session)) {
134
+ var edited = brief(Object.assign({}, session.loopInterviewBrief, msg.brief || {}));
135
+ if (edited) { edited.interviewId = session.loopInterviewBrief.interviewId; edited.version = session.loopInterviewBrief.version + 1; session.loopInterviewBrief = edited; sm.saveSessionFile(session); publish(session); }
136
+ }
137
+ return true;
138
+ }
139
+ var proposal = session.loopInterviewBrief;
140
+ if (!proposal || proposal.interviewId !== currentInterview(session) || text(msg.proposalId, 200) !== proposal.id || Number(msg.version) !== Number(proposal.version)) {
141
+ sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session.localId, requestId: text(msg.requestId, 200), error: "This Loop brief is stale or no longer pending." });
142
+ return true;
143
+ }
144
+ autonomousRun.startApprovedBrief(ws, session, proposal, text(msg.requestId, 200), function () {
145
+ return !!(session.loopInterviewBrief && session.loopInterviewBrief.id === proposal.id && Number(session.loopInterviewBrief.version) === Number(proposal.version) && session.loopInterviewBrief.interviewId === currentInterview(session));
146
+ }).then(function (ok) {
147
+ if (ok && session.loopInterviewBrief && session.loopInterviewBrief.id === proposal.id) {
148
+ var closeEntry = { type: "loop_interview_close", source: "loop_interview_close", interviewId: proposal.interviewId, proposalId: proposal.id, proposalVersion: proposal.version, _ts: Date.now() };
149
+ session.history.push(closeEntry);
150
+ sm.appendToSessionFile(session, closeEntry);
151
+ session.loopInterviewBrief = null;
152
+ sm.saveSessionFile(session);
153
+ publish(session);
154
+ }
155
+ }).catch(function (error) {
156
+ sendTo(ws, { type: "loop_interview_start_run_result", ok: false, sessionId: session.localId, requestId: text(msg.requestId, 200), error: error.message || String(error) });
157
+ });
158
+ return true;
159
+ }
160
+ return { getToolDefs: getToolDefs, handleMessage: handleMessage };
161
+ }
162
+
163
+ module.exports = { attachLoopInterview: attachLoopInterview, brief: brief, activeInterviewId: activeInterviewId };