clay-server 2.22.3-beta.1 → 2.23.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/project.js CHANGED
@@ -1486,6 +1486,10 @@ function createProjectContext(opts) {
1486
1486
  handleDebateConcludeResponse(ws, msg);
1487
1487
  return;
1488
1488
  }
1489
+ if (msg.type === "debate_confirm_brief") {
1490
+ handleDebateConfirmBrief(ws);
1491
+ return;
1492
+ }
1489
1493
 
1490
1494
  // --- Knowledge file management ---
1491
1495
  if (msg.type === "knowledge_list") {
@@ -4234,6 +4238,9 @@ function createProjectContext(opts) {
4234
4238
 
4235
4239
  sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
4236
4240
 
4241
+ // Check if the mate wrote a debate brief during this turn
4242
+ checkForDmDebateBrief(session, msg.mateId, mateCtx);
4243
+
4237
4244
  // Generate session digest for mate's long-term memory
4238
4245
  digestMentionSession(session, msg.mateId, mateCtx, fullText, msg.text);
4239
4246
  },
@@ -4906,10 +4913,33 @@ function createProjectContext(opts) {
4906
4913
  var mateCtx = debate.mateCtx || matesModule.buildMateCtx(null);
4907
4914
  debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
4908
4915
 
4909
- console.log("[debate] Brief picked up, transitioning to live. Topic:", debate.topic);
4910
-
4911
- // Transition to live
4912
- startDebateLive(session);
4916
+ // If debate was started from DM (no setupSessionId), go to reviewing phase
4917
+ if (!debate.setupSessionId) {
4918
+ console.log("[debate] Brief picked up from DM, entering review phase. Topic:", debate.topic);
4919
+ debate.phase = "reviewing";
4920
+ persistDebateState(session);
4921
+
4922
+ var moderatorProfile = getMateProfile(mateCtx, debate.moderatorId);
4923
+ var briefReadyMsg = {
4924
+ type: "debate_brief_ready",
4925
+ debateId: debate.debateId,
4926
+ topic: debate.topic,
4927
+ format: debate.format || "free_discussion",
4928
+ context: debate.context || "",
4929
+ specialRequests: debate.specialRequests || null,
4930
+ moderatorId: debate.moderatorId,
4931
+ moderatorName: moderatorProfile.name,
4932
+ panelists: debate.panelists.map(function (p) {
4933
+ var prof = getMateProfile(mateCtx, p.mateId);
4934
+ return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
4935
+ }),
4936
+ };
4937
+ sendToSession(session.localId, briefReadyMsg);
4938
+ } else {
4939
+ console.log("[debate] Brief picked up, transitioning to live. Topic:", debate.topic);
4940
+ // Transition to live (standard flow via modal/skill)
4941
+ startDebateLive(session);
4942
+ }
4913
4943
  } catch (e) {
4914
4944
  // File not ready yet or invalid JSON, keep watching
4915
4945
  }
@@ -4946,7 +4976,7 @@ function createProjectContext(opts) {
4946
4976
  if (!session.debateState) return;
4947
4977
 
4948
4978
  var phase = session.debateState.phase;
4949
- if (phase !== "preparing" && phase !== "live") return;
4979
+ if (phase !== "preparing" && phase !== "reviewing" && phase !== "live") return;
4950
4980
 
4951
4981
  // Restore _debate from persisted state
4952
4982
  var debate = restoreDebateFromState(session);
@@ -4980,6 +5010,22 @@ function createProjectContext(opts) {
4980
5010
  return { mateId: p.mateId, name: prof.name };
4981
5011
  }),
4982
5012
  });
5013
+ } else if (phase === "reviewing") {
5014
+ console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
5015
+ sendTo(ws, {
5016
+ type: "debate_brief_ready",
5017
+ debateId: debate.debateId,
5018
+ topic: debate.topic,
5019
+ format: debate.format || "free_discussion",
5020
+ context: debate.context || "",
5021
+ specialRequests: debate.specialRequests || null,
5022
+ moderatorId: debate.moderatorId,
5023
+ moderatorName: moderatorProfile.name,
5024
+ panelists: debate.panelists.map(function (p) {
5025
+ var prof = getMateProfile(mateCtx, p.mateId);
5026
+ return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
5027
+ }),
5028
+ });
4983
5029
  } else if (phase === "live") {
4984
5030
  console.log("[debate] Restoring debate (live). topic:", debate.topic, "awaitingConclude:", debate.awaitingConcludeConfirm);
4985
5031
  // Debate was live when server restarted. It can't resume AI turns,
@@ -5097,6 +5143,87 @@ function createProjectContext(opts) {
5097
5143
  };
5098
5144
  }
