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.
- package/lib/builtin-mates.js +22 -2
- package/lib/mates.js +417 -0
- package/lib/project.js +282 -200
- package/lib/public/app.js +35 -3
- package/lib/public/css/home-hub.css +13 -0
- package/lib/public/css/icon-strip.css +9 -0
- package/lib/public/css/mates.css +73 -0
- package/lib/public/css/sidebar.css +1 -1
- package/lib/public/modules/debate.js +15 -0
- package/lib/public/modules/input.js +8 -0
- package/lib/public/modules/mate-sidebar.js +89 -0
- package/lib/public/modules/sidebar.js +16 -12
- package/lib/sdk-bridge.js +147 -59
- package/lib/server.js +114 -61
- package/lib/session-search.js +662 -0
- package/lib/terminal.js +3 -2
- package/lib/users.js +2 -2
- package/package.json +1 -1
package/lib/builtin-mates.js
CHANGED
|
@@ -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,
|
|
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
|
};
|