clay-server 4.0.1-beta.1 → 4.1.0-beta.2
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/lib/daemon-project-access.js +118 -0
- package/lib/daemon-projects.js +19 -5
- package/lib/daemon.js +19 -18
- package/lib/project-logs-access-scope.js +94 -0
- package/lib/project-logs-query.js +3 -0
- package/lib/project-logs-service.js +86 -84
- package/lib/project-logs-store.js +5 -3
- package/lib/project-pair-lifecycle.js +140 -102
- package/lib/project-pair-message.js +50 -0
- package/lib/project-pair-replacement-state.js +72 -0
- package/lib/project-pair-task-control.js +264 -0
- package/lib/project-pair-usage.js +76 -0
- package/lib/project-session-pair.js +115 -116
- package/lib/project-sessions.js +25 -1
- package/lib/project-worker-proposal.js +69 -58
- package/lib/project.js +21 -4
- package/lib/public/css/icon-strip.css +40 -0
- package/lib/public/modules/app-messages.js +9 -0
- package/lib/public/modules/project-access.js +141 -0
- package/lib/public/modules/project-grouping.js +22 -0
- package/lib/public/modules/sidebar-projects.js +14 -171
- package/lib/public/modules/worker-proposal.js +4 -0
- package/lib/sdk-bridge.js +20 -0
- package/lib/server-admin.js +3 -3
- package/lib/server-global-ws.js +10 -0
- package/lib/server-mates.js +4 -3
- package/lib/server-palette.js +17 -14
- package/lib/server.js +184 -102
- package/lib/session-pair-factory.js +3 -0
- package/lib/session-pair-history.js +40 -0
- package/lib/session-pair-mcp-server.js +19 -2
- package/lib/session-pair-operation.js +11 -0
- package/lib/session-pair-prompts.js +24 -2
- package/lib/session-pair-turn-control.js +8 -3
- package/lib/session-provenance.js +1 -0
- package/lib/session-spawn-mcp-server.js +2 -1
- package/lib/sessions.js +2 -0
- package/lib/worker-proposal-control.js +69 -0
- package/lib/worker-proposal-decision.js +7 -0
- package/lib/worker-runtime-catalog.js +52 -0
- package/lib/workspace-query-access.js +3 -2
- package/lib/workspace-query-service.js +1 -1
- package/lib/worktree.js +8 -4
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/adapters/codex.js +25 -7
- package/package.json +1 -1
|
@@ -3,10 +3,21 @@ var yoke = require("./yoke");
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
4
|
var driverEligibility = require("./session-driver-eligibility");
|
|
5
5
|
var driverOrchestration = require("./session-driver-orchestration");
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var MAX_TASK_CHARS = 30000;
|
|
9
|
-
var
|
|
6
|
+
var proposalControl = require("./worker-proposal-control");
|
|
7
|
+
var runtimeCatalog = require("./worker-runtime-catalog");
|
|
8
|
+
var MAX_SUMMARY_CHARS = 600, MAX_PLAN_CHARS = 6000, MAX_TASK_CHARS = 30000, MAX_RATIONALE_CHARS = 1000;
|
|
9
|
+
var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
|
|
10
|
+
function normalizeEvaluation(raw) {
|
|
11
|
+
if (raw == null) return null;
|
|
12
|
+
var input = typeof raw === "string" ? { outcome: raw } : raw;
|
|
13
|
+
if (!input || typeof input !== "object") return { error: "evaluation must be an outcome string or object" };
|
|
14
|
+
var outcome = typeof input.outcome === "string" ? input.outcome.trim().toLowerCase() : "";
|
|
15
|
+
if (EVALUATION_OUTCOMES.indexOf(outcome) === -1) {
|
|
16
|
+
return { error: "evaluation outcome must be one of: " + EVALUATION_OUTCOMES.join(", ") };
|
|
17
|
+
}
|
|
18
|
+
var note = typeof input.note === "string" ? input.note.replace(/\s+/g, " ").trim().slice(0, 400) : "";
|
|
19
|
+
return { value: { outcome: outcome, note: note } };
|
|
20
|
+
}
|
|
10
21
|
function modelValue(entry) {
|
|
11
22
|
if (typeof entry === "string") return entry;
|
|
12
23
|
return entry && (entry.value || entry.id) || "";
|
|
@@ -15,16 +26,14 @@ function modelLabel(entry) {
|
|
|
15
26
|
if (typeof entry === "string") return entry;
|
|
16
27
|
return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
|
|
17
28
|
}
|
|
18
|
-
function toolResult(value) {
|
|
19
|
-
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
20
|
-
}
|
|
29
|
+
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
|
|
21
30
|
function attachWorkerProposal(ctx) {
|
|
22
31
|
var sm = ctx.sm;
|
|
23
32
|
var store = ctx.splitStore;
|
|
33
|
+
var catalogErrors = {};
|
|
24
34
|
function isLiveSession(session) {
|
|
25
35
|
return !!session && sm.sessions.get(session.localId) === session;
|
|
26
36
|
}
|
|
27
|
-
|
|
28
37
|
function canOffer(session) { return !ctx.isMate && isLiveSession(session) && driverEligibility.isEligibleDriverSession(session, sm); }
|
|
29
38
|
function isEligible(session) {
|
|
30
39
|
return canOffer(session) && !store.groupForMember(session.localId);
|
|
@@ -45,7 +54,6 @@ function attachWorkerProposal(ctx) {
|
|
|
45
54
|
}
|
|
46
55
|
return result;
|
|
47
56
|
}
|
|
48
|
-
|
|
49
57
|
function proposalOptions() {
|
|
50
58
|
var installed = (sm.installedVendors || []).slice();
|
|
51
59
|
var capabilities = {};
|
|
@@ -59,9 +67,10 @@ function attachWorkerProposal(ctx) {
|
|
|
59
67
|
installedVendors: installed,
|
|
60
68
|
modelsByVendor: safeModelsByVendor(installed),
|
|
61
69
|
capabilitiesByVendor: capabilities,
|
|
70
|
+
unavailableReasons: Object.assign({}, catalogErrors),
|
|
71
|
+
availableVendors: Array.isArray(sm.availableVendors) ? sm.availableVendors.slice() : null,
|
|
62
72
|
};
|
|
63
73
|
}
|
|
64
|
-
|
|
65
74
|
async function ensureModelCatalogs() {
|
|
66
75
|
var installed = sm.installedVendors || [];
|
|
67
76
|
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
@@ -70,11 +79,10 @@ function attachWorkerProposal(ctx) {
|
|
|
70
79
|
if (sm.modelsByVendor[vendor] && sm.modelsByVendor[vendor].length > 0) continue;
|
|
71
80
|
var adapter = ctx.adapters && ctx.adapters[vendor];
|
|
72
81
|
if (!adapter || typeof adapter.supportedModels !== "function") continue;
|
|
73
|
-
try { sm.modelsByVendor[vendor] = await adapter.supportedModels(); }
|
|
74
|
-
catch (err) { console.warn("[worker-proposal] Could not load " + vendor + " models:",
|
|
82
|
+
try { sm.modelsByVendor[vendor] = await adapter.supportedModels(); delete catalogErrors[vendor]; }
|
|
83
|
+
catch (err) { catalogErrors[vendor] = err.message || String(err); console.warn("[worker-proposal] Could not load " + vendor + " models:", catalogErrors[vendor]); }
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
|
-
|
|
78
86
|
function modelIsAvailable(options, vendor, model) {
|
|
79
87
|
if (!model) return true;
|
|
80
88
|
var models = options.modelsByVendor[vendor] || [];
|
|
@@ -83,7 +91,6 @@ function attachWorkerProposal(ctx) {
|
|
|
83
91
|
}
|
|
84
92
|
return false;
|
|
85
93
|
}
|
|
86
|
-
|
|
87
94
|
function effortIsAvailable(options, vendor, model, effort) {
|
|
88
95
|
var capabilities = options.capabilitiesByVendor[vendor] || {};
|
|
89
96
|
if (capabilities.effort === false) return !effort;
|
|
@@ -97,7 +104,6 @@ function attachWorkerProposal(ctx) {
|
|
|
97
104
|
}
|
|
98
105
|
return true;
|
|
99
106
|
}
|
|
100
|
-
|
|
101
107
|
function chooseRecommendation(args, session, options) {
|
|
102
108
|
var installed = options.installedVendors;
|
|
103
109
|
var vendor = args.recommendedVendor;
|
|
@@ -125,12 +131,10 @@ function attachWorkerProposal(ctx) {
|
|
|
125
131
|
var effort = yoke.clampEffort(vendor, args.recommendedEffort || "medium") || "";
|
|
126
132
|
return { vendor: vendor, model: model, effort: effort };
|
|
127
133
|
}
|
|
128
|
-
|
|
129
134
|
function skipPermissionsEnabled(session) {
|
|
130
135
|
return !!(session && (session.permissionMode === "bypassPermissions" ||
|
|
131
136
|
session.dangerouslySkipPermissions || ctx.dangerouslySkipPermissions));
|
|
132
137
|
}
|
|
133
|
-
|
|
134
138
|
function recommendationCanAutoAccept(args, recommendation, options) {
|
|
135
139
|
var requestedVendor = typeof args.recommendedVendor === "string" ? args.recommendedVendor.trim() : "";
|
|
136
140
|
var requestedModel = typeof args.recommendedModel === "string" ? args.recommendedModel.trim() : "";
|
|
@@ -143,32 +147,12 @@ function attachWorkerProposal(ctx) {
|
|
|
143
147
|
return requestedEffort === recommendation.effort &&
|
|
144
148
|
effortIsAvailable(options, requestedVendor, requestedModel, requestedEffort);
|
|
145
149
|
}
|
|
146
|
-
|
|
147
150
|
function autoAcceptanceWs(session) {
|
|
148
151
|
return {
|
|
149
152
|
_clayActiveSession: session.localId,
|
|
150
153
|
_clayUser: session.ownerId ? { id: session.ownerId } : null,
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
|
-
|
|
154
|
-
function findProposal(session, proposalId) {
|
|
155
|
-
var history = (session && session.history) || [];
|
|
156
|
-
for (var i = history.length - 1; i >= 0; i--) {
|
|
157
|
-
if (history[i] && history[i].type === "worker_proposal" && history[i].proposalId === proposalId) return history[i];
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function hasPendingProposal(session) {
|
|
163
|
-
var history = (session && session.history) || [];
|
|
164
|
-
for (var i = history.length - 1; i >= 0; i--) {
|
|
165
|
-
var item = history[i];
|
|
166
|
-
if (!item || item.type !== "worker_proposal") continue;
|
|
167
|
-
return item.status === "pending" || item.status === "starting" || item.status === "running";
|
|
168
|
-
}
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
156
|
function updateProposal(session, proposal, patch) {
|
|
173
157
|
Object.assign(proposal, patch, { updatedAt: Date.now() });
|
|
174
158
|
sm.saveSessionFile(session);
|
|
@@ -177,10 +161,13 @@ function attachWorkerProposal(ctx) {
|
|
|
177
161
|
proposalId: proposal.proposalId,
|
|
178
162
|
}, patch));
|
|
179
163
|
}
|
|
180
|
-
|
|
181
164
|
async function propose(args, session) {
|
|
182
165
|
if (!isEligible(session)) return toolResult({ error: "Split Worker proposals are only available in an eligible unpaired Driver session." });
|
|
183
|
-
|
|
166
|
+
var superseded = proposalControl.pendingProposal(session);
|
|
167
|
+
var supersedeId = typeof args.supersedeProposalId === "string" ? args.supersedeProposalId.trim() : "";
|
|
168
|
+
if (superseded && superseded.proposalId !== supersedeId) return toolResult({ error: "A Split Worker suggestion is already awaiting a decision. Inspect it or supply its exact id to supersede it." });
|
|
169
|
+
if (!superseded && supersedeId) return toolResult({ error: "The Split Worker suggestion to supersede is no longer pending." });
|
|
170
|
+
if (superseded && superseded.status !== "pending") return toolResult({ error: "The Split Worker suggestion is already starting and cannot be superseded." });
|
|
184
171
|
var summary = typeof args.summary === "string" ? args.summary.trim() : "";
|
|
185
172
|
var plan = typeof args.plan === "string" ? args.plan.trim() : "";
|
|
186
173
|
var task = typeof args.message === "string" ? args.message.trim() : "";
|
|
@@ -193,6 +180,8 @@ function attachWorkerProposal(ctx) {
|
|
|
193
180
|
if (!isEligible(session)) {
|
|
194
181
|
return toolResult({ error: "The Driver session changed while Split Worker runtimes were loading." });
|
|
195
182
|
}
|
|
183
|
+
var currentPending = proposalControl.pendingProposal(session);
|
|
184
|
+
if (currentPending !== superseded) return toolResult({ error: "The pending Split Worker suggestion changed while runtimes were loading." });
|
|
196
185
|
var options = proposalOptions();
|
|
197
186
|
if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Split Worker session." });
|
|
198
187
|
var recommendation = chooseRecommendation(args, session, options);
|
|
@@ -203,12 +192,14 @@ function attachWorkerProposal(ctx) {
|
|
|
203
192
|
plan: plan.slice(0, MAX_PLAN_CHARS),
|
|
204
193
|
message: task,
|
|
205
194
|
status: "pending",
|
|
195
|
+
createdAt: Date.now(),
|
|
206
196
|
recommendedVendor: recommendation.vendor,
|
|
207
197
|
recommendedModel: recommendation.model,
|
|
208
198
|
recommendedEffort: recommendation.effort,
|
|
209
199
|
recommendationRationale: rationale.slice(0, MAX_RATIONALE_CHARS),
|
|
210
200
|
options: options,
|
|
211
201
|
};
|
|
202
|
+
if (superseded) updateProposal(session, superseded, { status: "superseded", supersededBy: proposal.proposalId });
|
|
212
203
|
sm.sendAndRecord(session, proposal);
|
|
213
204
|
if (skipPermissionsEnabled(session) && recommendationCanAutoAccept(args, recommendation, options)) {
|
|
214
205
|
var accepted = await acceptProposal(session, proposal, {
|
|
@@ -230,7 +221,6 @@ function attachWorkerProposal(ctx) {
|
|
|
230
221
|
instruction: "The Split Worker suggestion is visible in the chat. End this turn now and wait for the user's decision.",
|
|
231
222
|
});
|
|
232
223
|
}
|
|
233
|
-
|
|
234
224
|
async function proposeReplacement(args, session) {
|
|
235
225
|
var group = session && store.groupForMember(session.localId);
|
|
236
226
|
if (ctx.isMate || !isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) || !group ||
|
|
@@ -239,12 +229,18 @@ function attachWorkerProposal(ctx) {
|
|
|
239
229
|
}
|
|
240
230
|
var sourceGroupId = group.id;
|
|
241
231
|
var sourceWorkerId = group.pair.workerId;
|
|
242
|
-
|
|
232
|
+
var superseded = proposalControl.pendingProposal(session);
|
|
233
|
+
var supersedeId = typeof args.supersedeProposalId === "string" ? args.supersedeProposalId.trim() : "";
|
|
234
|
+
if (superseded && superseded.proposalId !== supersedeId) return toolResult({ error: "A Split Worker proposal is already awaiting a decision. Inspect it or supply its exact id to supersede it." });
|
|
235
|
+
if (!superseded && supersedeId) return toolResult({ error: "The Split Worker proposal to supersede is no longer pending." });
|
|
236
|
+
if (superseded && superseded.status !== "pending") return toolResult({ error: "The Split Worker proposal is already starting and cannot be superseded." });
|
|
243
237
|
var task = typeof args.message === "string" ? args.message.trim() : "";
|
|
244
238
|
var rationale = typeof args.recommendationRationale === "string" ? args.recommendationRationale.trim() : "";
|
|
245
239
|
if (!task) return toolResult({ error: "message is required so an accepted replacement delegates exactly once." });
|
|
246
240
|
if (!rationale) return toolResult({ error: "recommendationRationale is required for the replacement audit trail." });
|
|
247
241
|
if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Split Worker task is too long." });
|
|
242
|
+
var normalizedEvaluation = normalizeEvaluation(args.evaluation);
|
|
243
|
+
if (normalizedEvaluation && normalizedEvaluation.error) return toolResult({ error: normalizedEvaluation.error });
|
|
248
244
|
await ensureModelCatalogs();
|
|
249
245
|
var liveGroup = store.groupForMember(session.localId);
|
|
250
246
|
if (!isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) ||
|
|
@@ -252,6 +248,8 @@ function attachWorkerProposal(ctx) {
|
|
|
252
248
|
liveGroup.pair.driverId !== session.localId || liveGroup.pair.workerId !== sourceWorkerId) {
|
|
253
249
|
return toolResult({ error: "The Driver/Split Worker pair changed while replacement runtimes were loading." });
|
|
254
250
|
}
|
|
251
|
+
var currentPending = proposalControl.pendingProposal(session);
|
|
252
|
+
if (currentPending !== superseded) return toolResult({ error: "The pending Split Worker proposal changed while runtimes were loading." });
|
|
255
253
|
var options = proposalOptions();
|
|
256
254
|
if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Split Worker session." });
|
|
257
255
|
var recommendationArgs = {
|
|
@@ -268,6 +266,7 @@ function attachWorkerProposal(ctx) {
|
|
|
268
266
|
plan: "1. Preserve the current Worker's session and history\n2. Create the selected replacement runtime\n3. Delegate the next task exactly once",
|
|
269
267
|
message: task,
|
|
270
268
|
status: "pending",
|
|
269
|
+
createdAt: Date.now(),
|
|
271
270
|
recommendedVendor: recommendation.vendor,
|
|
272
271
|
recommendedModel: recommendation.model,
|
|
273
272
|
recommendedEffort: recommendation.effort,
|
|
@@ -276,8 +275,10 @@ function attachWorkerProposal(ctx) {
|
|
|
276
275
|
sourceGroupId: sourceGroupId,
|
|
277
276
|
sourceWorkerId: sourceWorkerId,
|
|
278
277
|
interrupt: args.interrupt === true,
|
|
279
|
-
evaluation:
|
|
278
|
+
evaluation: normalizedEvaluation ? normalizedEvaluation.value : null,
|
|
279
|
+
transactionId: "replace_" + crypto.randomUUID(),
|
|
280
280
|
};
|
|
281
|
+
if (superseded) updateProposal(session, superseded, { status: "superseded", supersededBy: proposal.proposalId });
|
|
281
282
|
sm.sendAndRecord(session, proposal);
|
|
282
283
|
if (skipPermissionsEnabled(session) && recommendationCanAutoAccept(recommendationArgs, recommendation, options)) {
|
|
283
284
|
var accepted = await acceptProposal(session, proposal, {
|
|
@@ -299,7 +300,6 @@ function attachWorkerProposal(ctx) {
|
|
|
299
300
|
instruction: "The replacement configuration is visible in the chat. End this turn now and wait for the user's decision.",
|
|
300
301
|
});
|
|
301
302
|
}
|
|
302
|
-
|
|
303
303
|
function resumeDriver(session, text) {
|
|
304
304
|
var sdk = ctx.getSdk();
|
|
305
305
|
if (!sdk) return Promise.reject(new Error("SDK bridge is not ready"));
|
|
@@ -335,7 +335,7 @@ function attachWorkerProposal(ctx) {
|
|
|
335
335
|
if (result.status === "complete") {
|
|
336
336
|
followup = "[Split Worker execution completed]\nReview and verify the Split Worker's result. The Split Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Split Worker-owned files yourself. If that Split Worker is no longer available, keep the work in the visible Split Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background Sub-agent.\n\n" + (result.response || "The Split Worker completed without a text summary.");
|
|
337
337
|
} else if (result.status === "interrupted") {
|
|
338
|
-
followup = "[Split Worker execution interrupted]\nThe
|
|
338
|
+
followup = "[Split Worker execution interrupted]\nThe Split Worker stopped mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Check partner_status before deciding next steps; this result alone does not identify who interrupted it. If the human stopped it, do not retry until a new human Driver message. Otherwise continue within the user's authorized task.";
|
|
339
339
|
} else if (result.status === "running") {
|
|
340
340
|
followup = "[Split Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
|
|
341
341
|
} else {
|
|
@@ -398,6 +398,7 @@ function attachWorkerProposal(ctx) {
|
|
|
398
398
|
workerModel: model,
|
|
399
399
|
workerEffort: effort,
|
|
400
400
|
evaluation: proposal.evaluation || undefined,
|
|
401
|
+
transactionId: proposal.transactionId,
|
|
401
402
|
}, session);
|
|
402
403
|
liveGroup = store.groupForMember(session.localId);
|
|
403
404
|
updateProposal(session, proposal, { status: "running", groupId: liveGroup.id, workerId: replaced.workerSessionId });
|
|
@@ -426,6 +427,7 @@ function attachWorkerProposal(ctx) {
|
|
|
426
427
|
restoredGroup.pair.workerId === proposal.sourceWorkerId) {
|
|
427
428
|
proposal.sourceGroupId = restoredGroup.id;
|
|
428
429
|
}
|
|
430
|
+
proposal.transactionId = "replace_" + crypto.randomUUID();
|
|
429
431
|
}
|
|
430
432
|
updateProposal(session, proposal, {
|
|
431
433
|
status: "pending",
|
|
@@ -440,7 +442,7 @@ function attachWorkerProposal(ctx) {
|
|
|
440
442
|
|
|
441
443
|
async function respondToProposal(ws, msg) {
|
|
442
444
|
var session = sessionForResponse(ws);
|
|
443
|
-
var proposal = findProposal(session, msg.proposalId);
|
|
445
|
+
var proposal = proposalControl.findProposal(session, msg.proposalId);
|
|
444
446
|
if (!proposal) throw new Error("Split Worker suggestion not found");
|
|
445
447
|
if (proposal.status !== "pending") throw new Error("Split Worker suggestion has already been resolved");
|
|
446
448
|
if (!msg.accepted) {
|
|
@@ -464,8 +466,9 @@ function attachWorkerProposal(ctx) {
|
|
|
464
466
|
|
|
465
467
|
function getToolDefs(session, options) {
|
|
466
468
|
var persistent = !!(options && options.persistent);
|
|
469
|
+
var controlsOnly = !!(options && options.controlsOnly);
|
|
467
470
|
if (!isEligible(session) && !(persistent && canOffer(session))) return [];
|
|
468
|
-
|
|
471
|
+
var proposalTools = controlsOnly ? [] : [{
|
|
469
472
|
name: "propose_worker",
|
|
470
473
|
description: "Propose a visible Split Worker for implementation-heavy execution. This non-mutating tool only shows the user a card where they choose vendor, model, and effort. It never creates a session or delegates work until the user accepts.",
|
|
471
474
|
inputSchema: buildShape({
|
|
@@ -476,25 +479,33 @@ function attachWorkerProposal(ctx) {
|
|
|
476
479
|
recommendedModel: { type: "string", description: "Optional exact Split Worker model id. Omit when uncertain." },
|
|
477
480
|
recommendedEffort: { type: "string", description: "Optional reasoning effort: minimal, low, medium, high, xhigh, or max." },
|
|
478
481
|
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended vendor, model, and effort fit this exact task." },
|
|
482
|
+
supersedeProposalId: { type: "string", description: "Exact pending proposal id to resolve as superseded when posting this replacement suggestion." },
|
|
479
483
|
}, ["summary", "plan", "message", "recommendationRationale"]),
|
|
480
484
|
handler: function (args) { return propose(args || {}, session); },
|
|
481
485
|
}];
|
|
486
|
+
return proposalTools.concat(proposalControl.getToolDefs({
|
|
487
|
+
inspect: function () { return toolResult(isLiveSession(session) ? proposalControl.projection(proposalControl.pendingProposal(session)) : { status: "rejected", reason: "The Driver session is no longer live." }); },
|
|
488
|
+
cancel: function (args) {
|
|
489
|
+
if (!isLiveSession(session)) return toolResult({ status: "rejected", proposalId: args.proposalId || null, reason: "The Driver session is no longer live." });
|
|
490
|
+
var proposal = proposalControl.findProposal(session, args.proposalId);
|
|
491
|
+
if (!proposal || proposal.status !== "pending") return toolResult({ status: "rejected", proposalId: args.proposalId || null, reason: "The exact proposal is not pending." });
|
|
492
|
+
updateProposal(session, proposal, { status: "cancelled" });
|
|
493
|
+
return toolResult({ status: "cancelled", proposalId: proposal.proposalId });
|
|
494
|
+
},
|
|
495
|
+
catalog: function () { return ensureModelCatalogs().then(function () {
|
|
496
|
+
return toolResult(runtimeCatalog.build(proposalOptions(), skipPermissionsEnabled(session), function (vendor, model, effort) {
|
|
497
|
+
return ctx.preflightWorkerForDriver ? ctx.preflightWorkerForDriver(session, { workerVendor: vendor, workerModel: model, workerEffort: effort || "" }) : null;
|
|
498
|
+
}));
|
|
499
|
+
}); },
|
|
500
|
+
}));
|
|
482
501
|
}
|
|
483
502
|
|
|
484
503
|
function getSystemPrompt(session) {
|
|
485
504
|
if (!isEligible(session) || !driverOrchestration.isHighTierDriverSession(session, sm)) return "";
|
|
486
505
|
return "For implementation-heavy work, use propose_worker before substantial execution. Work is implementation-heavy when it spans multiple modules, crosses client/server/data boundaries, requires a migration, or contains independent investigation and execution. Do not skip delegation merely because you can implement it yourself; preserve the Driver for decomposition, runtime selection, judgment, review, and integration. The user's explicit request to work directly takes precedence, and work that must remain sequential in this session stays here. Recommend an exact available vendor, model, and effort, and give a concise recommendationRationale explaining why all three fit the task. Clay always records and shows the runtime configuration card. In full-access mode Clay may auto-accept that exact validated recommendation; otherwise it waits for the user's explicit choice. After posting, end the turn. If accepted, Clay creates the exact Driver/Split Worker pair and delegates the proposed task once. If declined, continue in this Driver session. Do not call send_to_partner while unpaired.";
|
|
487
506
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
getSystemPrompt: getSystemPrompt,
|
|
492
|
-
proposeReplacement: proposeReplacement,
|
|
493
|
-
handleMessage: handleMessage,
|
|
494
|
-
respondToProposal: respondToProposal,
|
|
495
|
-
};
|
|
507
|
+
return { getToolDefs: getToolDefs, getSystemPrompt: getSystemPrompt, proposeReplacement: proposeReplacement,
|
|
508
|
+
handleMessage: handleMessage, respondToProposal: respondToProposal,
|
|
509
|
+
proposalStatus: function (session) { return proposalControl.projection(proposalControl.pendingProposal(session)); } };
|
|
496
510
|
}
|
|
497
|
-
|
|
498
|
-
module.exports = {
|
|
499
|
-
attachWorkerProposal: attachWorkerProposal,
|
|
500
|
-
};
|
|
511
|
+
module.exports = { attachWorkerProposal: attachWorkerProposal };
|
package/lib/project.js
CHANGED
|
@@ -390,14 +390,31 @@ function createProjectContext(opts) {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
function send(obj) {
|
|
393
|
-
var data = JSON.stringify(obj);
|
|
394
393
|
for (var ws of clients) {
|
|
395
|
-
if (ws.readyState
|
|
394
|
+
if (ws.readyState !== 1) continue;
|
|
395
|
+
var payload = obj;
|
|
396
|
+
if (obj && obj.type === "info") {
|
|
397
|
+
var infoUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
398
|
+
var visibleProjects = getProjectList(infoUserId);
|
|
399
|
+
payload = Object.assign({}, obj, { projects: visibleProjects, projectCount: visibleProjects.length });
|
|
400
|
+
} else if (obj && obj.type === "loop_registry_updated") {
|
|
401
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
402
|
+
payload = Object.assign({}, obj, { records: getHubSchedules(userId) });
|
|
403
|
+
}
|
|
404
|
+
ws.send(JSON.stringify(payload));
|
|
396
405
|
}
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
function sendTo(ws, obj) {
|
|
400
|
-
if (ws.readyState
|
|
409
|
+
if (ws.readyState !== 1) return;
|
|
410
|
+
var payload = obj;
|
|
411
|
+
if (obj && (obj.type === "loop_registry_updated" || obj.type === "hub_schedules")) {
|
|
412
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
413
|
+
payload = Object.assign({}, obj);
|
|
414
|
+
if (obj.type === "loop_registry_updated") payload.records = getHubSchedules(userId);
|
|
415
|
+
else payload.schedules = getHubSchedules(userId);
|
|
416
|
+
}
|
|
417
|
+
ws.send(JSON.stringify(payload));
|
|
401
418
|
}
|
|
402
419
|
|
|
403
420
|
function sendToAdmins(obj) {
|
|
@@ -2256,7 +2273,7 @@ function createProjectContext(opts) {
|
|
|
2256
2273
|
var _mateId = path.basename(cwd);
|
|
2257
2274
|
var _mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
2258
2275
|
// Collect non-mate projects for project registry injection
|
|
2259
|
-
var _projectList = (getProjectList() || []).filter(function (p) { return !p.isMate; });
|
|
2276
|
+
var _projectList = (getProjectList(projectOwnerId) || []).filter(function (p) { return !p.isMate; });
|
|
2260
2277
|
var _enforceOpts = { ctx: _mateCtx, mateId: _mateId, projects: _projectList };
|
|
2261
2278
|
// Enforce all system sections atomically on startup (single read/write)
|
|
2262
2279
|
var _selfWrite = false; // suppress watcher when we wrote the file ourselves
|
|
@@ -593,6 +593,37 @@
|
|
|
593
593
|
line-height: 1;
|
|
594
594
|
}
|
|
595
595
|
.project-access-close:hover { color: var(--text); }
|
|
596
|
+
.project-access-scope {
|
|
597
|
+
display: flex;
|
|
598
|
+
gap: 10px;
|
|
599
|
+
margin: 12px 14px 4px;
|
|
600
|
+
padding: 10px;
|
|
601
|
+
color: var(--text);
|
|
602
|
+
background: rgba(var(--overlay-rgb), 0.045);
|
|
603
|
+
border: 1px solid var(--border);
|
|
604
|
+
border-radius: 9px;
|
|
605
|
+
}
|
|
606
|
+
.project-access-scope-icon {
|
|
607
|
+
display: flex;
|
|
608
|
+
align-items: center;
|
|
609
|
+
justify-content: center;
|
|
610
|
+
width: 28px;
|
|
611
|
+
height: 28px;
|
|
612
|
+
flex: 0 0 28px;
|
|
613
|
+
color: var(--accent, #6366f1);
|
|
614
|
+
background: var(--accent-8, rgba(99, 102, 241, 0.08));
|
|
615
|
+
border-radius: 7px;
|
|
616
|
+
}
|
|
617
|
+
.project-access-scope-icon .lucide { width: 15px; height: 15px; }
|
|
618
|
+
.project-access-scope strong,
|
|
619
|
+
.project-access-scope small { display: block; }
|
|
620
|
+
.project-access-scope strong { font-size: 12px; font-weight: 600; }
|
|
621
|
+
.project-access-scope small {
|
|
622
|
+
margin-top: 2px;
|
|
623
|
+
color: var(--text-secondary);
|
|
624
|
+
font-size: 10px;
|
|
625
|
+
line-height: 1.35;
|
|
626
|
+
}
|
|
596
627
|
.project-access-section {
|
|
597
628
|
padding: 10px 14px;
|
|
598
629
|
}
|
|
@@ -653,6 +684,15 @@
|
|
|
653
684
|
cursor: pointer;
|
|
654
685
|
}
|
|
655
686
|
.project-access-user-item:hover { color: var(--text); }
|
|
687
|
+
.project-access-user-item small {
|
|
688
|
+
margin-left: auto;
|
|
689
|
+
color: var(--text-tertiary);
|
|
690
|
+
font-size: 9px;
|
|
691
|
+
text-transform: uppercase;
|
|
692
|
+
letter-spacing: 0.04em;
|
|
693
|
+
}
|
|
694
|
+
.project-access-user-item.is-inherited { color: var(--text-secondary); cursor: default; }
|
|
695
|
+
.project-access-user-item.is-inherited input { cursor: default; }
|
|
656
696
|
.project-access-empty {
|
|
657
697
|
font-size: 12px;
|
|
658
698
|
color: var(--text-tertiary);
|
|
@@ -89,6 +89,10 @@ var inputEl = document.getElementById("input");
|
|
|
89
89
|
var connectOverlay = document.getElementById("connect-overlay");
|
|
90
90
|
|
|
91
91
|
export function processMessage(msg) {
|
|
92
|
+
if (msg.type === "access_revoked") {
|
|
93
|
+
window.location.assign("/");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
92
96
|
// Preserve original timestamp from history replay
|
|
93
97
|
store.set({ currentMsgTs: msg._ts || null });
|
|
94
98
|
var isMateDm = store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate;
|
|
@@ -723,6 +727,11 @@ export function processMessage(msg) {
|
|
|
723
727
|
syncPaneTitles();
|
|
724
728
|
break;
|
|
725
729
|
|
|
730
|
+
case "partner_task_completed":
|
|
731
|
+
// The result is resumed into the Driver transcript; this correlated
|
|
732
|
+
// event is available to clients that track task state separately.
|
|
733
|
+
break;
|
|
734
|
+
|
|
726
735
|
case "session_presence":
|
|
727
736
|
updateSessionPresence(msg.presence || {});
|
|
728
737
|
break;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
|
|
4
|
+
var projectAccessPopover = null;
|
|
5
|
+
|
|
6
|
+
function closeAccessOnOutside(e) {
|
|
7
|
+
if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function closeAccessOnEscape(e) {
|
|
11
|
+
if (e.key === "Escape") closeProjectAccessPopover();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function closeProjectAccessPopover() {
|
|
15
|
+
if (!projectAccessPopover) return;
|
|
16
|
+
projectAccessPopover.remove();
|
|
17
|
+
projectAccessPopover = null;
|
|
18
|
+
document.removeEventListener("click", closeAccessOnOutside);
|
|
19
|
+
document.removeEventListener("keydown", closeAccessOnEscape);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function positionPopover(popover, anchorEl) {
|
|
23
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
24
|
+
popover.style.position = "fixed";
|
|
25
|
+
popover.style.left = (rect.right + 8) + "px";
|
|
26
|
+
popover.style.top = rect.top + "px";
|
|
27
|
+
popover.style.zIndex = "9999";
|
|
28
|
+
var popRect = popover.getBoundingClientRect();
|
|
29
|
+
if (popRect.right > window.innerWidth - 8) popover.style.left = (rect.left - popRect.width - 8) + "px";
|
|
30
|
+
if (popRect.bottom > window.innerHeight - 8) popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function selectedExactUsers(popover) {
|
|
34
|
+
var selected = [];
|
|
35
|
+
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (checkbox) {
|
|
36
|
+
if (!checkbox.disabled || checkbox.dataset.explicit === "true") selected.push(checkbox.dataset.uid);
|
|
37
|
+
});
|
|
38
|
+
return selected;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderAccessPopover(popover, slug, access, allUsers) {
|
|
42
|
+
var visibility = access.visibility || "public";
|
|
43
|
+
var isWorktree = access.isWorktree === true;
|
|
44
|
+
var allowedUsers = isWorktree ? (access.worktreeAllowedUsers || []) : (access.allowedUsers || []);
|
|
45
|
+
var inheritedUsers = isWorktree ? (access.parentAllowedUsers || []) : [];
|
|
46
|
+
var ownerId = access.ownerId;
|
|
47
|
+
var selectableUsers = allUsers.filter(function (user) { return user.id !== ownerId; });
|
|
48
|
+
var html = '';
|
|
49
|
+
html += '<div class="project-access-header">';
|
|
50
|
+
html += '<span class="project-access-title">' + (isWorktree ? 'Worktree Access' : 'Project Access') + '</span>';
|
|
51
|
+
html += '<button class="project-access-close" aria-label="Close access settings">×</button>';
|
|
52
|
+
html += '</div>';
|
|
53
|
+
|
|
54
|
+
if (isWorktree) {
|
|
55
|
+
var scopeTitle = visibility === "public" ? "Public parent project" : "This worktree only";
|
|
56
|
+
var scopeText = visibility === "public"
|
|
57
|
+
? "Every authenticated user can access this worktree. Exact grants are retained if the parent becomes private."
|
|
58
|
+
: "Entire-project access is inherited automatically.";
|
|
59
|
+
html += '<div class="project-access-scope">';
|
|
60
|
+
html += '<span class="project-access-scope-icon">' + iconHtml(visibility === "public" ? "globe" : "git-branch") + '</span>';
|
|
61
|
+
html += '<span><strong>' + scopeTitle + '</strong><small>' + scopeText + '</small></span>';
|
|
62
|
+
html += '</div>';
|
|
63
|
+
} else {
|
|
64
|
+
html += '<div class="project-access-section">';
|
|
65
|
+
html += '<label class="project-access-label">Visibility</label>';
|
|
66
|
+
html += '<div class="project-access-vis-row">';
|
|
67
|
+
html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">' + iconHtml("lock") + ' Private</button>';
|
|
68
|
+
html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">' + iconHtml("globe") + ' Public</button>';
|
|
69
|
+
html += '</div></div>';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
html += '<div class="project-access-section project-access-users-section"' + (!isWorktree && visibility !== "private" ? ' style="display:none"' : '') + '>';
|
|
73
|
+
html += '<label class="project-access-label">' + (isWorktree ? 'People with exact access' : 'Allowed Users') + '</label>';
|
|
74
|
+
html += '<div class="project-access-user-list">';
|
|
75
|
+
for (var i = 0; i < selectableUsers.length; i++) {
|
|
76
|
+
var user = selectableUsers[i];
|
|
77
|
+
var explicit = allowedUsers.indexOf(user.id) !== -1;
|
|
78
|
+
var inherited = inheritedUsers.indexOf(user.id) !== -1;
|
|
79
|
+
html += '<label class="project-access-user-item' + (inherited ? ' is-inherited' : '') + '">';
|
|
80
|
+
html += '<input type="checkbox" data-uid="' + user.id + '" data-explicit="' + (explicit ? 'true' : 'false') + '"' + (explicit || inherited ? ' checked' : '') + (inherited ? ' disabled' : '') + '>';
|
|
81
|
+
html += '<span>' + escapeHtml(user.displayName || user.username || user.id) + '</span>';
|
|
82
|
+
if (inherited) html += '<small>Entire project</small>';
|
|
83
|
+
html += '</label>';
|
|
84
|
+
}
|
|
85
|
+
if (selectableUsers.length === 0) html += '<div class="project-access-empty">No other users</div>';
|
|
86
|
+
html += '</div></div>';
|
|
87
|
+
popover.innerHTML = html;
|
|
88
|
+
refreshIcons();
|
|
89
|
+
|
|
90
|
+
popover.querySelector(".project-access-close").addEventListener("click", closeProjectAccessPopover);
|
|
91
|
+
popover.querySelectorAll(".project-access-vis-btn").forEach(function (button) {
|
|
92
|
+
button.addEventListener("click", function () {
|
|
93
|
+
var visibilityValue = button.dataset.vis;
|
|
94
|
+
popover.querySelectorAll(".project-access-vis-btn").forEach(function (item) { item.classList.remove("active"); });
|
|
95
|
+
button.classList.add("active");
|
|
96
|
+
var usersSection = popover.querySelector(".project-access-users-section");
|
|
97
|
+
if (usersSection) usersSection.style.display = visibilityValue === "private" ? "" : "none";
|
|
98
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
|
|
99
|
+
method: "PUT",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ visibility: visibilityValue }),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (checkbox) {
|
|
106
|
+
checkbox.addEventListener("change", function () {
|
|
107
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
|
|
108
|
+
method: "PUT",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ allowedUsers: selectedExactUsers(popover) }),
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function showProjectAccessPopover(anchorEl, slug) {
|
|
117
|
+
closeProjectAccessPopover();
|
|
118
|
+
var popover = document.createElement("div");
|
|
119
|
+
popover.className = "project-access-popover";
|
|
120
|
+
popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
|
|
121
|
+
popover.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
122
|
+
document.body.appendChild(popover);
|
|
123
|
+
projectAccessPopover = popover;
|
|
124
|
+
requestAnimationFrame(function () { positionPopover(popover, anchorEl); });
|
|
125
|
+
setTimeout(function () {
|
|
126
|
+
document.addEventListener("click", closeAccessOnOutside);
|
|
127
|
+
document.addEventListener("keydown", closeAccessOnEscape);
|
|
128
|
+
}, 0);
|
|
129
|
+
Promise.all([
|
|
130
|
+
fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (response) { return response.json(); }),
|
|
131
|
+
fetch("/api/admin/users").then(function (response) { return response.json(); }),
|
|
132
|
+
]).then(function (results) {
|
|
133
|
+
if (results[0].error || results[1].error) {
|
|
134
|
+
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
renderAccessPopover(popover, slug, results[0], results[1].users || []);
|
|
138
|
+
}).catch(function () {
|
|
139
|
+
popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Groups visible parent projects with their visible worktrees. A worktree whose
|
|
2
|
+
// parent is not in the authorized projection remains a standalone item.
|
|
3
|
+
|
|
4
|
+
export function groupProjects(projects) {
|
|
5
|
+
var source = Array.isArray(projects) ? projects : [];
|
|
6
|
+
var parentSlugs = {};
|
|
7
|
+
var parents = [];
|
|
8
|
+
var wtByParent = {};
|
|
9
|
+
for (var i = 0; i < source.length; i++) {
|
|
10
|
+
if (!source[i].isWorktree) parentSlugs[source[i].slug] = true;
|
|
11
|
+
}
|
|
12
|
+
for (var j = 0; j < source.length; j++) {
|
|
13
|
+
var project = source[j];
|
|
14
|
+
if (project.isWorktree && project.parentSlug && parentSlugs[project.parentSlug]) {
|
|
15
|
+
if (!wtByParent[project.parentSlug]) wtByParent[project.parentSlug] = [];
|
|
16
|
+
wtByParent[project.parentSlug].push(project);
|
|
17
|
+
} else {
|
|
18
|
+
parents.push(project);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { parents: parents, wtByParent: wtByParent };
|
|
22
|
+
}
|