5099
5145
 
5146
+ // Check if a mate wrote a debate brief during a DM mention turn
5147
+ function checkForDmDebateBrief(session, mateId, mateCtx) {
5148
+ // Skip if there's already an active debate on this session
5149
+ if (session._debate && (session._debate.phase === "preparing" || session._debate.phase === "reviewing" || session._debate.phase === "live")) return;
5150
+
5151
+ var debatesDir = path.join(cwd, ".clay", "debates");
5152
+ var dirs;
5153
+ try {
5154
+ dirs = fs.readdirSync(debatesDir);
5155
+ } catch (e) {
5156
+ return; // No debates directory
5157
+ }
5158
+
5159
+ for (var i = 0; i < dirs.length; i++) {
5160
+ var briefPath = path.join(debatesDir, dirs[i], "brief.json");
5161
+ var raw;
5162
+ try {
5163
+ raw = fs.readFileSync(briefPath, "utf8");
5164
+ } catch (e) {
5165
+ continue; // No brief.json in this dir
5166
+ }
5167
+
5168
+ var brief;
5169
+ try {
5170
+ brief = JSON.parse(raw);
5171
+ } catch (e) {
5172
+ continue; // Invalid JSON
5173
+ }
5174
+
5175
+ // Found a valid brief - create debate state
5176
+ var debateId = dirs[i];
5177
+ console.log("[debate] Found DM debate brief from mate " + mateId + ", debateId:", debateId);
5178
+
5179
+ // Clean up the brief file
5180
+ try { fs.unlinkSync(briefPath); } catch (e) {}
5181
+
5182
+ var debate = {
5183
+ phase: "reviewing",
5184
+ topic: brief.topic || "Untitled debate",
5185
+ format: brief.format || "free_discussion",
5186
+ context: brief.context || "",
5187
+ specialRequests: brief.specialRequests || null,
5188
+ moderatorId: mateId,
5189
+ panelists: (brief.panelists || []).map(function (p) {
5190
+ return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
5191
+ }),
5192
+ mateCtx: mateCtx,
5193
+ moderatorSession: null,
5194
+ panelistSessions: {},
5195
+ nameMap: null,
5196
+ turnInProgress: false,
5197
+ pendingComment: null,
5198
+ round: 1,
5199
+ history: [],
5200
+ setupSessionId: null,
5201
+ debateId: debateId,
5202
+ briefPath: briefPath,
5203
+ };
5204
+ debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
5205
+ session._debate = debate;
5206
+ persistDebateState(session);
5207
+
5208
+ var moderatorProfile = getMateProfile(mateCtx, mateId);
5209
+ sendToSession(session.localId, {
5210
+ type: "debate_brief_ready",
5211
+ debateId: debateId,
5212
+ topic: debate.topic,
5213
+ format: debate.format,
5214
+ context: debate.context,
5215
+ specialRequests: debate.specialRequests,
5216
+ moderatorId: mateId,
5217
+ moderatorName: moderatorProfile.name,
5218
+ panelists: debate.panelists.map(function (p) {
5219
+ var prof = getMateProfile(mateCtx, p.mateId);
5220
+ return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
5221
+ }),
5222
+ });
5223
+ return; // Only process first brief found
5224
+ }
5225
+ }
5226
+
5100
5227
  function handleDebateStart(ws, msg) {
5101
5228
  var session = getSessionForWs(ws);
5102
5229
  if (!session) return;
@@ -5142,8 +5269,152 @@ function createProjectContext(opts) {
5142
5269
  };
5143
5270
  session._debate = debate;
5144
5271
 
5145
- // Create a new session for the setup skill (like Ralph crafting)
5146
5272
  var debateId = "debate_" + Date.now();
5273
+ var debateDir = path.join(cwd, ".clay", "debates", debateId);
5274
+ try { fs.mkdirSync(debateDir, { recursive: true }); } catch (e) {}
5275
+ var briefPath = path.join(debateDir, "brief.json");
5276
+ console.log("[debate] cwd=" + cwd + " debateDir=" + debateDir + " briefPath=" + briefPath);
5277
+
5278
+ debate.debateId = debateId;
5279
+ debate.briefPath = briefPath;
5280
+
5281
+ if (msg.quickStart) {
5282
+ // --- Quick Start: moderator mate generates brief from DM context ---
5283
+ handleDebateQuickStart(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath);
5284
+ } else {
5285
+ // --- Standard: clay-debate-setup skill ---
5286
+ handleDebateSkillSetup(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath);
5287
+ }
5288
+ }
5289
+
5290
+ // Quick start: moderator mate uses DM conversation context to generate the debate brief directly
5291
+ function handleDebateQuickStart(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath) {
5292
+ var debateId = debate.debateId;
5293
+
5294
+ // Create setup session (still needed for session grouping)
5295
+ var setupSession = sm.createSession();
5296
+ setupSession.title = "Debate Setup: " + (msg.topic || "Quick").slice(0, 40);
5297
+ setupSession.debateSetupMode = true;
5298
+ setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: (msg.topic || "Quick").slice(0, 40), source: "debate", startedAt: Date.now() };
5299
+ sm.saveSessionFile(setupSession);
5300
+ sm.switchSession(setupSession.localId, null, hydrateImageRefs);
5301
+ debate.setupSessionId = setupSession.localId;
5302
+ debate.setupStartedAt = setupSession.loop.startedAt;
5303
+
5304
+ // Build DM conversation context for the moderator
5305
+ var dmContext = msg.dmContext || "";
5306
+
5307
+ // Build panelist info
5308
+ var panelistInfo = msg.panelists.map(function (p) {
5309
+ var prof = getMateProfile(mateCtx, p.mateId);
5310
+ return "- " + (prof.name || p.mateId) + " (ID: " + p.mateId + ", bio: " + (prof.bio || "none") + ")";
5311
+ }).join("\n");
5312
+
5313
+ var quickBriefPrompt = [
5314
+ "You are " + (moderatorProfile.name || "the moderator") + ". You were just having a DM conversation with the user, and they want to turn this into a structured debate.",
5315
+ "",
5316
+ "## Recent DM Conversation",
5317
+ dmContext,
5318
+ "",
5319
+ "## Topic Suggestion",
5320
+ msg.topic || "(Derive from conversation above)",
5321
+ "",
5322
+ "## Available Panelists",
5323
+ panelistInfo,
5324
+ "",
5325
+ "## Your Task",
5326
+ "Based on the conversation context, create a debate brief. You know the topic well because you were just discussing it.",
5327
+ "Assign each panelist a role and perspective that will create the most productive debate.",
5328
+ "",
5329
+ "Output ONLY a valid JSON object (no markdown fences, no extra text):",
5330
+ "{",
5331
+ ' "topic": "refined debate topic",',
5332
+ ' "format": "free_discussion",',
5333
+ ' "context": "key context from DM conversation that panelists should know",',
5334
+ ' "specialRequests": "any special instructions (null if none)",',
5335
+ ' "panelists": [',
5336
+ ' { "mateId": "...", "role": "perspective/stance", "brief": "what this panelist should argue for" }',
5337
+ " ]",
5338
+ "}",
5339
+ ].join("\n");
5340
+
5341
+ // Persist and start watcher
5342
+ persistDebateState(session);
5343
+ startDebateBriefWatcher(session, debate, briefPath);
5344
+
5345
+ // Notify clients
5346
+ var preparingMsg = {
5347
+ type: "debate_preparing",
5348
+ topic: debate.topic || "(Setting up...)",
5349
+ moderatorId: debate.moderatorId,
5350
+ moderatorName: moderatorProfile.name,
5351
+ setupSessionId: setupSession.localId,
5352
+ panelists: debate.panelists.map(function (p) {
5353
+ var prof = getMateProfile(mateCtx, p.mateId);
5354
+ return { mateId: p.mateId, name: prof.name };
5355
+ }),
5356
+ };
5357
+ sendTo(ws, preparingMsg);
5358
+ sendToSession(session.localId, preparingMsg);
5359
+ sendToSession(setupSession.localId, preparingMsg);
5360
+
5361
+ // Use moderator's own Claude identity to generate the brief via mention session
5362
+ var claudeMd = loadMateClaudeMd(mateCtx, debate.moderatorId);
5363
+ var digests = loadMateDigests(mateCtx, debate.moderatorId);
5364
+
5365
+ var briefText = "";
5366
+ sdk.createMentionSession({
5367
+ claudeMd: claudeMd,
5368
+ initialContext: digests,
5369
+ initialMessage: quickBriefPrompt,
5370
+ onActivity: function () {},
5371
+ onDelta: function (delta) { briefText += delta; },
5372
+ onDone: function () {
5373
+ try {
5374
+ var cleaned = briefText.trim();
5375
+ if (cleaned.indexOf("```") === 0) {
5376
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5377
+ }
5378
+ // Validate it is parseable JSON
5379
+ JSON.parse(cleaned);
5380
+ // Write brief.json for the watcher to pick up
5381
+ fs.writeFileSync(briefPath, cleaned, "utf8");
5382
+ console.log("[debate-quick] Moderator generated brief, wrote to " + briefPath);
5383
+ } catch (e) {
5384
+ console.error("[debate-quick] Failed to generate brief:", e.message);
5385
+ console.error("[debate-quick] Raw output:", briefText.substring(0, 500));
5386
+ // Fall back: write a minimal brief
5387
+ var fallbackBrief = {
5388
+ topic: debate.topic || "Discussion",
5389
+ format: "free_discussion",
5390
+ context: "",
5391
+ specialRequests: null,
5392
+ panelists: debate.panelists.map(function (p) {
5393
+ var prof = getMateProfile(mateCtx, p.mateId);
5394
+ return { mateId: p.mateId, role: "participant", brief: "Share your perspective on the topic." };
5395
+ }),
5396
+ };
5397
+ try {
5398
+ fs.writeFileSync(briefPath, JSON.stringify(fallbackBrief), "utf8");
5399
+ console.log("[debate-quick] Wrote fallback brief");
5400
+ } catch (fe) {
5401
+ console.error("[debate-quick] Failed to write fallback brief:", fe.message);
5402
+ endDebate(session, "error");
5403
+ }
5404
+ }
5405
+ },
5406
+ onError: function (err) {
5407
+ console.error("[debate-quick] Moderator brief generation failed:", err);
5408
+ endDebate(session, "error");
5409
+ },
5410
+ });
5411
+ }
5412
+
5413
+ // Standard debate setup via clay-debate-setup skill
5414
+ function handleDebateSkillSetup(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath) {
5415
+ var debateId = debate.debateId;
5416
+
5417
+ // Create a new session for the setup skill (like Ralph crafting)
5147
5418
  var setupSession = sm.createSession();
5148
5419
  setupSession.title = "Debate Setup: " + msg.topic.slice(0, 40);
5149
5420
  setupSession.debateSetupMode = true;
@@ -5151,7 +5422,6 @@ function createProjectContext(opts) {
5151
5422
  sm.saveSessionFile(setupSession);
5152
5423
  sm.switchSession(setupSession.localId, null, hydrateImageRefs);
5153
5424
  debate.setupSessionId = setupSession.localId;
5154
- debate.debateId = debateId;
5155
5425
  debate.setupStartedAt = setupSession.loop.startedAt;
5156
5426
 
5157
5427
  // Build panelist info for the skill prompt
@@ -5160,11 +5430,6 @@ function createProjectContext(opts) {
5160
5430
  return prof.name || p.mateId;
5161
5431
  }).join(", ");
5162
5432
 
5163
- var debateDir = path.join(cwd, ".clay", "debates", debateId);
5164
- try { fs.mkdirSync(debateDir, { recursive: true }); } catch (e) {}
5165
- var briefPath = path.join(debateDir, "brief.json");
5166
- console.log("[debate] cwd=" + cwd + " debateDir=" + debateDir + " briefPath=" + briefPath);
5167
-
5168
5433
  var craftingPrompt = "Use the /clay-debate-setup skill to prepare a structured debate. " +
5169
5434
  "You MUST invoke the clay-debate-setup skill. Do NOT start the debate yourself.\n\n" +
5170
5435
  "## Initial Topic\n" + msg.topic + "\n\n" +
@@ -5180,7 +5445,6 @@ function createProjectContext(opts) {
5180
5445
  "## Spoken Language\nKorean (unless user switches)";
5181
5446
 
5182
5447
  // Persist debate state before starting watcher
5183
- debate.briefPath = briefPath;
5184
5448
  persistDebateState(session);
5185
5449
 
5186
5450
  // Watch for brief.json in the debate-specific directory
@@ -5693,12 +5957,33 @@ function createProjectContext(opts) {
5693
5957
  debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
5694
5958
  }
5695
5959
 
5960
+ function handleDebateConfirmBrief(ws) {
5961
+ var session = getSessionForWs(ws);
5962
+ if (!session) return;
5963
+
5964
+ var debate = session._debate;
5965
+ if (!debate || debate.phase !== "reviewing") {
5966
+ sendTo(ws, { type: "debate_error", error: "No debate brief to confirm." });
5967
+ return;
5968
+ }
5969
+
5970
+ console.log("[debate] User confirmed brief, transitioning to live. Topic:", debate.topic);
5971
+ startDebateLive(session);
5972
+ }
5973
+
5696
5974
  function handleDebateStop(ws) {
5697
5975
  var session = getSessionForWs(ws);
5698
5976
  if (!session) return;
5699
5977
 
5700
5978
  var debate = session._debate;
5701
- if (!debate || debate.phase !== "live") return;
5979
+ if (!debate) return;
5980
+
5981
+ if (debate.phase === "reviewing") {
5982
+ endDebate(session, "user_stopped");
5983
+ return;
5984
+ }
5985
+
5986
+ if (debate.phase !== "live") return;
5702
5987
 
5703
5988
  if (debate.turnInProgress) {
5704
5989
  // Let current turn finish, then end
@@ -6756,8 +7041,14 @@ function createProjectContext(opts) {
6756
7041
  var claudeMdPath = path.join(cwd, "CLAUDE.md");
6757
7042
  // Enforce immediately on startup
6758
7043
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
7044
+ try {
7045
+ var _projList = getProjectList();
7046
+ var _projData = _projList.filter(function (p) { return !p.isMate && !p.isWorktree; }).map(function (p) { return { slug: p.slug, path: p.path, title: p.title || p.project, icon: p.icon }; });
7047
+ matesModule.enforceProjectRegistry(claudeMdPath, _projData);
7048
+ } catch (e) {}
6759
7049
  try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
6760
7050
  try { matesModule.enforceStickyNotes(claudeMdPath); } catch (e) {}
7051
+ try { matesModule.enforceDebateAwareness(claudeMdPath); } catch (e) {}
6761
7052
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
6762
7053
  // Sync sticky notes knowledge file on startup
6763
7054
  try {
@@ -6778,8 +7069,14 @@ function createProjectContext(opts) {
6778
7069
  crisisDebounce = setTimeout(function () {
6779
7070
  crisisDebounce = null;
6780
7071
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
7072
+ try {
7073
+ var _projList2 = getProjectList();
7074
+ var _projData2 = _projList2.filter(function (p) { return !p.isMate && !p.isWorktree; }).map(function (p) { return { slug: p.slug, path: p.path, title: p.title || p.project, icon: p.icon }; });
7075
+ matesModule.enforceProjectRegistry(claudeMdPath, _projData2);
7076
+ } catch (e) {}
6781
7077
  try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
6782
7078
  try { matesModule.enforceStickyNotes(claudeMdPath); } catch (e) {}
7079
+ try { matesModule.enforceDebateAwareness(claudeMdPath); } catch (e) {}
6783
7080
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
6784
7081
  }, 500);
6785
7082
  });