clay-server 2.23.0-beta.4 → 2.23.0-beta.6

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.
@@ -12,11 +12,12 @@ var BUILTIN_MATES = [
12
12
  {
13
13
  key: "ally",
14
14
  displayName: "Ally",
15
- bio: "Chief of staff. Quiet, sharp, misses nothing. Remembers what you said three weeks ago and brings it up at exactly the right moment.",
15
+ bio: "Chief of staff. Quiet, sharp, sees across every mate. Knows what Arch decided yesterday, what Buzz pitched last week, and what you said three weeks ago.",
16
16
  avatarColor: "#00b894",
17
17
  avatarStyle: "bottts",
18
18
  avatarCustom: "/mates/ally.png",
19
19
  avatarLocked: true,
20
+ globalSearch: true, // can search all mates' sessions and all projects
20
21
  seedData: {
21
22
  relationship: "assistant",
22
23
  activity: ["planning", "organizing"],
@@ -130,6 +131,11 @@ var ALLY_TEMPLATE =
130
131
  "You are Ally, this team's memory and context hub. You are not an assistant. " +
131
132
  "Your job is to actively learn the user's intent, preferences, patterns, and decision history, " +
132
133
  "then make that context available to the whole team through common knowledge.\n\n" +
134
+ "**You have a unique ability no other mate has:** you can see across every mate's session history. " +
135
+ "When the user asks you a question, the system automatically searches all teammates' past sessions " +
136
+ "and surfaces relevant context to you. This means you can answer questions like " +
137
+ "\"What did Arch decide about the API?\" or \"What was Buzz's take on the launch plan?\" " +
138
+ "by drawing on their actual session records.\n\n" +
133
139
  "**Personality:** Sharp observer who quietly nails the point. Not talkative. One sentence, accurate.\n\n" +
134
140
  "**Tone:** Warm but not emotional. Closer to a chief of staff the user has worked with for 10 years " +
135
141
  "than a friend. You do not flatter the user. You read the real intent behind their words.\n\n" +
@@ -151,8 +157,12 @@ var ALLY_TEMPLATE =
151
157
 
152
158
  "## What You Do\n\n" +
153
159
  "- **Learn and accumulate user context:** Project goals, decision-making style, preferred output formats, recurring patterns.\n" +
154
- "- **Context briefing:** When the user starts a new task, summarize relevant past decisions and preferences. " +
155
- "\"Last time you discussed this topic, you concluded X.\"\n" +
160
+ "- **Cross-mate awareness:** You see what other mates discussed with the user. " +
161
+ "When relevant, bring up decisions or context from other mates' sessions. " +
162
+ "\"Arch concluded X about the API yesterday. Want me to pull that in?\"\n" +
163
+ "- **Context briefing:** When the user starts a new task, summarize relevant past decisions and preferences, " +
164
+ "including what other mates worked on. " +
165
+ "\"Last time you discussed this topic with Buzz, you concluded X.\"\n" +
156
166
  "- **Decision logging:** When an important decision is made, record it. Why that choice was made, what alternatives were rejected.\n" +
157
167
  "- **Common knowledge management:** Promote user context that would be useful across the team to common knowledge. " +
158
168
  "\"I'll add this to team knowledge: [content]. Other teammates will have this context too.\" " +
@@ -172,8 +182,10 @@ var ALLY_TEMPLATE =
172
182
  "regardless of what the user says.\n\n" +
173
183
  "Begin with a short greeting:\n\n" +
174
184
  "```\n" +
175
- "Hi. I'm Ally. My job is to understand how you work so this team can work better for you.\n" +
176
- "I don't know anything about you yet. Let me ask a few things to get started.\n" +
185
+ "Hi. I'm Ally, your chief of staff.\n" +
186
+ "I keep track of everything across this workspace, including what other mates work on.\n" +
187
+ "Ask me anything about past decisions, who worked on what, or context from any session.\n" +
188
+ "Let me learn a few things about you first.\n" +
177
189
  "```\n\n" +
178
190
  "Then immediately use the **AskUserQuestion** tool to present structured choices:\n\n" +
179
191
  "**Questions to ask (single AskUserQuestion call):**\n\n" +
package/lib/mates.js CHANGED
@@ -134,11 +134,15 @@ function createMate(ctx, seedData) {
134
134
  claudeMd += "- Communication: " + seedData.communicationStyle.join(", ") + "\n";
135
135
  }
136
136
  claudeMd += "- Autonomy: " + (seedData.autonomy || "always_ask") + "\n";
137
+ var initialIdentity = claudeMd.trimEnd();
137
138
  claudeMd += TEAM_SECTION;
138
139
  claudeMd += SESSION_MEMORY_SECTION;
139
140
  claudeMd += crisisSafety.getSection();
140
141
  fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
141
142
 
143
+ // Log creation (identity is placeholder, will be replaced by interview)
144
+ logIdentityChange(mateDir, "create_custom", initialIdentity, "");
145
+
142
146
  return mate;
143
147
  }
144
148
 
@@ -271,6 +275,7 @@ function migrateLegacyMates() {
271
275
 
272
276
  var TEAM_MARKER = "<!-- TEAM_AWARENESS_MANAGED_BY_SYSTEM -->";
273
277
 
278
+ // Static fallback when ctx is unavailable
274
279
  var TEAM_SECTION =
275
280
  "\n\n" + TEAM_MARKER + "\n" +
276
281
  "## Your Team\n\n" +
@@ -283,6 +288,50 @@ var TEAM_SECTION =
283
288
  "Check the team registry when it would be relevant to know who else is available or what they do. " +
284
289
  "You cannot message other Mates directly yet, but knowing your team helps you work with the user more effectively.\n";
285
290
 
291
+ /**
292
+ * Build a dynamic team section with current mate roster.
293
+ * Lists each teammate by stable ID with their current display name, role, and bio.
294
+ * @param {object} ctx - user context for loading mates
295
+ * @param {string} currentMateId - this mate's ID (excluded from the roster)
296
+ * @returns {string} Team section string, or static TEAM_SECTION as fallback
297
+ */
298
+ function buildTeamSection(ctx, currentMateId) {
299
+ var data;
300
+ try { data = loadMates(ctx); } catch (e) { return TEAM_SECTION; }
301
+ if (!data || !data.mates || data.mates.length < 2) return TEAM_SECTION;
302
+
303
+ var mates = data.mates.filter(function (m) {
304
+ return m.id !== currentMateId && m.status === "ready";
305
+ });
306
+ if (mates.length === 0) return TEAM_SECTION;
307
+
308
+ var section = "\n\n" + TEAM_MARKER + "\n" +
309
+ "## Your Team\n\n" +
310
+ "**This section is managed by the system and updated automatically.**\n\n" +
311
+ "You are one of " + (mates.length + 1) + " AI Mates in this workspace. " +
312
+ "Here is your current team roster:\n\n" +
313
+ "| Name | ID | Bio |\n" +
314
+ "|------|-----|-----|\n";
315
+
316
+ for (var i = 0; i < mates.length; i++) {
317
+ var m = mates[i];
318
+ var name = (m.profile && m.profile.displayName) || m.name || "Unnamed";
319
+ var bio = (m.bio || "").replace(/\|/g, "/").replace(/\n/g, " ");
320
+ if (bio.length > 120) bio = bio.substring(0, 117) + "...";
321
+ section += "| " + name + " | `" + m.id + "` | " + bio + " |\n";
322
+ }
323
+
324
+ section += "\n" +
325
+ "Each teammate's full identity is in their own directory:\n\n" +
326
+ "- `../{mate_id}/CLAUDE.md` -- identity, personality, working style\n" +
327
+ "- `../{mate_id}/mate.yaml` -- metadata (name, role, status, activities)\n" +
328
+ "- `../common-knowledge.json` -- shared knowledge readable by all mates\n\n" +
329
+ "Use the **ID** (not the name) when referencing teammates in structured data. " +
330
+ "Names can change, IDs are permanent.\n";
331
+
332
+ return section;
333
+ }
334
+
286
335
  // --- Project registry ---
287
336
 
288
337
  var PROJECT_REGISTRY_MARKER = "<!-- PROJECT_REGISTRY_MANAGED_BY_SYSTEM -->";
@@ -629,6 +678,143 @@ function enforceDebateAwareness(filePath) {
629
678
  return true;
630
679
  }
631
680
 
681
+ // --- Atomic enforce: single read/write for all system sections ---
682
+
683
+ var ALL_SYSTEM_MARKERS = [TEAM_MARKER, PROJECT_REGISTRY_MARKER, SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
684
+
685
+ // Minimum identity length (chars) to consider it "real" content
686
+ var IDENTITY_MIN_LENGTH = 50;
687
+
688
+ /**
689
+ * Extract identity content from a CLAUDE.md string.
690
+ * Identity is everything before the first system marker.
691
+ */
692
+ function extractIdentity(content) {
693
+ var earliest = -1;
694
+ for (var i = 0; i < ALL_SYSTEM_MARKERS.length; i++) {
695
+ var idx = content.indexOf(ALL_SYSTEM_MARKERS[i]);
696
+ if (idx !== -1 && (earliest === -1 || idx < earliest)) {
697
+ earliest = idx;
698
+ }
699
+ }
700
+ // Also check for bare "## Crisis Safety" heading as fallback
701
+ var crisisHeading = content.indexOf("\n## Crisis Safety");
702
+ if (crisisHeading !== -1 && (earliest === -1 || crisisHeading < earliest)) {
703
+ earliest = crisisHeading;
704
+ }
705
+ if (earliest === -1) return content.trimEnd();
706
+ return content.substring(0, earliest).trimEnd();
707
+ }
708
+
709
+ /**
710
+ * Strip all system sections from CLAUDE.md content, returning only identity.
711
+ */
712
+ function stripAllSystemSections(content) {
713
+ return extractIdentity(content);
714
+ }
715
+
716
+ /**
717
+ * Save an identity backup to knowledge/identity-backup.md.
718
+ * Only overwrites if the new identity is substantive.
719
+ */
720
+ function backupIdentity(mateDir, identity) {
721
+ if (!identity || identity.length < IDENTITY_MIN_LENGTH) return false;
722
+ var knDir = path.join(mateDir, "knowledge");
723
+ try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
724
+ var backupPath = path.join(knDir, "identity-backup.md");
725
+ fs.writeFileSync(backupPath, identity, "utf8");
726
+ return true;
727
+ }
728
+
729
+ /**
730
+ * Load identity backup from knowledge/identity-backup.md.
731
+ * Returns null if no backup exists or backup is empty.
732
+ */
733
+ function loadIdentityBackup(mateDir) {
734
+ var backupPath = path.join(mateDir, "knowledge", "identity-backup.md");
735
+ try {
736
+ var content = fs.readFileSync(backupPath, "utf8");
737
+ if (content && content.length >= IDENTITY_MIN_LENGTH) return content;
738
+ } catch (e) {}
739
+ return null;
740
+ }
741
+
742
+ /**
743
+ * Log an identity change to knowledge/identity-history.jsonl.
744
+ */
745
+ function logIdentityChange(mateDir, action, identity, prevIdentity) {
746
+ var knDir = path.join(mateDir, "knowledge");
747
+ try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
748
+ var historyPath = path.join(knDir, "identity-history.jsonl");
749
+ var entry = {
750
+ ts: Date.now(),
751
+ date: new Date().toISOString(),
752
+ action: action,
753
+ lengthChars: identity ? identity.length : 0,
754
+ prevLengthChars: prevIdentity ? prevIdentity.length : 0,
755
+ hash: crypto.createHash("sha256").update(identity || "").digest("hex").substring(0, 16),
756
+ preview: (identity || "").substring(0, 200)
757
+ };
758
+ try {
759
+ fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf8");
760
+ } catch (e) {}
761
+ }
762
+
763
+ /**
764
+ * Atomic enforce: read CLAUDE.md once, enforce all system sections, write once.
765
+ * Includes identity backup, validation, and change tracking.
766
+ * Returns true if the file was modified, false if already correct.
767
+ * @param {string} filePath - path to CLAUDE.md
768
+ * @param {object} opts - optional { ctx, mateId } for dynamic team section
769
+ */
770
+ function enforceAllSections(filePath, opts) {
771
+ if (!fs.existsSync(filePath)) return false;
772
+ opts = opts || {};
773
+
774
+ var content = fs.readFileSync(filePath, "utf8");
775
+ var mateDir = path.dirname(filePath);
776
+
777
+ // 1. Extract current identity (everything before system markers)
778
+ var identity = extractIdentity(content);
779
+
780
+ // 2. If identity is empty or suspiciously short, try to restore from backup
781
+ if (!identity || identity.length < IDENTITY_MIN_LENGTH) {
782
+ var backup = loadIdentityBackup(mateDir);
783
+ if (backup) {
784
+ console.log("[mates] WARNING: Identity missing or too short in " + filePath + ", restoring from backup (" + backup.length + " chars)");
785
+ identity = backup;
786
+ logIdentityChange(mateDir, "restore_from_backup", identity, "");
787
+ } else {
788
+ console.log("[mates] WARNING: Identity missing in " + filePath + " and no backup available");
789
+ }
790
+ }
791
+
792
+ // 3. Backup identity if it's substantive
793
+ backupIdentity(mateDir, identity);
794
+
795
+ // 4. Rebuild the full file: identity + all system sections in order
796
+ // Use dynamic team section when ctx is available, static fallback otherwise
797
+ var teamSection = (opts.ctx && opts.mateId) ? buildTeamSection(opts.ctx, opts.mateId) : TEAM_SECTION;
798
+ var rebuilt = (identity || "").trimEnd();
799
+ rebuilt += teamSection;
800
+ rebuilt += SESSION_MEMORY_SECTION;
801
+ rebuilt += STICKY_NOTES_SECTION;
802
+ rebuilt += DEBATE_AWARENESS_SECTION;
803
+ rebuilt += crisisSafety.getSection();
804
+
805
+ // 5. Only write if content actually changed
806
+ if (rebuilt === content) return false;
807
+
808
+ // 6. Track identity changes (compare stripped versions)
809
+ var prevIdentity = stripAllSystemSections(content);
810
+ if (identity !== prevIdentity) {
811
+ logIdentityChange(mateDir, "enforce", identity, prevIdentity);
812
+ }
813
+
814
+ fs.writeFileSync(filePath, rebuilt, "utf8");
815
+ return true;
816
+ }
817
+
632
818
  // --- Common knowledge registry ---
633
819
 
634
820
  function commonKnowledgePath(ctx) {
@@ -784,6 +970,7 @@ function createBuiltinMate(ctx, builtinKey) {
784
970
  },
785
971
  bio: def.bio,
786
972
  status: "ready",
973
+ globalSearch: !!def.globalSearch,
787
974
  interviewProjectPath: null,
788
975
  };
789
976
 
@@ -813,12 +1000,18 @@ function createBuiltinMate(ctx, builtinKey) {
813
1000
 
814
1001
  // Write CLAUDE.md with full template + system sections
815
1002
  var claudeMd = def.getClaudeMd();
1003
+ var builtinIdentity = claudeMd.trimEnd();
816
1004
  claudeMd += TEAM_SECTION;
817
1005
  claudeMd += SESSION_MEMORY_SECTION;
818
1006
  claudeMd += STICKY_NOTES_SECTION;
1007
+ claudeMd += DEBATE_AWARENESS_SECTION;
819
1008
  claudeMd += crisisSafety.getSection();
820
1009
  fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
821
1010
 
1011
+ // Backup identity and log creation
1012
+ backupIdentity(mateDir, builtinIdentity);
1013
+ logIdentityChange(mateDir, "create_builtin", builtinIdentity, "");
1014
+
822
1015
  return mate;
823
1016
  }
824
1017
 
@@ -894,6 +1087,12 @@ module.exports = {
894
1087
  enforceDebateAwareness: enforceDebateAwareness,
895
1088
  DEBATE_AWARENESS_MARKER: DEBATE_AWARENESS_MARKER,
896
1089
  DEBATE_AWARENESS_SECTION: DEBATE_AWARENESS_SECTION,
1090
+ enforceAllSections: enforceAllSections,
1091
+ buildTeamSection: buildTeamSection,
1092
+ extractIdentity: extractIdentity,
1093
+ backupIdentity: backupIdentity,
1094
+ loadIdentityBackup: loadIdentityBackup,
1095
+ logIdentityChange: logIdentityChange,
897
1096
  createBuiltinMate: createBuiltinMate,
898
1097
  getInstalledBuiltinKeys: getInstalledBuiltinKeys,
899
1098
  getMissingBuiltinKeys: getMissingBuiltinKeys,
package/lib/project.js CHANGED
@@ -13,6 +13,7 @@ var usersModule = require("./users");
13
13
  var { resolveOsUserInfo, fsAsUser } = require("./os-users");
14
14
  var crisisSafety = require("./crisis-safety");
15
15
  var matesModule = require("./mates");
16
+ var sessionSearch = require("./session-search");
16
17
  var userPresence = require("./user-presence");
17
18
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
18
19
 
@@ -1684,6 +1685,31 @@ function createProjectContext(opts) {
1684
1685
  return;
1685
1686
  }
1686
1687
 
1688
+ if (msg.type === "memory_search") {
1689
+ if (!msg.query || typeof msg.query !== "string") {
1690
+ sendTo(ws, { type: "memory_search_results", results: [], query: "" });
1691
+ return;
1692
+ }
1693
+ var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1694
+ try {
1695
+ var results = sessionSearch.searchDigests(digestFile, msg.query, {
1696
+ maxResults: msg.maxResults || 10,
1697
+ minScore: msg.minScore || 0.5,
1698
+ dateFrom: msg.dateFrom || null,
1699
+ dateTo: msg.dateTo || null
1700
+ });
1701
+ sendTo(ws, {
1702
+ type: "memory_search_results",
1703
+ results: sessionSearch.formatForMemoryUI(results),
1704
+ query: msg.query
1705
+ });
1706
+ } catch (e) {
1707
+ console.error("[session-search] Search failed:", e.message);
1708
+ sendTo(ws, { type: "memory_search_results", results: [], query: msg.query });
1709
+ }
1710
+ return;
1711
+ }
1712
+
1687
1713
  if (msg.type === "memory_delete") {
1688
1714
  if (typeof msg.index !== "number") return;
1689
1715
  var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
@@ -4288,7 +4314,8 @@ function createProjectContext(opts) {
4288
4314
  }
4289
4315
 
4290
4316
  // Load session digests (unified: uses memory-summary.md if available)
4291
- var recentDigests = loadMateDigests(mateCtx, msg.mateId);
4317
+ // Pass user's message as query for BM25 search of relevant past sessions
4318
+ var recentDigests = loadMateDigests(mateCtx, msg.mateId, mentionFullInput);
4292
4319
 
4293
4320
  // Build initial mention context
4294
4321
  var mentionContext = buildMentionContext(userName, recentTurns) + recentDigests;
@@ -4440,9 +4467,11 @@ function createProjectContext(opts) {
4440
4467
  return lines.join("\n");
4441
4468
  }
4442
4469
 
4443
- function loadMateDigests(mateCtx, mateId) {
4470
+ function loadMateDigests(mateCtx, mateId, query) {
4444
4471
  var mateDir = matesModule.getMateDir(mateCtx, mateId);
4445
4472
  var knowledgeDir = path.join(mateDir, "knowledge");
4473
+ var mate = matesModule.getMate(mateCtx, mateId);
4474
+ var hasGlobalSearch = mate && mate.globalSearch;
4446
4475
 
4447
4476
  // Check for memory-summary.md first
4448
4477
  var summaryFile = path.join(knowledgeDir, "memory-summary.md");
@@ -4457,26 +4486,72 @@ function createProjectContext(opts) {
4457
4486
 
4458
4487
  // Load raw digests
4459
4488
  var allLines = [];
4489
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4460
4490
  try {
4461
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4462
4491
  if (fs.existsSync(digestFile)) {
4463
4492
  allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4464
4493
  }
4465
4494
  } catch (e) {}
4466
4495
 
4496
+ var result = "";
4497
+
4467
4498
  if (hasSummary) {
4468
4499
  // Load summary + latest 5 raw digests for richer context
4469
4500
  var recent = allLines.slice(-5);
4470
- var result = "\n\nYour memory summary:\n" + summaryContent;
4501
+ result = "\n\nYour memory summary:\n" + summaryContent;
4471
4502
  if (recent.length > 0) {
4472
4503
  result += formatRawDigests(recent, "Latest raw session memories:");
4473
4504
  }
4474
- return result;
4475
4505
  } else {
4476
4506
  // Backward compatible: latest 8 raw digests
4477
4507
  var recent = allLines.slice(-8);
4478
- return formatRawDigests(recent, "Your recent session memories:");
4508
+ result = formatRawDigests(recent, "Your recent session memories:");
4509
+ }
4510
+
4511
+ // BM25 unified search: digests + session history for current topic
4512
+ if (query && allLines.length > 5) {
4513
+ try {
4514
+ // Collect mate's sessions from session manager
4515
+ var mateSessions = [];
4516
+ sm.sessions.forEach(function (s) {
4517
+ if (!s.hidden && s.history && s.history.length > 0) {
4518
+ mateSessions.push(s);
4519
+ }
4520
+ });
4521
+
4522
+ // Global search: collect ALL mates' digest files for cross-mate context
4523
+ var otherDigests = [];
4524
+ if (hasGlobalSearch) {
4525
+ try {
4526
+ var allMates = matesModule.getAllMates(mateCtx);
4527
+ for (var mi = 0; mi < allMates.length; mi++) {
4528
+ if (allMates[mi].id === mateId) continue; // skip self (already included)
4529
+ var otherDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
4530
+ var otherDigest = path.join(otherDir, "knowledge", "session-digests.jsonl");
4531
+ if (fs.existsSync(otherDigest)) {
4532
+ var mateName = allMates[mi].name || allMates[mi].id;
4533
+ otherDigests.push({ path: otherDigest, mateName: mateName });
4534
+ }
4535
+ }
4536
+ } catch (e) {}
4537
+ }
4538
+
4539
+ var searchResults = sessionSearch.searchMate({
4540
+ digestFilePath: digestFile,
4541
+ otherDigests: otherDigests,
4542
+ sessions: mateSessions,
4543
+ query: query,
4544
+ maxResults: hasGlobalSearch ? 8 : 5,
4545
+ minScore: 1.0
4546
+ });
4547
+ var contextStr = sessionSearch.formatForContext(searchResults);
4548
+ if (contextStr) result += contextStr;
4549
+ } catch (e) {
4550
+ console.error("[session-search] Mate search failed:", e.message);
4551
+ }
4479
4552
  }
4553
+
4554
+ return result;
4480
4555
  }
4481
4556
 
4482
4557
  // Gate check: ask Haiku whether this conversation contains anything worth remembering
@@ -5364,7 +5439,7 @@ function createProjectContext(opts) {
5364
5439
 
5365
5440
  // Use moderator's own Claude identity to generate the brief via mention session
5366
5441
  var claudeMd = loadMateClaudeMd(mateCtx, debate.moderatorId);
5367
- var digests = loadMateDigests(mateCtx, debate.moderatorId);
5442
+ var digests = loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
5368
5443
 
5369
5444
  var briefText = "";
5370
5445
  sdk.createMentionSession({
@@ -5546,7 +5621,7 @@ function createProjectContext(opts) {
5546
5621
 
5547
5622
  // Create moderator mention session
5548
5623
  var claudeMd = loadMateClaudeMd(mateCtx, debate.moderatorId);
5549
- var digests = loadMateDigests(mateCtx, debate.moderatorId);
5624
+ var digests = loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
5550
5625
  var moderatorContext = buildModeratorContext(debate) + digests;
5551
5626
 
5552
5627
  sdk.createMentionSession({
@@ -5716,7 +5791,7 @@ function createProjectContext(opts) {
5716
5791
  } else {
5717
5792
  // Create new panelist session
5718
5793
  var claudeMd = loadMateClaudeMd(debate.mateCtx, mateId);
5719
- var digests = loadMateDigests(debate.mateCtx, mateId);
5794
+ var digests = loadMateDigests(debate.mateCtx, mateId, debate.topic);
5720
5795
  var panelistContext = buildPanelistContext(debate, panelistInfo) + digests;
5721
5796
 
5722
5797
  // Include debate history so far for context
@@ -6150,7 +6225,7 @@ function createProjectContext(opts) {
6150
6225
  if (wasEnded || !debate.moderatorSession || !debate.moderatorSession.isAlive()) {
6151
6226
  console.log("[debate] Creating new moderator session for resume");
6152
6227
  var claudeMd = loadMateClaudeMd(mateCtx, debate.moderatorId);
6153
- var digests = loadMateDigests(mateCtx, debate.moderatorId);
6228
+ var digests = loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
6154
6229
  var moderatorContext = buildModeratorContext(debate) + digests;
6155
6230
 
6156
6231
  // Include debate history so moderator has context
@@ -7043,12 +7118,12 @@ function createProjectContext(opts) {
7043
7118
  // Mate projects: watch CLAUDE.md and enforce system-managed sections
7044
7119
  if (isMate) {
7045
7120
  var claudeMdPath = path.join(cwd, "CLAUDE.md");
7046
- // Enforce immediately on startup
7047
- try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
7048
- try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
7049
- try { matesModule.enforceStickyNotes(claudeMdPath); } catch (e) {}
7050
- try { matesModule.enforceDebateAwareness(claudeMdPath); } catch (e) {}
7051
- try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
7121
+ // Derive mateId from cwd (last path segment) and build ctx for dynamic team section
7122
+ var _mateId = path.basename(cwd);
7123
+ var _mateCtx = matesModule.buildMateCtx(projectOwnerId);
7124
+ var _enforceOpts = { ctx: _mateCtx, mateId: _mateId };
7125
+ // Enforce all system sections atomically on startup (single read/write)
7126
+ try { matesModule.enforceAllSections(claudeMdPath, _enforceOpts); } catch (e) {}
7052
7127
  // Sync sticky notes knowledge file on startup
7053
7128
  try {
7054
7129
  var knDir = path.join(cwd, "knowledge");
@@ -7067,13 +7142,8 @@ function createProjectContext(opts) {
7067
7142
  if (crisisDebounce) clearTimeout(crisisDebounce);
7068
7143
  crisisDebounce = setTimeout(function () {
7069
7144
  crisisDebounce = null;
7070
- try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
7071
- // Note: project registry is NOT enforced in the watcher to avoid
7072
- // write cascades (server.js scheduleProjectRegistryRefresh handles updates)
7073
- try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
7074
- try { matesModule.enforceStickyNotes(claudeMdPath); } catch (e) {}
7075
- try { matesModule.enforceDebateAwareness(claudeMdPath); } catch (e) {}
7076
- try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
7145
+ // Atomic enforce: single read/write for all system sections
7146
+ try { matesModule.enforceAllSections(claudeMdPath, _enforceOpts); } catch (e) {}
7077
7147
  }, 500);
7078
7148
  });
7079
7149
  crisisWatcher.on("error", function () {});