@deeplake/hivemind 0.7.22 → 0.7.23

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.
@@ -6,13 +6,13 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
9
- "version": "0.7.22"
9
+ "version": "0.7.23"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "hivemind",
14
14
  "description": "Persistent shared memory powered by Deeplake — captures all session activity and provides cross-session, cross-agent memory search",
15
- "version": "0.7.22",
15
+ "version": "0.7.23",
16
16
  "source": "./claude-code",
17
17
  "homepage": "https://github.com/activeloopai/hivemind"
18
18
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hivemind",
3
3
  "description": "Cloud-backed persistent memory powered by Deeplake — read, write, and share memory across Claude Code sessions and agents",
4
- "version": "0.7.22",
4
+ "version": "0.7.23",
5
5
  "author": {
6
6
  "name": "Activeloop",
7
7
  "url": "https://deeplake.ai"
package/README.md CHANGED
@@ -268,7 +268,7 @@ Hivemind **codifies recurring patterns from your team's recent sessions into reu
268
268
 
269
269
  ```bash
270
270
  hivemind skillify # show current scope, team, install, per-project state
271
- hivemind skillify scope <me|team|org> # who counts as "in scope" for mining
271
+ hivemind skillify scope <me|team> # who counts as "in scope" for mining
272
272
  hivemind skillify pull # install teammates' skills locally
273
273
  hivemind skillify unpull # remove pulled skills
274
274
  ```
package/bundle/cli.js CHANGED
@@ -4770,7 +4770,7 @@ function loadScopeConfig() {
4770
4770
  return DEFAULT;
4771
4771
  try {
4772
4772
  const raw = JSON.parse(readFileSync9(CONFIG_PATH2, "utf-8"));
4773
- const scope = raw.scope === "team" || raw.scope === "org" ? raw.scope : "me";
4773
+ const scope = raw.scope === "team" ? "team" : raw.scope === "org" ? "team" : "me";
4774
4774
  const team = Array.isArray(raw.team) ? raw.team.filter((s) => typeof s === "string") : [];
4775
4775
  const install = raw.install === "global" ? "global" : "project";
4776
4776
  return { scope, team, install };
@@ -4816,18 +4816,25 @@ function parseFrontmatter(text) {
4816
4816
  const head = text.slice(4, end).trim();
4817
4817
  const body = text.slice(end + 4).replace(/^\r?\n/, "");
4818
4818
  const fm = { source_sessions: [] };
4819
- let mode = "kv";
4819
+ let arrayKey = null;
4820
4820
  for (const raw of head.split(/\r?\n/)) {
4821
- if (mode === "sources") {
4821
+ if (arrayKey) {
4822
4822
  const m2 = raw.match(/^\s+-\s+(.+)$/);
4823
4823
  if (m2) {
4824
- fm.source_sessions.push(m2[1].trim());
4824
+ const arr = fm[arrayKey] ?? [];
4825
+ arr.push(m2[1].trim());
4826
+ fm[arrayKey] = arr;
4825
4827
  continue;
4826
4828
  }
4827
- mode = "kv";
4829
+ arrayKey = null;
4828
4830
  }
4829
4831
  if (raw.startsWith("source_sessions:")) {
4830
- mode = "sources";
4832
+ arrayKey = "source_sessions";
4833
+ continue;
4834
+ }
4835
+ if (raw.startsWith("contributors:")) {
4836
+ arrayKey = "contributors";
4837
+ fm.contributors = [];
4831
4838
  continue;
4832
4839
  }
4833
4840
  const m = raw.match(/^([a-zA-Z_]+):\s*(.*)$/);
@@ -5017,11 +5024,19 @@ function buildPullSql(args) {
5017
5024
  where.push(`name = '${esc(args.skillName)}'`);
5018
5025
  }
5019
5026
  const whereClause = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
5020
- return `SELECT name, project, project_key, body, version, source_agent, scope, author, description, trigger_text, source_sessions, install, created_at, updated_at FROM "${args.tableName}"${whereClause} ORDER BY project_key ASC, name ASC, version DESC`;
5027
+ const contributorsCol = args.includeContributors === false ? "" : "contributors, ";
5028
+ return `SELECT name, project, project_key, body, version, source_agent, scope, author, ${contributorsCol}description, trigger_text, source_sessions, install, created_at, updated_at FROM "${args.tableName}"${whereClause} ORDER BY project_key ASC, name ASC, version DESC`;
5029
+ }
5030
+ function isMissingContributorsColumnError(message) {
5031
+ if (!message)
5032
+ return false;
5033
+ return /contributors.*(?:does not exist|not found|unknown)/i.test(message) || /(?:does not exist|unknown column).*contributors/i.test(message);
5021
5034
  }
5022
5035
  function isMissingTableError(message) {
5023
5036
  if (!message)
5024
5037
  return false;
5038
+ if (/\bcolumn\b/i.test(message))
5039
+ return false;
5025
5040
  return /Table does not exist|relation .* does not exist|no such table/i.test(message);
5026
5041
  }
5027
5042
  function resolvePullDestination(install, cwd) {
@@ -5117,11 +5132,16 @@ function selectLatestPerName(rows) {
5117
5132
  }
5118
5133
  function renderSkillFile(row) {
5119
5134
  const sources = parseSourceSessions(row.source_sessions);
5135
+ const author = typeof row.author === "string" && row.author.length > 0 ? row.author : void 0;
5136
+ const contributors = parseContributors(row.contributors);
5137
+ const renderedContributors = contributors.length > 0 ? contributors : author ? [author] : [];
5120
5138
  const fm = {
5121
5139
  name: String(row.name ?? ""),
5122
5140
  description: String(row.description ?? ""),
5123
5141
  trigger: typeof row.trigger_text === "string" && row.trigger_text.length > 0 ? String(row.trigger_text) : void 0,
5142
+ author,
5124
5143
  source_sessions: sources,
5144
+ contributors: renderedContributors,
5125
5145
  version: Number(row.version ?? 1),
5126
5146
  created_by_agent: String(row.source_agent ?? "unknown"),
5127
5147
  created_at: String(row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
@@ -5146,15 +5166,35 @@ function parseSourceSessions(v) {
5146
5166
  }
5147
5167
  return [];
5148
5168
  }
5169
+ function parseContributors(v) {
5170
+ if (Array.isArray(v))
5171
+ return v.map(String);
5172
+ if (typeof v === "string") {
5173
+ try {
5174
+ const parsed = JSON.parse(v);
5175
+ if (Array.isArray(parsed))
5176
+ return parsed.map(String);
5177
+ } catch {
5178
+ }
5179
+ }
5180
+ return [];
5181
+ }
5149
5182
  function renderFrontmatter(fm) {
5150
5183
  const lines = ["---"];
5151
5184
  lines.push(`name: ${fm.name}`);
5152
5185
  lines.push(`description: ${JSON.stringify(fm.description)}`);
5153
5186
  if (fm.trigger)
5154
5187
  lines.push(`trigger: ${JSON.stringify(fm.trigger)}`);
5188
+ if (fm.author)
5189
+ lines.push(`author: ${fm.author}`);
5155
5190
  lines.push(`source_sessions:`);
5156
5191
  for (const s of fm.source_sessions)
5157
5192
  lines.push(` - ${s}`);
5193
+ if (fm.contributors && fm.contributors.length > 0) {
5194
+ lines.push(`contributors:`);
5195
+ for (const c of fm.contributors)
5196
+ lines.push(` - ${c}`);
5197
+ }
5158
5198
  lines.push(`version: ${fm.version}`);
5159
5199
  lines.push(`created_by_agent: ${fm.created_by_agent}`);
5160
5200
  lines.push(`created_at: ${fm.created_at}`);
@@ -5194,10 +5234,19 @@ async function runPull(opts) {
5194
5234
  try {
5195
5235
  rows = await opts.query(sql);
5196
5236
  } catch (e) {
5197
- if (isMissingTableError(e?.message))
5237
+ if (isMissingTableError(e?.message)) {
5198
5238
  rows = [];
5199
- else
5239
+ } else if (isMissingContributorsColumnError(e?.message)) {
5240
+ const legacySql = buildPullSql({
5241
+ tableName: opts.tableName,
5242
+ users: opts.users,
5243
+ skillName: opts.skillName,
5244
+ includeContributors: false
5245
+ });
5246
+ rows = await opts.query(legacySql);
5247
+ } else {
5200
5248
  throw e;
5249
+ }
5201
5250
  }
5202
5251
  const latest = selectLatestPerName(rows);
5203
5252
  const root = resolvePullDestination(opts.install, opts.cwd);
@@ -5511,8 +5560,8 @@ function showStatus() {
5511
5560
  }
5512
5561
  }
5513
5562
  function setScope(scope) {
5514
- if (scope !== "me" && scope !== "team" && scope !== "org") {
5515
- console.error(`Invalid scope '${scope}'. Use one of: me, team, org`);
5563
+ if (scope !== "me" && scope !== "team") {
5564
+ console.error(`Invalid scope '${scope}'. Use one of: me, team`);
5516
5565
  process.exit(1);
5517
5566
  }
5518
5567
  const cfg = loadScopeConfig();
@@ -5591,7 +5640,7 @@ function teamList() {
5591
5640
  function usage() {
5592
5641
  console.log("Usage:");
5593
5642
  console.log(" hivemind skillify show current scope, team, install, and per-project state");
5594
- console.log(" hivemind skillify scope <me|team|org> set the mining scope");
5643
+ console.log(" hivemind skillify scope <me|team> set the mining scope");
5595
5644
  console.log(" hivemind skillify install <project|global> set where new skills are written");
5596
5645
  console.log(" hivemind skillify promote <skill-name> move a project skill to the global location");
5597
5646
  console.log(" hivemind skillify team add <username> add a username to the team list");
@@ -6041,7 +6090,7 @@ Skill management (mine + share reusable Claude skills across the org):
6041
6090
  --to <project|global>, --dry-run,
6042
6091
  --all (also locally-mined),
6043
6092
  --legacy-cleanup (pre-suffix-author dirs).
6044
- hivemind skillify scope <me|team|org> Set the sharing scope for newly mined skills.
6093
+ hivemind skillify scope <me|team> Set the sharing scope for newly mined skills.
6045
6094
  hivemind skillify install <project|global> Set where new skills are written.
6046
6095
  hivemind skillify promote <name> Move a project skill to the global location.
6047
6096
  hivemind skillify team add <username> Add a username to the team list.
@@ -653,18 +653,25 @@ function parseFrontmatter(text) {
653
653
  const head = text.slice(4, end).trim();
654
654
  const body = text.slice(end + 4).replace(/^\r?\n/, "");
655
655
  const fm = { source_sessions: [] };
656
- let mode = "kv";
656
+ let arrayKey = null;
657
657
  for (const raw of head.split(/\r?\n/)) {
658
- if (mode === "sources") {
658
+ if (arrayKey) {
659
659
  const m2 = raw.match(/^\s+-\s+(.+)$/);
660
660
  if (m2) {
661
- fm.source_sessions.push(m2[1].trim());
661
+ const arr = fm[arrayKey] ?? [];
662
+ arr.push(m2[1].trim());
663
+ fm[arrayKey] = arr;
662
664
  continue;
663
665
  }
664
- mode = "kv";
666
+ arrayKey = null;
665
667
  }
666
668
  if (raw.startsWith("source_sessions:")) {
667
- mode = "sources";
669
+ arrayKey = "source_sessions";
670
+ continue;
671
+ }
672
+ if (raw.startsWith("contributors:")) {
673
+ arrayKey = "contributors";
674
+ fm.contributors = [];
668
675
  continue;
669
676
  }
670
677
  const m = raw.match(/^([a-zA-Z_]+):\s*(.*)$/);
@@ -879,11 +886,19 @@ function buildPullSql(args) {
879
886
  where.push(`name = '${esc(args.skillName)}'`);
880
887
  }
881
888
  const whereClause = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
882
- return `SELECT name, project, project_key, body, version, source_agent, scope, author, description, trigger_text, source_sessions, install, created_at, updated_at FROM "${args.tableName}"${whereClause} ORDER BY project_key ASC, name ASC, version DESC`;
889
+ const contributorsCol = args.includeContributors === false ? "" : "contributors, ";
890
+ return `SELECT name, project, project_key, body, version, source_agent, scope, author, ${contributorsCol}description, trigger_text, source_sessions, install, created_at, updated_at FROM "${args.tableName}"${whereClause} ORDER BY project_key ASC, name ASC, version DESC`;
891
+ }
892
+ function isMissingContributorsColumnError(message) {
893
+ if (!message)
894
+ return false;
895
+ return /contributors.*(?:does not exist|not found|unknown)/i.test(message) || /(?:does not exist|unknown column).*contributors/i.test(message);
883
896
  }
884
897
  function isMissingTableError(message) {
885
898
  if (!message)
886
899
  return false;
900
+ if (/\bcolumn\b/i.test(message))
901
+ return false;
887
902
  return /Table does not exist|relation .* does not exist|no such table/i.test(message);
888
903
  }
889
904
  function resolvePullDestination(install, cwd) {
@@ -979,11 +994,16 @@ function selectLatestPerName(rows) {
979
994
  }
980
995
  function renderSkillFile(row) {
981
996
  const sources = parseSourceSessions(row.source_sessions);
997
+ const author = typeof row.author === "string" && row.author.length > 0 ? row.author : void 0;
998
+ const contributors = parseContributors(row.contributors);
999
+ const renderedContributors = contributors.length > 0 ? contributors : author ? [author] : [];
982
1000
  const fm = {
983
1001
  name: String(row.name ?? ""),
984
1002
  description: String(row.description ?? ""),
985
1003
  trigger: typeof row.trigger_text === "string" && row.trigger_text.length > 0 ? String(row.trigger_text) : void 0,
1004
+ author,
986
1005
  source_sessions: sources,
1006
+ contributors: renderedContributors,
987
1007
  version: Number(row.version ?? 1),
988
1008
  created_by_agent: String(row.source_agent ?? "unknown"),
989
1009
  created_at: String(row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
@@ -1008,15 +1028,35 @@ function parseSourceSessions(v) {
1008
1028
  }
1009
1029
  return [];
1010
1030
  }
1031
+ function parseContributors(v) {
1032
+ if (Array.isArray(v))
1033
+ return v.map(String);
1034
+ if (typeof v === "string") {
1035
+ try {
1036
+ const parsed = JSON.parse(v);
1037
+ if (Array.isArray(parsed))
1038
+ return parsed.map(String);
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ return [];
1043
+ }
1011
1044
  function renderFrontmatter(fm) {
1012
1045
  const lines = ["---"];
1013
1046
  lines.push(`name: ${fm.name}`);
1014
1047
  lines.push(`description: ${JSON.stringify(fm.description)}`);
1015
1048
  if (fm.trigger)
1016
1049
  lines.push(`trigger: ${JSON.stringify(fm.trigger)}`);
1050
+ if (fm.author)
1051
+ lines.push(`author: ${fm.author}`);
1017
1052
  lines.push(`source_sessions:`);
1018
1053
  for (const s of fm.source_sessions)
1019
1054
  lines.push(` - ${s}`);
1055
+ if (fm.contributors && fm.contributors.length > 0) {
1056
+ lines.push(`contributors:`);
1057
+ for (const c of fm.contributors)
1058
+ lines.push(` - ${c}`);
1059
+ }
1020
1060
  lines.push(`version: ${fm.version}`);
1021
1061
  lines.push(`created_by_agent: ${fm.created_by_agent}`);
1022
1062
  lines.push(`created_at: ${fm.created_at}`);
@@ -1056,10 +1096,19 @@ async function runPull(opts) {
1056
1096
  try {
1057
1097
  rows = await opts.query(sql);
1058
1098
  } catch (e) {
1059
- if (isMissingTableError(e?.message))
1099
+ if (isMissingTableError(e?.message)) {
1060
1100
  rows = [];
1061
- else
1101
+ } else if (isMissingContributorsColumnError(e?.message)) {
1102
+ const legacySql = buildPullSql({
1103
+ tableName: opts.tableName,
1104
+ users: opts.users,
1105
+ skillName: opts.skillName,
1106
+ includeContributors: false
1107
+ });
1108
+ rows = await opts.query(legacySql);
1109
+ } else {
1062
1110
  throw e;
1111
+ }
1063
1112
  }
1064
1113
  const latest = selectLatestPerName(rows);
1065
1114
  const root = resolvePullDestination(opts.install, opts.cwd);
@@ -1273,7 +1322,7 @@ SKILLS (skillify) \u2014 mine + share reusable skills across the org:
1273
1322
  - hivemind skillify unpull --user <email> \u2014 remove only that author's pulls
1274
1323
  - hivemind skillify unpull --not-mine \u2014 remove all pulls except your own
1275
1324
  - hivemind skillify unpull --dry-run \u2014 preview without touching disk
1276
- - hivemind skillify scope <me|team|org> \u2014 sharing scope for new skills
1325
+ - hivemind skillify scope <me|team> \u2014 sharing scope for new skills
1277
1326
  - hivemind skillify install <project|global> \u2014 default install location
1278
1327
  - hivemind skillify team add|remove|list <name> \u2014 manage team list`;
1279
1328
  async function main() {
@@ -90,9 +90,16 @@ function renderFrontmatter(fm) {
90
90
  lines.push(`description: ${JSON.stringify(fm.description)}`);
91
91
  if (fm.trigger)
92
92
  lines.push(`trigger: ${JSON.stringify(fm.trigger)}`);
93
+ if (fm.author)
94
+ lines.push(`author: ${fm.author}`);
93
95
  lines.push(`source_sessions:`);
94
96
  for (const s of fm.source_sessions)
95
97
  lines.push(` - ${s}`);
98
+ if (fm.contributors && fm.contributors.length > 0) {
99
+ lines.push(`contributors:`);
100
+ for (const c of fm.contributors)
101
+ lines.push(` - ${c}`);
102
+ }
96
103
  lines.push(`version: ${fm.version}`);
97
104
  lines.push(`created_by_agent: ${fm.created_by_agent}`);
98
105
  lines.push(`created_at: ${fm.created_at}`);
@@ -109,18 +116,25 @@ function parseFrontmatter(text) {
109
116
  const head = text.slice(4, end).trim();
110
117
  const body = text.slice(end + 4).replace(/^\r?\n/, "");
111
118
  const fm = { source_sessions: [] };
112
- let mode = "kv";
119
+ let arrayKey = null;
113
120
  for (const raw of head.split(/\r?\n/)) {
114
- if (mode === "sources") {
121
+ if (arrayKey) {
115
122
  const m2 = raw.match(/^\s+-\s+(.+)$/);
116
123
  if (m2) {
117
- fm.source_sessions.push(m2[1].trim());
124
+ const arr = fm[arrayKey] ?? [];
125
+ arr.push(m2[1].trim());
126
+ fm[arrayKey] = arr;
118
127
  continue;
119
128
  }
120
- mode = "kv";
129
+ arrayKey = null;
121
130
  }
122
131
  if (raw.startsWith("source_sessions:")) {
123
- mode = "sources";
132
+ arrayKey = "source_sessions";
133
+ continue;
134
+ }
135
+ if (raw.startsWith("contributors:")) {
136
+ arrayKey = "contributors";
137
+ fm.contributors = [];
124
138
  continue;
125
139
  }
126
140
  const m = raw.match(/^([a-zA-Z_]+):\s*(.*)$/);
@@ -151,11 +165,15 @@ function writeNewSkill(args) {
151
165
  }
152
166
  mkdirSync(dir, { recursive: true });
153
167
  const now = (/* @__PURE__ */ new Date()).toISOString();
168
+ const author = args.author && args.author.length > 0 ? args.author : void 0;
169
+ const contributors = author ? [author] : [];
154
170
  const fm = {
155
171
  name: args.name,
156
172
  description: args.description,
157
173
  trigger: args.trigger,
174
+ author,
158
175
  source_sessions: args.sourceSessions,
176
+ contributors,
159
177
  version: 1,
160
178
  created_by_agent: args.agent,
161
179
  created_at: now,
@@ -166,7 +184,15 @@ function writeNewSkill(args) {
166
184
  ${args.body.trim()}
167
185
  `;
168
186
  writeFileSync(path, text);
169
- return { path, action: "created", version: 1, createdAt: now, updatedAt: now };
187
+ return {
188
+ path,
189
+ action: "created",
190
+ version: 1,
191
+ createdAt: now,
192
+ updatedAt: now,
193
+ author,
194
+ contributors
195
+ };
170
196
  }
171
197
  function mergeSkill(args) {
172
198
  assertValidSkillName(args.name);
@@ -179,12 +205,20 @@ function mergeSkill(args) {
179
205
  const prevVersion = parsed?.fm.version ?? 1;
180
206
  const prevSources = parsed?.fm.source_sessions ?? [];
181
207
  const merged = Array.from(/* @__PURE__ */ new Set([...prevSources, ...args.newSourceSessions]));
208
+ const author = parsed?.fm.author;
209
+ const prevContribs = parsed?.fm.contributors && parsed.fm.contributors.length > 0 ? parsed.fm.contributors : author ? [author] : [];
210
+ const contributors = [...prevContribs];
211
+ if (args.editor && args.editor.length > 0 && !contributors.includes(args.editor)) {
212
+ contributors.push(args.editor);
213
+ }
182
214
  const now = (/* @__PURE__ */ new Date()).toISOString();
183
215
  const fm = {
184
216
  name: args.name,
185
217
  description: args.description ?? parsed?.fm.description ?? "",
186
218
  trigger: parsed?.fm.trigger,
219
+ author,
187
220
  source_sessions: merged,
221
+ contributors,
188
222
  version: prevVersion + 1,
189
223
  created_by_agent: parsed?.fm.created_by_agent ?? args.agent,
190
224
  created_at: parsed?.fm.created_at ?? now,
@@ -195,7 +229,15 @@ function mergeSkill(args) {
195
229
  ${args.body.trim()}
196
230
  `;
197
231
  writeFileSync(path, text);
198
- return { path, action: "merged", version: fm.version, createdAt: fm.created_at, updatedAt: fm.updated_at };
232
+ return {
233
+ path,
234
+ action: "merged",
235
+ version: fm.version,
236
+ createdAt: fm.created_at,
237
+ updatedAt: fm.updated_at,
238
+ author,
239
+ contributors
240
+ };
199
241
  }
200
242
  function listSkills(skillsRoot) {
201
243
  if (!existsSync(skillsRoot))
@@ -220,9 +262,14 @@ function resolveSkillsRoot(install, cwd) {
220
262
  function listAllExistingSkills(cwd) {
221
263
  const projectRoot = resolveSkillsRoot("project", cwd);
222
264
  const globalRoot = resolveSkillsRoot("global", cwd);
265
+ const tag = (source) => (s) => {
266
+ const parsed = parseFrontmatter(s.body);
267
+ const author = typeof parsed?.fm.author === "string" && parsed.fm.author.length > 0 ? parsed.fm.author : void 0;
268
+ return { name: s.name, body: s.body, source, author };
269
+ };
223
270
  const tagged = [
224
- ...listSkills(projectRoot).map((s) => ({ ...s, source: "project" })),
225
- ...listSkills(globalRoot).map((s) => ({ ...s, source: "global" }))
271
+ ...listSkills(projectRoot).map(tag("project")),
272
+ ...listSkills(globalRoot).map(tag("global"))
226
273
  ];
227
274
  const seen = /* @__PURE__ */ new Set();
228
275
  const out = [];
@@ -242,12 +289,13 @@ function renderExistingSkillsBlock(cwd, charCap) {
242
289
  block: "(no existing skills \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
243
290
  };
244
291
  }
245
- const mergeTargetNames = skills.filter((s) => s.source === "project").map((s) => s.name);
246
292
  let total = 0;
247
293
  const out = [];
294
+ const mergeTargetNames = [];
248
295
  for (const s of skills) {
249
- const tag = s.source === "project" ? "[project]" : "[global, read-only]";
250
- const block = `--- existing skill ${tag}: ${s.name} ---
296
+ const sourceTag = s.source === "project" ? "project" : "global, read-only";
297
+ const authorTag = s.author ? `, author=${s.author}` : "";
298
+ const block = `--- existing skill [${sourceTag}${authorTag}]: ${s.name} ---
251
299
  ${s.body}
252
300
  `;
253
301
  if (total + block.length > charCap) {
@@ -256,6 +304,8 @@ ${s.body}
256
304
  }
257
305
  out.push(block);
258
306
  total += block.length;
307
+ if (s.source === "project")
308
+ mergeTargetNames.push(s.name);
259
309
  }
260
310
  return { mergeTargetNames, block: out.join("\n") };
261
311
  }
@@ -274,7 +324,11 @@ function sqlIdent(name) {
274
324
  // dist/src/skillify/skills-table.js
275
325
  function createSkillsTableSql(tableName) {
276
326
  const safe = sqlIdent(tableName);
277
- return `CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`;
327
+ return `CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', contributors TEXT NOT NULL DEFAULT '[]', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`;
328
+ }
329
+ function addContributorsColumnSql(tableName) {
330
+ const safe = sqlIdent(tableName);
331
+ return `ALTER TABLE "${safe}" ADD COLUMN IF NOT EXISTS contributors TEXT NOT NULL DEFAULT '[]'`;
278
332
  }
279
333
  function esc(s) {
280
334
  return s.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
@@ -282,12 +336,20 @@ function esc(s) {
282
336
  function isMissingTableError(message) {
283
337
  if (!message)
284
338
  return false;
339
+ if (/\bcolumn\b/i.test(message))
340
+ return false;
285
341
  return /Table does not exist|relation .* does not exist|no such table/i.test(message);
286
342
  }
343
+ function isMissingContributorsColumnError(message) {
344
+ if (!message)
345
+ return false;
346
+ return /contributors.*(?:does not exist|not found|unknown)/i.test(message) || /(?:does not exist|unknown column).*contributors/i.test(message);
347
+ }
287
348
  async function insertSkillRow(args) {
288
349
  const id = args.id ?? randomUUID();
289
350
  const sourceSessionsJson = JSON.stringify(args.sourceSessions);
290
- const sql = `INSERT INTO "${sqlIdent(args.tableName)}" (id, name, project, project_key, local_path, install, source_sessions, source_agent, scope, author, description, trigger_text, body, version, created_at, updated_at) VALUES ('${esc(id)}', '${esc(args.name)}', '${esc(args.project)}', '${esc(args.projectKey)}', '${esc(args.localPath)}', '${esc(args.install)}', '${esc(sourceSessionsJson)}', '${esc(args.sourceAgent)}', '${esc(args.scope)}', '${esc(args.author)}', '${esc(args.description)}', '${esc(args.trigger ?? "")}', '${esc(args.body)}', ${args.version}, '${esc(args.createdAt)}', '${esc(args.updatedAt)}')`;
351
+ const contributorsJson = JSON.stringify(args.contributors);
352
+ const sql = `INSERT INTO "${sqlIdent(args.tableName)}" (id, name, project, project_key, local_path, install, source_sessions, source_agent, scope, author, contributors, description, trigger_text, body, version, created_at, updated_at) VALUES ('${esc(id)}', '${esc(args.name)}', '${esc(args.project)}', '${esc(args.projectKey)}', '${esc(args.localPath)}', '${esc(args.install)}', '${esc(sourceSessionsJson)}', '${esc(args.sourceAgent)}', '${esc(args.scope)}', '${esc(args.author)}', '${esc(contributorsJson)}', '${esc(args.description)}', '${esc(args.trigger ?? "")}', '${esc(args.body)}', ${args.version}, '${esc(args.createdAt)}', '${esc(args.updatedAt)}')`;
291
353
  try {
292
354
  await args.query(sql);
293
355
  } catch (e) {
@@ -296,6 +358,11 @@ async function insertSkillRow(args) {
296
358
  await args.query(sql);
297
359
  return;
298
360
  }
361
+ if (isMissingContributorsColumnError(e?.message)) {
362
+ await args.query(addContributorsColumnSql(args.tableName));
363
+ await args.query(sql);
364
+ return;
365
+ }
299
366
  throw e;
300
367
  }
301
368
  }
@@ -447,6 +514,14 @@ function runGate(opts) {
447
514
  }
448
515
  }
449
516
 
517
+ // dist/src/skillify/scope-promotion.js
518
+ function isCrossAuthorMergeVerdict(args) {
519
+ return args.verdict === "MERGE" && args.resultAuthor !== void 0 && args.resultAuthor !== args.userName;
520
+ }
521
+ function resolveRecordScope(args) {
522
+ return args.isCrossAuthorMerge && args.configScope === "me" ? "team" : args.configScope;
523
+ }
524
+
450
525
  // dist/src/skillify/state.js
451
526
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, writeSync, mkdirSync as mkdirSync2, renameSync as renameSync2, existsSync as existsSync4, unlinkSync, openSync, closeSync } from "node:fs";
452
527
  import { execSync } from "node:child_process";
@@ -652,8 +727,6 @@ async function query(sql, retries = 4) {
652
727
  return [];
653
728
  }
654
729
  function authorClause() {
655
- if (cfg.scope === "org")
656
- return "";
657
730
  if (cfg.scope === "team" && cfg.team.length > 0) {
658
731
  const list = cfg.team.map((n) => `'${esc2(n)}'`).join(", ");
659
732
  return ` AND author IN (${list})`;
@@ -721,7 +794,7 @@ ${answer}
721
794
  }
722
795
  function buildPrompt(pairs) {
723
796
  const existing = renderExistingSkillsBlock(cfg.cwd, EXISTING_SKILLS_CHAR_CAP);
724
- const mergeTargetsClause = existing.mergeTargetNames.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.mergeTargetNames.join(", ")}]. Any other name MUST use KEEP, not MERGE.` : `MERGE is FORBIDDEN \u2014 there are no project skills to merge into. Use KEEP or SKIP only.`;
797
+ const mergeTargetsClause = existing.mergeTargetNames.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.mergeTargetNames.join(", ")}]. Any other name MUST use KEEP, not MERGE.` : `MERGE is FORBIDDEN \u2014 there are no existing skills to merge into. Use KEEP or SKIP only.`;
725
798
  return [
726
799
  `You are a skill curator for the "${cfg.project}" project. You decide whether the recent`,
727
800
  `agent activity below contains a recurring, non-trivial pattern worth crystallizing as a`,
@@ -731,18 +804,19 @@ function buildPrompt(pairs) {
731
804
  `- KEEP only if the pattern recurs across at least 3 of the exchanges, is non-obvious to a`,
732
805
  ` competent engineer, and is not already covered by an existing skill below.`,
733
806
  `- SKIP if the activity is one-off, generic engineering work, or already covered.`,
734
- `- MERGE if the pattern is a meaningful extension of an existing PROJECT skill \u2014 produce a`,
807
+ `- MERGE if the pattern is a meaningful extension of an existing skill \u2014 produce a`,
735
808
  ` merged body that incorporates the new evidence without exceeding ~3000 characters or`,
736
809
  ` covering unrelated domains.`,
737
810
  `- ${mergeTargetsClause}`,
738
- `- Skills tagged [global, read-only] are autopulled from the team's shared skills`,
739
- ` table. They exist so you can recognise patterns already covered globally and pick`,
740
- ` SKIP (or a more specific KEEP) instead of duplicating them. They are NOT valid`,
741
- ` MERGE targets \u2014 only [project] skills can be merged into.`,
811
+ `- Cross-author MERGE has a real cost: editing a skill authored by someone else is`,
812
+ ` recorded as a team-level edit (scope=team, contributors+="${cfg.userName}"). Use it only`,
813
+ ` when the new evidence genuinely extends the existing skill; otherwise pick KEEP or SKIP.`,
814
+ ` Tags like [project, author=alice] / [global, author=bob] tell you whose skill it is.`,
742
815
  `- Skill bodies should follow the existing style: short sections (When to use, Workflow,`,
743
816
  ` Anti-patterns), concrete commands and file paths drawn from the exchanges, no marketing.`,
744
817
  ``,
745
- `=== EXISTING SKILLS ([project] are MERGE-eligible, [global] are reference only) ===`,
818
+ `=== EXISTING SKILLS (all MERGE-eligible; [global, author=X] entries from teammate X mean`,
819
+ `cross-author MERGE auto-promotes scope to team) ===`,
746
820
  existing.block,
747
821
  ``,
748
822
  `=== RECENT EXCHANGES (prompt + answer pairs, tool calls already stripped) ===`,
@@ -865,6 +939,17 @@ async function main() {
865
939
  const watermarkDate = oldest.lastMsg;
866
940
  const sourceSessions = usable.map((c) => (c.path.split("/").pop() ?? "").replace(/\.[^.]+$/, ""));
867
941
  async function recordToDeeplake(result, verdict2) {
942
+ const author = result.author ?? cfg.userName;
943
+ const isCrossAuthorMerge = isCrossAuthorMergeVerdict({
944
+ verdict: verdict2.verdict,
945
+ resultAuthor: result.author,
946
+ userName: cfg.userName
947
+ });
948
+ const scope = resolveRecordScope({
949
+ configScope: cfg.scope,
950
+ isCrossAuthorMerge
951
+ });
952
+ const contributors = result.contributors;
868
953
  try {
869
954
  await insertSkillRow({
870
955
  query,
@@ -876,8 +961,9 @@ async function main() {
876
961
  install: cfg.install,
877
962
  sourceSessions,
878
963
  sourceAgent: cfg.agent,
879
- scope: cfg.scope,
880
- author: cfg.userName,
964
+ scope,
965
+ author,
966
+ contributors,
881
967
  description: verdict2.description ?? "",
882
968
  trigger: verdict2.trigger,
883
969
  body: verdict2.body,
@@ -885,7 +971,7 @@ async function main() {
885
971
  createdAt: result.createdAt,
886
972
  updatedAt: result.updatedAt
887
973
  });
888
- wlog(`recorded to skills table: name=${verdict2.name} v${result.version}`);
974
+ wlog(`recorded to skills table: name=${verdict2.name} v${result.version} author=${author} scope=${scope} contributors=${contributors.length}` + (isCrossAuthorMerge ? " [auto-promoted me->team]" : ""));
889
975
  } catch (e) {
890
976
  wlog(`skills table insert failed (non-fatal): ${e.message}`);
891
977
  }
@@ -899,7 +985,8 @@ async function main() {
899
985
  trigger: verdict.trigger,
900
986
  body: verdict.body,
901
987
  sourceSessions,
902
- agent: cfg.agent
988
+ agent: cfg.agent,
989
+ author: cfg.userName
903
990
  });
904
991
  wlog(`wrote new skill: ${result.path}`);
905
992
  recordSkill(cfg.projectKey, verdict.name, watermarkUuid, watermarkDate);
@@ -916,7 +1003,8 @@ async function main() {
916
1003
  description: verdict.description,
917
1004
  body: verdict.body,
918
1005
  newSourceSessions: sourceSessions,
919
- agent: cfg.agent
1006
+ agent: cfg.agent,
1007
+ editor: cfg.userName
920
1008
  });
921
1009
  wlog(`merged into skill: ${result.path} (v${result.version})`);
922
1010
  recordSkill(cfg.projectKey, verdict.name, watermarkUuid, watermarkDate);
@@ -932,7 +1020,8 @@ async function main() {
932
1020
  trigger: verdict.trigger,
933
1021
  body: verdict.body,
934
1022
  sourceSessions,
935
- agent: cfg.agent
1023
+ agent: cfg.agent,
1024
+ author: cfg.userName
936
1025
  });
937
1026
  wlog(`wrote new skill (merge fallback): ${result.path}`);
938
1027
  recordSkill(cfg.projectKey, verdict.name, watermarkUuid, watermarkDate);