clay-server 2.23.0-beta.5 → 2.23.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.
@@ -8,15 +8,22 @@
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  var BUILTIN_MATES = [
11
- // ---- ALLY ----
11
+ // ---- ALLY (primary mate: code-managed, auto-updated, non-deletable) ----
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
+ // --- Primary mate flags ---
21
+ // Primary mates are system infrastructure, not just pre-made mates.
22
+ // They are auto-synced with the latest code on every startup,
23
+ // cannot be deleted by users, and have elevated capabilities.
24
+ primary: true, // code-managed, auto-updated on startup
25
+ globalSearch: true, // searches all mates' sessions and projects
26
+ templateVersion: 3, // v3: moved capabilities to dynamic system section
20
27
  seedData: {
21
28
  relationship: "assistant",
22
29
  activity: ["planning", "organizing"],
@@ -581,8 +588,21 @@ function getBuiltinKeys() {
581
588
  return keys;
582
589
  }
583
590
 
591
+ /**
592
+ * Get all primary mate definitions.
593
+ * Primary mates are code-managed system agents (not just pre-made mates).
594
+ */
595
+ function getPrimaryMates() {
596
+ var result = [];
597
+ for (var i = 0; i < BUILTIN_MATES.length; i++) {
598
+ if (BUILTIN_MATES[i].primary) result.push(BUILTIN_MATES[i]);
599
+ }
600
+ return result;
601
+ }
602
+
584
603
  module.exports = {
585
604
  BUILTIN_MATES: BUILTIN_MATES,
586
605
  getBuiltinByKey: getBuiltinByKey,
587
606
  getBuiltinKeys: getBuiltinKeys,
607
+ getPrimaryMates: getPrimaryMates,
588
608
  };
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
 
@@ -154,6 +158,17 @@ function updateMate(ctx, id, updates) {
154
158
  var data = loadMates(ctx);
155
159
  for (var i = 0; i < data.mates.length; i++) {
156
160
  if (data.mates[i].id === id) {
161
+ // Primary mates: protect name and core identity fields
162
+ if (data.mates[i].primary) {
163
+ delete updates.name;
164
+ delete updates.bio;
165
+ delete updates.primary;
166
+ delete updates.globalSearch;
167
+ delete updates.templateVersion;
168
+ if (updates.profile) {
169
+ delete updates.profile.displayName;
170
+ }
171
+ }
157
172
  var keys = Object.keys(updates);
158
173
  for (var j = 0; j < keys.length; j++) {
159
174
  data.mates[i][keys[j]] = updates[keys[j]];
@@ -167,6 +182,12 @@ function updateMate(ctx, id, updates) {
167
182
 
168
183
  function deleteMate(ctx, id) {
169
184
  var data = loadMates(ctx);
185
+ // Primary mates cannot be deleted (they are system infrastructure)
186
+ for (var di = 0; di < data.mates.length; di++) {
187
+ if (data.mates[di].id === id && data.mates[di].primary) {
188
+ return { error: "Primary mates cannot be deleted. They are managed by the system." };
189
+ }
190
+ }
170
191
  var before = data.mates.length;
171
192
  data.mates = data.mates.filter(function (m) {
172
193
  return m.id !== id;
@@ -271,6 +292,7 @@ function migrateLegacyMates() {
271
292
 
272
293
  var TEAM_MARKER = "<!-- TEAM_AWARENESS_MANAGED_BY_SYSTEM -->";
273
294
 
295
+ // Static fallback when ctx is unavailable
274
296
  var TEAM_SECTION =
275
297
  "\n\n" + TEAM_MARKER + "\n" +
276
298
  "## Your Team\n\n" +
@@ -283,6 +305,50 @@ var TEAM_SECTION =
283
305
  "Check the team registry when it would be relevant to know who else is available or what they do. " +
284
306
  "You cannot message other Mates directly yet, but knowing your team helps you work with the user more effectively.\n";
285
307
 
308
+ /**
309
+ * Build a dynamic team section with current mate roster.
310
+ * Lists each teammate by stable ID with their current display name, role, and bio.
311
+ * @param {object} ctx - user context for loading mates
312
+ * @param {string} currentMateId - this mate's ID (excluded from the roster)
313
+ * @returns {string} Team section string, or static TEAM_SECTION as fallback
314
+ */
315
+ function buildTeamSection(ctx, currentMateId) {
316
+ var data;
317
+ try { data = loadMates(ctx); } catch (e) { return TEAM_SECTION; }
318
+ if (!data || !data.mates || data.mates.length < 2) return TEAM_SECTION;
319
+
320
+ var mates = data.mates.filter(function (m) {
321
+ return m.id !== currentMateId && m.status === "ready";
322
+ });
323
+ if (mates.length === 0) return TEAM_SECTION;
324
+
325
+ var section = "\n\n" + TEAM_MARKER + "\n" +
326
+ "## Your Team\n\n" +
327
+ "**This section is managed by the system and updated automatically.**\n\n" +
328
+ "You are one of " + (mates.length + 1) + " AI Mates in this workspace. " +
329
+ "Here is your current team roster:\n\n" +
330
+ "| Name | ID | Bio |\n" +
331
+ "|------|-----|-----|\n";
332
+
333
+ for (var i = 0; i < mates.length; i++) {
334
+ var m = mates[i];
335
+ var name = (m.profile && m.profile.displayName) || m.name || "Unnamed";
336
+ var bio = (m.bio || "").replace(/\|/g, "/").replace(/\n/g, " ");
337
+ if (bio.length > 120) bio = bio.substring(0, 117) + "...";
338
+ section += "| " + name + " | `" + m.id + "` | " + bio + " |\n";
339
+ }
340
+
341
+ section += "\n" +
342
+ "Each teammate's full identity is in their own directory:\n\n" +
343
+ "- `../{mate_id}/CLAUDE.md` -- identity, personality, working style\n" +
344
+ "- `../{mate_id}/mate.yaml` -- metadata (name, role, status, activities)\n" +
345
+ "- `../common-knowledge.json` -- shared knowledge readable by all mates\n\n" +
346
+ "Use the **ID** (not the name) when referencing teammates in structured data. " +
347
+ "Names can change, IDs are permanent.\n";
348
+
349
+ return section;
350
+ }
351
+
286
352
  // --- Project registry ---
287
353
 
288
354
  var PROJECT_REGISTRY_MARKER = "<!-- PROJECT_REGISTRY_MANAGED_BY_SYSTEM -->";
@@ -629,6 +695,210 @@ function enforceDebateAwareness(filePath) {
629
695
  return true;
630
696
  }
631
697
 
698
+ // --- Primary mate capabilities (dynamically injected, never stored in identity) ---
699
+
700
+ var PRIMARY_CAPABILITIES_MARKER = "<!-- PRIMARY_CAPABILITIES_MANAGED_BY_SYSTEM -->";
701
+
702
+ /**
703
+ * Build the capabilities section for a primary mate.
704
+ * This is injected as a system section so it auto-updates with code changes
705
+ * without touching the mate's identity in CLAUDE.md.
706
+ */
707
+ function buildPrimaryCapabilitiesSection(mate) {
708
+ if (!mate || !mate.primary) return "";
709
+
710
+ var parts = [
711
+ "\n\n" + PRIMARY_CAPABILITIES_MARKER,
712
+ "## System Capabilities",
713
+ "",
714
+ "**This section is managed by the system and updated automatically with each release.**",
715
+ ""
716
+ ];
717
+
718
+ if (mate.globalSearch) {
719
+ parts.push("### Cross-Mate Awareness");
720
+ parts.push("");
721
+ parts.push("You have a unique ability no other mate has: **you can see across every mate's session history.**");
722
+ parts.push("When the user asks you a question, the system automatically searches all teammates' past sessions");
723
+ parts.push("and surfaces relevant context to you. Results from other mates are tagged with their name (e.g. @Arch).");
724
+ parts.push("");
725
+ parts.push("Use this to:");
726
+ parts.push("- Answer questions like \"What did Arch decide about the API?\" or \"What was Buzz's take on the launch plan?\"");
727
+ parts.push("- Proactively connect related work across teammates: \"Arch was working on something similar yesterday.\"");
728
+ parts.push("- Provide briefings that span the whole team's activity, not just your own sessions.");
729
+ parts.push("");
730
+ parts.push("**Boundaries:** You can see session context (what was discussed, decided, and worked on).");
731
+ parts.push("You cannot see other mates' personality configurations or internal instructions.");
732
+ parts.push("");
733
+ }
734
+
735
+ return parts.join("\n");
736
+ }
737
+
738
+ // --- Atomic enforce: single read/write for all system sections ---
739
+
740
+ var ALL_SYSTEM_MARKERS = [TEAM_MARKER, PROJECT_REGISTRY_MARKER, PRIMARY_CAPABILITIES_MARKER, SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
741
+
742
+ // Minimum identity length (chars) to consider it "real" content
743
+ var IDENTITY_MIN_LENGTH = 50;
744
+
745
+ /**
746
+ * Extract identity content from a CLAUDE.md string.
747
+ * Identity is everything before the first system marker.
748
+ */
749
+ function extractIdentity(content) {
750
+ var earliest = -1;
751
+ for (var i = 0; i < ALL_SYSTEM_MARKERS.length; i++) {
752
+ var idx = content.indexOf(ALL_SYSTEM_MARKERS[i]);
753
+ if (idx !== -1 && (earliest === -1 || idx < earliest)) {
754
+ earliest = idx;
755
+ }
756
+ }
757
+ // Also check for bare "## Crisis Safety" heading as fallback
758
+ var crisisHeading = content.indexOf("\n## Crisis Safety");
759
+ if (crisisHeading !== -1 && (earliest === -1 || crisisHeading < earliest)) {
760
+ earliest = crisisHeading;
761
+ }
762
+ if (earliest === -1) return content.trimEnd();
763
+ return content.substring(0, earliest).trimEnd();
764
+ }
765
+
766
+ /**
767
+ * Strip all system sections from CLAUDE.md content, returning only identity.
768
+ */
769
+ function stripAllSystemSections(content) {
770
+ return extractIdentity(content);
771
+ }
772
+
773
+ /**
774
+ * Save an identity backup to knowledge/identity-backup.md.
775
+ * Only overwrites if the new identity is substantive.
776
+ */
777
+ function backupIdentity(mateDir, identity) {
778
+ if (!identity || identity.length < IDENTITY_MIN_LENGTH) return false;
779
+ var knDir = path.join(mateDir, "knowledge");
780
+ try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
781
+ var backupPath = path.join(knDir, "identity-backup.md");
782
+ fs.writeFileSync(backupPath, identity, "utf8");
783
+ return true;
784
+ }
785
+
786
+ /**
787
+ * Load identity backup from knowledge/identity-backup.md.
788
+ * Returns null if no backup exists or backup is empty.
789
+ */
790
+ function loadIdentityBackup(mateDir) {
791
+ var backupPath = path.join(mateDir, "knowledge", "identity-backup.md");
792
+ try {
793
+ var content = fs.readFileSync(backupPath, "utf8");
794
+ if (content && content.length >= IDENTITY_MIN_LENGTH) return content;
795
+ } catch (e) {}
796
+ return null;
797
+ }
798
+
799
+ /**
800
+ * Log an identity change to knowledge/identity-history.jsonl.
801
+ */
802
+ function logIdentityChange(mateDir, action, identity, prevIdentity) {
803
+ var knDir = path.join(mateDir, "knowledge");
804
+ try { fs.mkdirSync(knDir, { recursive: true }); } catch (e) {}
805
+ var historyPath = path.join(knDir, "identity-history.jsonl");
806
+ var entry = {
807
+ ts: Date.now(),
808
+ date: new Date().toISOString(),
809
+ action: action,
810
+ lengthChars: identity ? identity.length : 0,
811
+ prevLengthChars: prevIdentity ? prevIdentity.length : 0,
812
+ hash: crypto.createHash("sha256").update(identity || "").digest("hex").substring(0, 16),
813
+ preview: (identity || "").substring(0, 200)
814
+ };
815
+ try {
816
+ fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf8");
817
+ } catch (e) {}
818
+ }
819
+
820
+ // Tracks paths we've already warned about missing identity (avoids log spam)
821
+ var _warnedPaths = {};
822
+
823
+ /**
824
+ * Atomic enforce: read CLAUDE.md once, enforce all system sections, write once.
825
+ * Includes identity backup, validation, and change tracking.
826
+ * Returns true if the file was modified, false if already correct.
827
+ * @param {string} filePath - path to CLAUDE.md
828
+ * @param {object} opts - optional { ctx, mateId } for dynamic team section
829
+ */
830
+ function enforceAllSections(filePath, opts) {
831
+ if (!fs.existsSync(filePath)) return false;
832
+ opts = opts || {};
833
+
834
+ var content = fs.readFileSync(filePath, "utf8");
835
+ var mateDir = path.dirname(filePath);
836
+
837
+ // 1. Extract current identity (everything before system markers)
838
+ var identity = extractIdentity(content);
839
+ var identityMissing = !identity || identity.length < IDENTITY_MIN_LENGTH;
840
+
841
+ // 2. If identity is empty or suspiciously short, try to restore from backup
842
+ if (identityMissing) {
843
+ var backup = loadIdentityBackup(mateDir);
844
+ if (backup) {
845
+ console.log("[mates] Restoring identity from backup: " + filePath + " (" + backup.length + " chars)");
846
+ identity = backup;
847
+ logIdentityChange(mateDir, "restore_from_backup", identity, "");
848
+ identityMissing = false;
849
+ }
850
+ }
851
+
852
+ // 3. Backup identity if it's substantive
853
+ backupIdentity(mateDir, identity);
854
+
855
+ // 4. Rebuild the full file: identity + all system sections in order
856
+ // Use dynamic team section when ctx is available, static fallback otherwise
857
+ var teamSection = (opts.ctx && opts.mateId) ? buildTeamSection(opts.ctx, opts.mateId) : TEAM_SECTION;
858
+
859
+ // Primary mate capabilities (dynamically injected from code)
860
+ var capSection = "";
861
+ if (opts.ctx && opts.mateId) {
862
+ try {
863
+ var mate = getMate(opts.ctx, opts.mateId);
864
+ capSection = buildPrimaryCapabilitiesSection(mate);
865
+ } catch (e) {}
866
+ }
867
+
868
+ // Project registry (dynamically injected when project list is available)
869
+ var projSection = "";
870
+ if (opts.projects) {
871
+ projSection = buildProjectRegistrySection(opts.projects);
872
+ }
873
+
874
+ var rebuilt = (identity || "").trimEnd();
875
+ rebuilt += teamSection;
876
+ rebuilt += projSection;
877
+ rebuilt += capSection;
878
+ rebuilt += SESSION_MEMORY_SECTION;
879
+ rebuilt += STICKY_NOTES_SECTION;
880
+ rebuilt += DEBATE_AWARENESS_SECTION;
881
+ rebuilt += crisisSafety.getSection();
882
+
883
+ // 5. Only write if content actually changed
884
+ if (rebuilt === content) return false;
885
+
886
+ // 6. Warn about missing identity only once per path, and only when actually writing
887
+ if (identityMissing && !_warnedPaths[filePath]) {
888
+ _warnedPaths[filePath] = true;
889
+ console.log("[mates] WARNING: Identity missing in " + filePath + " and no backup available");
890
+ }
891
+
892
+ // 7. Track identity changes (compare stripped versions)
893
+ var prevIdentity = stripAllSystemSections(content);
894
+ if (identity !== prevIdentity) {
895
+ logIdentityChange(mateDir, "enforce", identity, prevIdentity);
896
+ }
897
+
898
+ fs.writeFileSync(filePath, rebuilt, "utf8");
899
+ return true;
900
+ }
901
+
632
902
  // --- Common knowledge registry ---
633
903
 
634
904
  function commonKnowledgePath(ctx) {
@@ -784,6 +1054,9 @@ function createBuiltinMate(ctx, builtinKey) {
784
1054
  },
785
1055
  bio: def.bio,
786
1056
  status: "ready",
1057
+ primary: !!def.primary,
1058
+ globalSearch: !!def.globalSearch,
1059
+ templateVersion: def.templateVersion || 0,
787
1060
  interviewProjectPath: null,
788
1061
  };
789
1062
 
@@ -813,12 +1086,25 @@ function createBuiltinMate(ctx, builtinKey) {
813
1086
 
814
1087
  // Write CLAUDE.md with full template + system sections
815
1088
  var claudeMd = def.getClaudeMd();
1089
+ var builtinIdentity = claudeMd.trimEnd();
816
1090
  claudeMd += TEAM_SECTION;
817
1091
  claudeMd += SESSION_MEMORY_SECTION;
818
1092
  claudeMd += STICKY_NOTES_SECTION;
1093
+ claudeMd += DEBATE_AWARENESS_SECTION;
819
1094
  claudeMd += crisisSafety.getSection();
820
1095
  fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
821
1096
 
1097
+ // Backup identity and log creation
1098
+ backupIdentity(mateDir, builtinIdentity);
1099
+ logIdentityChange(mateDir, "create_builtin", builtinIdentity, "");
1100
+
1101
+ // Save base template for primary mates (used for 3-way sync comparison)
1102
+ if (def.primary) {
1103
+ try {
1104
+ fs.writeFileSync(path.join(mateDir, "knowledge", "base-template.md"), builtinIdentity, "utf8");
1105
+ } catch (e) {}
1106
+ }
1107
+
822
1108
  return mate;
823
1109
  }
824
1110
 
@@ -858,6 +1144,128 @@ function ensureBuiltinMates(ctx, deletedKeys) {
858
1144
  return created;
859
1145
  }
860
1146
 
1147
+ /**
1148
+ * Sync primary mates with their latest code definition.
1149
+ *
1150
+ * Primary mates (def.primary === true) are system infrastructure, not just
1151
+ * pre-made mates. They are the only mates whose identity, metadata, and
1152
+ * capabilities are managed by code rather than by the user.
1153
+ *
1154
+ * What gets synced:
1155
+ * - Metadata: bio, name, profile, globalSearch, primary flag
1156
+ * - CLAUDE.md identity: re-applied when templateVersion changes
1157
+ * - templateVersion is stored on the mate object to track updates
1158
+ *
1159
+ * What is NOT synced (regular builtin mates):
1160
+ * - Arch, Rush, Ward, Pixel, Buzz are left as-is after creation
1161
+ *
1162
+ * Returns array of synced mate IDs.
1163
+ */
1164
+ function syncPrimaryMates(ctx) {
1165
+ var builtinMates = require("./builtin-mates");
1166
+ var primaryDefs = builtinMates.getPrimaryMates();
1167
+ if (primaryDefs.length === 0) return [];
1168
+
1169
+ var data = loadMates(ctx);
1170
+ var synced = [];
1171
+
1172
+ for (var pi = 0; pi < primaryDefs.length; pi++) {
1173
+ var def = primaryDefs[pi];
1174
+
1175
+ // Find the installed mate matching this primary definition
1176
+ var mate = null;
1177
+ for (var mi = 0; mi < data.mates.length; mi++) {
1178
+ if (data.mates[mi].builtinKey === def.key) { mate = data.mates[mi]; break; }
1179
+ }
1180
+ if (!mate) continue; // not installed yet (ensureBuiltinMates handles creation)
1181
+
1182
+ var changed = false;
1183
+
1184
+ // --- Sync metadata (always, every startup) ---
1185
+ if (mate.bio !== def.bio) { mate.bio = def.bio; changed = true; }
1186
+ if (mate.name !== def.displayName) { mate.name = def.displayName; changed = true; }
1187
+ if (!mate.primary) { mate.primary = true; changed = true; }
1188
+ if (!mate.globalSearch && def.globalSearch) { mate.globalSearch = true; changed = true; }
1189
+ if (mate.profile) {
1190
+ if (mate.profile.displayName !== def.displayName) {
1191
+ mate.profile.displayName = def.displayName; changed = true;
1192
+ }
1193
+ }
1194
+
1195
+ // --- Sync CLAUDE.md identity (only when templateVersion changes) ---
1196
+ // Uses 3-way comparison to preserve user/Ally modifications:
1197
+ // - base-template.md = the template that was last synced (what code wrote)
1198
+ // - current identity = what's in CLAUDE.md now (may have been modified)
1199
+ // - new template = latest code definition
1200
+ //
1201
+ // If current identity === old base → user didn't touch it → safe to replace
1202
+ // If current identity !== old base → user/Ally modified it → preserve, skip identity sync
1203
+ var currentVersion = mate.templateVersion || 0;
1204
+ var latestVersion = def.templateVersion || 0;
1205
+
1206
+ if (latestVersion > currentVersion) {
1207
+ var mateDir = getMateDir(ctx, mate.id);
1208
+ var claudePath = path.join(mateDir, "CLAUDE.md");
1209
+ var basePath = path.join(mateDir, "knowledge", "base-template.md");
1210
+ var latestIdentity = def.getClaudeMd().trimEnd();
1211
+
1212
+ try {
1213
+ var currentIdentity = "";
1214
+ if (fs.existsSync(claudePath)) {
1215
+ currentIdentity = extractIdentity(fs.readFileSync(claudePath, "utf8"));
1216
+ }
1217
+
1218
+ // Load the base template from last sync
1219
+ var oldBase = "";
1220
+ try { oldBase = fs.readFileSync(basePath, "utf8").trimEnd(); } catch (e) {}
1221
+
1222
+ var shouldUpdateIdentity = false;
1223
+ if (!currentIdentity || currentIdentity.length < IDENTITY_MIN_LENGTH) {
1224
+ // No identity at all, always apply
1225
+ shouldUpdateIdentity = true;
1226
+ } else if (!oldBase || currentIdentity === oldBase) {
1227
+ // Identity unchanged from last sync (user didn't modify) → safe to update
1228
+ shouldUpdateIdentity = true;
1229
+ } else {
1230
+ // User/Ally modified the identity since last sync → preserve their changes
1231
+ console.log("[mates] Primary mate " + mate.name + ": identity modified by user/Ally, preserving (v" + currentVersion + " -> v" + latestVersion + " metadata only)");
1232
+ }
1233
+
1234
+ if (shouldUpdateIdentity) {
1235
+ // Write just the identity, then let enforceAllSections rebuild system sections
1236
+ // (including dynamic capabilities section)
1237
+ fs.writeFileSync(claudePath, latestIdentity, "utf8");
1238
+ backupIdentity(mateDir, latestIdentity);
1239
+ logIdentityChange(mateDir, "sync_primary_v" + latestVersion, latestIdentity, currentIdentity);
1240
+ console.log("[mates] Primary mate identity updated: " + mate.name + " (v" + currentVersion + " -> v" + latestVersion + ")");
1241
+ } else {
1242
+ console.log("[mates] Primary mate metadata updated: " + mate.name + " (v" + currentVersion + " -> v" + latestVersion + ", identity preserved)");
1243
+ }
1244
+
1245
+ // Re-run enforceAllSections to inject latest dynamic sections (capabilities, team, etc.)
1246
+ enforceAllSections(claudePath, { ctx: ctx, mateId: mate.id });
1247
+
1248
+ // Save the latest template as base for future 3-way comparison
1249
+ try {
1250
+ fs.mkdirSync(path.join(mateDir, "knowledge"), { recursive: true });
1251
+ fs.writeFileSync(basePath, latestIdentity, "utf8");
1252
+ } catch (e) {}
1253
+
1254
+ } catch (e) {
1255
+ console.error("[mates] Failed to sync primary mate " + mate.id + ":", e.message);
1256
+ }
1257
+
1258
+ mate.templateVersion = latestVersion;
1259
+ changed = true;
1260
+ }
1261
+
1262
+ if (changed) synced.push(mate.id);
1263
+ }
1264
+
1265
+ if (synced.length > 0) saveMates(ctx, data);
1266
+ return synced;
1267
+ }
1268
+
861
1269
  module.exports = {
862
1270
  resolveMatesRoot: resolveMatesRoot,
863
1271
  buildMateCtx: buildMateCtx,
@@ -894,8 +1302,17 @@ module.exports = {
894
1302
  enforceDebateAwareness: enforceDebateAwareness,
895
1303
  DEBATE_AWARENESS_MARKER: DEBATE_AWARENESS_MARKER,
896
1304
  DEBATE_AWARENESS_SECTION: DEBATE_AWARENESS_SECTION,
1305
+ enforceAllSections: enforceAllSections,
1306
+ buildTeamSection: buildTeamSection,
1307
+ buildPrimaryCapabilitiesSection: buildPrimaryCapabilitiesSection,
1308
+ PRIMARY_CAPABILITIES_MARKER: PRIMARY_CAPABILITIES_MARKER,
1309
+ extractIdentity: extractIdentity,
1310
+ backupIdentity: backupIdentity,
1311
+ loadIdentityBackup: loadIdentityBackup,
1312
+ logIdentityChange: logIdentityChange,
897
1313
  createBuiltinMate: createBuiltinMate,
898
1314
  getInstalledBuiltinKeys: getInstalledBuiltinKeys,
899
1315
  getMissingBuiltinKeys: getMissingBuiltinKeys,
900
1316
  ensureBuiltinMates: ensureBuiltinMates,
1317
+ syncPrimaryMates: syncPrimaryMates,
901
1318
  };