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.
- package/LICENSE +661 -21
- package/LICENSES/MIT-legacy.txt +21 -0
- package/LICENSES/README.md +7 -0
- package/README.md +2 -2
- package/bin/cli.js +0 -2
- package/lib/autonomous-run-record.js +32 -0
- package/lib/autonomous-run-scheduler.js +92 -0
- package/lib/daemon.js +0 -2
- package/lib/default-ai-runtime.js +117 -0
- package/lib/durable-scheduler-record.js +61 -0
- package/lib/durable-scheduler-store.js +185 -0
- package/lib/durable-scheduler.js +495 -0
- package/lib/git-cli.js +1 -0
- package/lib/issue-work-session.js +10 -0
- package/lib/issues-comments.js +40 -0
- package/lib/issues-commit-verification.js +30 -0
- package/lib/issues-mcp-server.js +27 -0
- package/lib/issues-schema.js +119 -0
- package/lib/issues-service.js +339 -0
- package/lib/issues-store.js +210 -0
- package/lib/loop-cron.js +47 -0
- package/lib/loop-durable-schedule.js +159 -0
- package/lib/loop-guidance.js +35 -0
- package/lib/loop-registry-occurrence.js +76 -0
- package/lib/model-entry.js +15 -0
- package/lib/project-autonomous-run.js +498 -0
- package/lib/project-capsule-turn.js +1 -0
- package/lib/project-connection.js +6 -1
- package/lib/project-delegated-session.js +1 -0
- package/lib/project-filesystem.js +10 -10
- package/lib/project-full-access.js +86 -0
- package/lib/project-http.js +3 -1
- package/lib/project-issue-launch.js +154 -0
- package/lib/project-issues.js +58 -0
- package/lib/project-logs-mcp-server.js +8 -12
- package/lib/project-loop-interview.js +163 -0
- package/lib/project-loop.js +150 -64
- package/lib/project-message-delivery.js +4 -2
- package/lib/project-models.js +71 -3
- package/lib/project-pair-autonomous-stop.js +16 -0
- package/lib/project-pair-owned-stop.js +18 -0
- package/lib/project-pair-task-control.js +14 -1
- package/lib/project-pending-message-queue.js +264 -0
- package/lib/project-scheduled-messages.js +316 -0
- package/lib/project-scheduled-task-execution.js +236 -0
- package/lib/project-scheduled-tasks.js +481 -0
- package/lib/project-session-pair.js +16 -19
- package/lib/project-sessions.js +114 -31
- package/lib/project-tool-llm-runtime.js +48 -0
- package/lib/project-user-message.js +202 -60
- package/lib/project-workspace-query.js +25 -0
- package/lib/project.js +361 -69
- package/lib/public/app.js +85 -22
- package/lib/public/css/default-ai.css +33 -0
- package/lib/public/css/home-hub.css +15 -2
- package/lib/public/css/home-sidebar.css +50 -2
- package/lib/public/css/input.css +160 -5
- package/lib/public/css/issues.css +101 -0
- package/lib/public/css/messages.css +19 -0
- package/lib/public/css/pane.css +58 -7
- package/lib/public/css/pending-message-queue.css +187 -0
- package/lib/public/css/rewind.css +11 -2
- package/lib/public/css/scheduled-tasks.css +122 -0
- package/lib/public/index.html +42 -150
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-cursors.js +29 -12
- package/lib/public/modules/app-loop-ui.js +4 -8
- package/lib/public/modules/app-message-router.js +8 -0
- package/lib/public/modules/app-messages.js +34 -9
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rate-limit.js +8 -5
- package/lib/public/modules/autonomous-run.js +285 -0
- package/lib/public/modules/clay-file-links.js +111 -0
- package/lib/public/modules/clay-issue-links.js +131 -0
- package/lib/public/modules/clay-log-links.js +2 -0
- package/lib/public/modules/cursor-sharing-preference.js +38 -0
- package/lib/public/modules/default-ai.js +377 -0
- package/lib/public/modules/filebrowser.js +43 -2
- package/lib/public/modules/home-sidebar.js +16 -8
- package/lib/public/modules/home-surface.js +5 -0
- package/lib/public/modules/input.js +11 -20
- package/lib/public/modules/issues-context.js +117 -0
- package/lib/public/modules/issues-render.js +29 -0
- package/lib/public/modules/issues.js +270 -0
- package/lib/public/modules/loop-interview.js +151 -0
- package/lib/public/modules/markdown.js +4 -0
- package/lib/public/modules/message-delivery.js +5 -3
- package/lib/public/modules/pending-message-image-editor.js +143 -0
- package/lib/public/modules/pending-message-queue-model.js +22 -0
- package/lib/public/modules/pending-message-queue.js +449 -0
- package/lib/public/modules/project-logs.js +2 -2
- package/lib/public/modules/scheduled-message-state.js +46 -0
- package/lib/public/modules/scheduled-task-edit-state.js +7 -0
- package/lib/public/modules/scheduled-task-engine.js +48 -0
- package/lib/public/modules/scheduled-task-form.js +122 -0
- package/lib/public/modules/scheduled-task-recurrence.js +46 -0
- package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
- package/lib/public/modules/scheduled-tasks.js +459 -0
- package/lib/public/modules/scheduler-config.js +6 -52
- package/lib/public/modules/scheduler.js +9 -21
- package/lib/public/modules/sidebar-mobile.js +3 -0
- package/lib/public/modules/sidebar.js +0 -11
- package/lib/public/modules/split-view.js +2 -0
- package/lib/public/modules/sticky-note-markdown.js +1 -0
- package/lib/public/modules/terminal.js +2 -0
- package/lib/public/modules/tool-palette-order.js +3 -3
- package/lib/public/modules/tools.js +5 -2
- package/lib/public/style.css +3 -0
- package/lib/record-revision.js +11 -0
- package/lib/schedule-rule.js +151 -0
- package/lib/schedule-validation.js +124 -0
- package/lib/scheduled-run-state.js +87 -0
- package/lib/scheduled-task-records.js +127 -0
- package/lib/scheduler.js +179 -196
- package/lib/sdk-bridge.js +157 -22
- package/lib/sdk-message-processor.js +3 -0
- package/lib/server-cursor-sharing.js +65 -0
- package/lib/server-default-ai.js +265 -0
- package/lib/server-durable-scheduler.js +26 -0
- package/lib/server-home-chat.js +29 -9
- package/lib/server-home-clay-entry.js +4 -1
- package/lib/server-home-debate-planning.js +32 -14
- package/lib/server-home-mate-creation.js +30 -11
- package/lib/server-home-models.js +14 -6
- package/lib/server-issue-links.js +18 -0
- package/lib/server.js +63 -0
- package/lib/session-notes-mcp-server.js +1 -1
- package/lib/session-pair-factory.js +42 -4
- package/lib/session-pair-query-start.js +22 -0
- package/lib/session-query-tool-bridge.js +52 -0
- package/lib/session-search.js +4 -1
- package/lib/session-split-groups.js +35 -1
- package/lib/sessions.js +51 -3
- package/lib/tool-llm.js +1 -0
- package/lib/users-default-ai-preferences.js +80 -0
- package/lib/users-home-surface-preferences.js +2 -1
- package/lib/users-preferences.js +35 -0
- package/lib/users-scheduled-task-preferences.js +44 -0
- package/lib/users.js +16 -0
- package/lib/workspace-query-projection.js +29 -0
- package/lib/workspace-query-service.js +19 -29
- package/lib/ws-schema.js +61 -1
- package/lib/yoke/adapters/antigravity.js +1 -0
- package/lib/yoke/adapters/claude-worker.js +11 -6
- package/lib/yoke/adapters/claude.js +35 -1
- package/lib/yoke/adapters/codex.js +3 -0
- package/lib/yoke/claude-user-input.js +2 -0
- package/lib/yoke/index.js +2 -0
- package/lib/yoke/mcp-bridge-server.js +7 -1
- package/lib/yoke/package.json +1 -1
- package/lib/yoke/session-tool-transport.js +53 -0
- package/lib/yoke/user-input.js +21 -7
- package/package.json +5 -6
- package/bin/claude-relay.js +0 -6
- 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 };
|
package/lib/project-http.js
CHANGED
|
@@ -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
|
|
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
|
|
56
|
-
// makes creating a note mutate
|
|
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
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"If the
|
|
65
|
-
"
|
|
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 };
|