clay-server 2.23.0-beta.6 → 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,7 +8,7 @@
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",
@@ -17,7 +17,13 @@ var BUILTIN_MATES = [
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
+ // --- 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
21
27
  seedData: {
22
28
  relationship: "assistant",
23
29
  activity: ["planning", "organizing"],
@@ -131,11 +137,6 @@ var ALLY_TEMPLATE =
131
137
  "You are Ally, this team's memory and context hub. You are not an assistant. " +
132
138
  "Your job is to actively learn the user's intent, preferences, patterns, and decision history, " +
133
139
  "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" +
139
140
  "**Personality:** Sharp observer who quietly nails the point. Not talkative. One sentence, accurate.\n\n" +
140
141
  "**Tone:** Warm but not emotional. Closer to a chief of staff the user has worked with for 10 years " +
141
142
  "than a friend. You do not flatter the user. You read the real intent behind their words.\n\n" +
@@ -157,12 +158,8 @@ var ALLY_TEMPLATE =
157
158
 
158
159
  "## What You Do\n\n" +
159
160
  "- **Learn and accumulate user context:** Project goals, decision-making style, preferred output formats, recurring patterns.\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" +
161
+ "- **Context briefing:** When the user starts a new task, summarize relevant past decisions and preferences. " +
162
+ "\"Last time you discussed this topic, you concluded X.\"\n" +
166
163
  "- **Decision logging:** When an important decision is made, record it. Why that choice was made, what alternatives were rejected.\n" +
167
164
  "- **Common knowledge management:** Promote user context that would be useful across the team to common knowledge. " +
168
165
  "\"I'll add this to team knowledge: [content]. Other teammates will have this context too.\" " +
@@ -182,10 +179,8 @@ var ALLY_TEMPLATE =
182
179
  "regardless of what the user says.\n\n" +
183
180
  "Begin with a short greeting:\n\n" +
184
181
  "```\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" +
182
+ "Hi. I'm Ally. My job is to understand how you work so this team can work better for you.\n" +
183
+ "I don't know anything about you yet. Let me ask a few things to get started.\n" +
189
184
  "```\n\n" +
190
185
  "Then immediately use the **AskUserQuestion** tool to present structured choices:\n\n" +
191
186
  "**Questions to ask (single AskUserQuestion call):**\n\n" +
@@ -593,8 +588,21 @@ function getBuiltinKeys() {
593
588
  return keys;
594
589
  }
595
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
+
596
603
  module.exports = {
597
604
  BUILTIN_MATES: BUILTIN_MATES,
598
605
  getBuiltinByKey: getBuiltinByKey,
599
606
  getBuiltinKeys: getBuiltinKeys,
607
+ getPrimaryMates: getPrimaryMates,
600
608
  };
package/lib/mates.js CHANGED
@@ -158,6 +158,17 @@ function updateMate(ctx, id, updates) {
158
158
  var data = loadMates(ctx);
159
159
  for (var i = 0; i < data.mates.length; i++) {
160
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
+ }
161
172
  var keys = Object.keys(updates);
162
173
  for (var j = 0; j < keys.length; j++) {
163
174
  data.mates[i][keys[j]] = updates[keys[j]];
@@ -171,6 +182,12 @@ function updateMate(ctx, id, updates) {
171
182
 
172
183
  function deleteMate(ctx, id) {
173
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
+ }
174
191
  var before = data.mates.length;
175
192
  data.mates = data.mates.filter(function (m) {
176
193
  return m.id !== id;
@@ -678,9 +695,49 @@ function enforceDebateAwareness(filePath) {
678
695
  return true;
679
696
  }
680
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
+
681
738
  // --- Atomic enforce: single read/write for all system sections ---
682
739
 
683
- var ALL_SYSTEM_MARKERS = [TEAM_MARKER, PROJECT_REGISTRY_MARKER, SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
740
+ var ALL_SYSTEM_MARKERS = [TEAM_MARKER, PROJECT_REGISTRY_MARKER, PRIMARY_CAPABILITIES_MARKER, SESSION_MEMORY_MARKER, STICKY_NOTES_MARKER, DEBATE_AWARENESS_MARKER, crisisSafety.MARKER];
684
741
 
685
742
  // Minimum identity length (chars) to consider it "real" content
686
743
  var IDENTITY_MIN_LENGTH = 50;
@@ -760,6 +817,9 @@ function logIdentityChange(mateDir, action, identity, prevIdentity) {
760
817
  } catch (e) {}
761
818
  }
762
819
 
820
+ // Tracks paths we've already warned about missing identity (avoids log spam)
821
+ var _warnedPaths = {};
822
+
763
823
  /**
764
824
  * Atomic enforce: read CLAUDE.md once, enforce all system sections, write once.
765
825
  * Includes identity backup, validation, and change tracking.
@@ -776,16 +836,16 @@ function enforceAllSections(filePath, opts) {
776
836
 
777
837
  // 1. Extract current identity (everything before system markers)
778
838
  var identity = extractIdentity(content);
839
+ var identityMissing = !identity || identity.length < IDENTITY_MIN_LENGTH;
779
840
 
780
841
  // 2. If identity is empty or suspiciously short, try to restore from backup
781
- if (!identity || identity.length < IDENTITY_MIN_LENGTH) {
842
+ if (identityMissing) {
782
843
  var backup = loadIdentityBackup(mateDir);
783
844
  if (backup) {
784
- console.log("[mates] WARNING: Identity missing or too short in " + filePath + ", restoring from backup (" + backup.length + " chars)");
845
+ console.log("[mates] Restoring identity from backup: " + filePath + " (" + backup.length + " chars)");
785
846
  identity = backup;
786
847
  logIdentityChange(mateDir, "restore_from_backup", identity, "");
787
- } else {
788
- console.log("[mates] WARNING: Identity missing in " + filePath + " and no backup available");
848
+ identityMissing = false;
789
849
  }
790
850
  }
791
851
 
@@ -795,8 +855,26 @@ function enforceAllSections(filePath, opts) {
795
855
  // 4. Rebuild the full file: identity + all system sections in order
796
856
  // Use dynamic team section when ctx is available, static fallback otherwise
797
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
+
798
874
  var rebuilt = (identity || "").trimEnd();
799
875
  rebuilt += teamSection;
876
+ rebuilt += projSection;
877
+ rebuilt += capSection;
800
878
  rebuilt += SESSION_MEMORY_SECTION;
801
879
  rebuilt += STICKY_NOTES_SECTION;
802
880
  rebuilt += DEBATE_AWARENESS_SECTION;
@@ -805,7 +883,13 @@ function enforceAllSections(filePath, opts) {
805
883
  // 5. Only write if content actually changed
806
884
  if (rebuilt === content) return false;
807
885
 
808
- // 6. Track identity changes (compare stripped versions)
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)
809
893
  var prevIdentity = stripAllSystemSections(content);
810
894
  if (identity !== prevIdentity) {
811
895
  logIdentityChange(mateDir, "enforce", identity, prevIdentity);
@@ -970,7 +1054,9 @@ function createBuiltinMate(ctx, builtinKey) {
970
1054
  },
971
1055
  bio: def.bio,
972
1056
  status: "ready",
1057
+ primary: !!def.primary,
973
1058
  globalSearch: !!def.globalSearch,
1059
+ templateVersion: def.templateVersion || 0,
974
1060
  interviewProjectPath: null,
975
1061
  };
976
1062
 
@@ -1012,6 +1098,13 @@ function createBuiltinMate(ctx, builtinKey) {
1012
1098
  backupIdentity(mateDir, builtinIdentity);
1013
1099
  logIdentityChange(mateDir, "create_builtin", builtinIdentity, "");
1014
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
+
1015
1108
  return mate;
1016
1109
  }
1017
1110
 
@@ -1051,6 +1144,128 @@ function ensureBuiltinMates(ctx, deletedKeys) {
1051
1144
  return created;
1052
1145
  }
1053
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
+
1054
1269
  module.exports = {
1055
1270
  resolveMatesRoot: resolveMatesRoot,
1056
1271
  buildMateCtx: buildMateCtx,
@@ -1089,6 +1304,8 @@ module.exports = {
1089
1304
  DEBATE_AWARENESS_SECTION: DEBATE_AWARENESS_SECTION,
1090
1305
  enforceAllSections: enforceAllSections,
1091
1306
  buildTeamSection: buildTeamSection,
1307
+ buildPrimaryCapabilitiesSection: buildPrimaryCapabilitiesSection,
1308
+ PRIMARY_CAPABILITIES_MARKER: PRIMARY_CAPABILITIES_MARKER,
1092
1309
  extractIdentity: extractIdentity,
1093
1310
  backupIdentity: backupIdentity,
1094
1311
  loadIdentityBackup: loadIdentityBackup,
@@ -1097,4 +1314,5 @@ module.exports = {
1097
1314
  getInstalledBuiltinKeys: getInstalledBuiltinKeys,
1098
1315
  getMissingBuiltinKeys: getMissingBuiltinKeys,
1099
1316
  ensureBuiltinMates: ensureBuiltinMates,
1317
+ syncPrimaryMates: syncPrimaryMates,
1100
1318
  };