@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.
@@ -1024,7 +1024,7 @@ function loadScopeConfig() {
1024
1024
  return DEFAULT;
1025
1025
  try {
1026
1026
  const raw = JSON.parse(readFileSync5(CONFIG_PATH, "utf-8"));
1027
- const scope = raw.scope === "team" || raw.scope === "org" ? raw.scope : "me";
1027
+ const scope = raw.scope === "team" ? "team" : raw.scope === "org" ? "team" : "me";
1028
1028
  const team = Array.isArray(raw.team) ? raw.team.filter((s) => typeof s === "string") : [];
1029
1029
  const install = raw.install === "global" ? "global" : "project";
1030
1030
  return { scope, team, install };
@@ -1458,7 +1458,7 @@ function loadScopeConfig() {
1458
1458
  return DEFAULT;
1459
1459
  try {
1460
1460
  const raw = JSON.parse(readFileSync7(CONFIG_PATH, "utf-8"));
1461
- const scope = raw.scope === "team" || raw.scope === "org" ? raw.scope : "me";
1461
+ const scope = raw.scope === "team" ? "team" : raw.scope === "org" ? "team" : "me";
1462
1462
  const team = Array.isArray(raw.team) ? raw.team.filter((s) => typeof s === "string") : [];
1463
1463
  const install = raw.install === "global" ? "global" : "project";
1464
1464
  return { scope, team, install };
@@ -573,7 +573,7 @@ function loadScopeConfig() {
573
573
  return DEFAULT;
574
574
  try {
575
575
  const raw = JSON.parse(readFileSync5(CONFIG_PATH, "utf-8"));
576
- const scope = raw.scope === "team" || raw.scope === "org" ? raw.scope : "me";
576
+ const scope = raw.scope === "team" ? "team" : raw.scope === "org" ? "team" : "me";
577
577
  const team = Array.isArray(raw.team) ? raw.team.filter((s) => typeof s === "string") : [];
578
578
  const install = raw.install === "global" ? "global" : "project";
579
579
  return { scope, team, install };
@@ -705,18 +705,25 @@ function parseFrontmatter(text) {
705
705
  const head = text.slice(4, end).trim();
706
706
  const body = text.slice(end + 4).replace(/^\r?\n/, "");
707
707
  const fm = { source_sessions: [] };
708
- let mode = "kv";
708
+ let arrayKey = null;
709
709
  for (const raw of head.split(/\r?\n/)) {
710
- if (mode === "sources") {
710
+ if (arrayKey) {
711
711
  const m2 = raw.match(/^\s+-\s+(.+)$/);
712
712
  if (m2) {
713
- fm.source_sessions.push(m2[1].trim());
713
+ const arr = fm[arrayKey] ?? [];
714
+ arr.push(m2[1].trim());
715
+ fm[arrayKey] = arr;
714
716
  continue;
715
717
  }
716
- mode = "kv";
718
+ arrayKey = null;
717
719
  }
718
720
  if (raw.startsWith("source_sessions:")) {
719
- mode = "sources";
721
+ arrayKey = "source_sessions";
722
+ continue;
723
+ }
724
+ if (raw.startsWith("contributors:")) {
725
+ arrayKey = "contributors";
726
+ fm.contributors = [];
720
727
  continue;
721
728
  }
722
729
  const m = raw.match(/^([a-zA-Z_]+):\s*(.*)$/);
@@ -931,11 +938,19 @@ function buildPullSql(args) {
931
938
  where.push(`name = '${esc(args.skillName)}'`);
932
939
  }
933
940
  const whereClause = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
934
- 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`;
941
+ const contributorsCol = args.includeContributors === false ? "" : "contributors, ";
942
+ 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`;
943
+ }
944
+ function isMissingContributorsColumnError(message) {
945
+ if (!message)
946
+ return false;
947
+ return /contributors.*(?:does not exist|not found|unknown)/i.test(message) || /(?:does not exist|unknown column).*contributors/i.test(message);
935
948
  }
936
949
  function isMissingTableError(message) {
937
950
  if (!message)
938
951
  return false;
952
+ if (/\bcolumn\b/i.test(message))
953
+ return false;
939
954
  return /Table does not exist|relation .* does not exist|no such table/i.test(message);
940
955
  }
941
956
  function resolvePullDestination(install, cwd) {
@@ -1031,11 +1046,16 @@ function selectLatestPerName(rows) {
1031
1046
  }
1032
1047
  function renderSkillFile(row) {
1033
1048
  const sources = parseSourceSessions(row.source_sessions);
1049
+ const author = typeof row.author === "string" && row.author.length > 0 ? row.author : void 0;
1050
+ const contributors = parseContributors(row.contributors);
1051
+ const renderedContributors = contributors.length > 0 ? contributors : author ? [author] : [];
1034
1052
  const fm = {
1035
1053
  name: String(row.name ?? ""),
1036
1054
  description: String(row.description ?? ""),
1037
1055
  trigger: typeof row.trigger_text === "string" && row.trigger_text.length > 0 ? String(row.trigger_text) : void 0,
1056
+ author,
1038
1057
  source_sessions: sources,
1058
+ contributors: renderedContributors,
1039
1059
  version: Number(row.version ?? 1),
1040
1060
  created_by_agent: String(row.source_agent ?? "unknown"),
1041
1061
  created_at: String(row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
@@ -1060,15 +1080,35 @@ function parseSourceSessions(v) {
1060
1080
  }
1061
1081
  return [];
1062
1082
  }
1083
+ function parseContributors(v) {
1084
+ if (Array.isArray(v))
1085
+ return v.map(String);
1086
+ if (typeof v === "string") {
1087
+ try {
1088
+ const parsed = JSON.parse(v);
1089
+ if (Array.isArray(parsed))
1090
+ return parsed.map(String);
1091
+ } catch {
1092
+ }
1093
+ }
1094
+ return [];
1095
+ }
1063
1096
  function renderFrontmatter(fm) {
1064
1097
  const lines = ["---"];
1065
1098
  lines.push(`name: ${fm.name}`);
1066
1099
  lines.push(`description: ${JSON.stringify(fm.description)}`);
1067
1100
  if (fm.trigger)
1068
1101
  lines.push(`trigger: ${JSON.stringify(fm.trigger)}`);
1102
+ if (fm.author)
1103
+ lines.push(`author: ${fm.author}`);
1069
1104
  lines.push(`source_sessions:`);
1070
1105
  for (const s of fm.source_sessions)
1071
1106
  lines.push(` - ${s}`);
1107
+ if (fm.contributors && fm.contributors.length > 0) {
1108
+ lines.push(`contributors:`);
1109
+ for (const c of fm.contributors)
1110
+ lines.push(` - ${c}`);
1111
+ }
1072
1112
  lines.push(`version: ${fm.version}`);
1073
1113
  lines.push(`created_by_agent: ${fm.created_by_agent}`);
1074
1114
  lines.push(`created_at: ${fm.created_at}`);
@@ -1108,10 +1148,19 @@ async function runPull(opts) {
1108
1148
  try {
1109
1149
  rows = await opts.query(sql);
1110
1150
  } catch (e) {
1111
- if (isMissingTableError(e?.message))
1151
+ if (isMissingTableError(e?.message)) {
1112
1152
  rows = [];
1113
- else
1153
+ } else if (isMissingContributorsColumnError(e?.message)) {
1154
+ const legacySql = buildPullSql({
1155
+ tableName: opts.tableName,
1156
+ users: opts.users,
1157
+ skillName: opts.skillName,
1158
+ includeContributors: false
1159
+ });
1160
+ rows = await opts.query(legacySql);
1161
+ } else {
1114
1162
  throw e;
1163
+ }
1115
1164
  }
1116
1165
  const latest = selectLatestPerName(rows);
1117
1166
  const root = resolvePullDestination(opts.install, opts.cwd);
@@ -1314,7 +1363,7 @@ SKILLS (skillify) \u2014 mine + share reusable skills across the org:
1314
1363
  - hivemind skillify unpull --user <email> \u2014 remove only that author's pulls
1315
1364
  - hivemind skillify unpull --not-mine \u2014 remove all pulls except your own
1316
1365
  - hivemind skillify unpull --dry-run \u2014 preview without touching disk
1317
- - hivemind skillify scope <me|team|org> \u2014 sharing scope for new skills
1366
+ - hivemind skillify scope <me|team> \u2014 sharing scope for new skills
1318
1367
  - hivemind skillify install <project|global> \u2014 default install location
1319
1368
  - hivemind skillify team add|remove|list <name> \u2014 manage team list`;
1320
1369
  function resolveSessionId(input) {
@@ -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);
@@ -1458,7 +1458,7 @@ function loadScopeConfig() {
1458
1458
  return DEFAULT;
1459
1459
  try {
1460
1460
  const raw = JSON.parse(readFileSync7(CONFIG_PATH, "utf-8"));
1461
- const scope = raw.scope === "team" || raw.scope === "org" ? raw.scope : "me";
1461
+ const scope = raw.scope === "team" ? "team" : raw.scope === "org" ? "team" : "me";
1462
1462
  const team = Array.isArray(raw.team) ? raw.team.filter((s) => typeof s === "string") : [];
1463
1463
  const install = raw.install === "global" ? "global" : "project";
1464
1464
  return { scope, team, install };
@@ -572,7 +572,7 @@ function loadScopeConfig() {
572
572
  return DEFAULT;
573
573
  try {
574
574
  const raw = JSON.parse(readFileSync5(CONFIG_PATH, "utf-8"));
575
- const scope = raw.scope === "team" || raw.scope === "org" ? raw.scope : "me";
575
+ const scope = raw.scope === "team" ? "team" : raw.scope === "org" ? "team" : "me";
576
576
  const team = Array.isArray(raw.team) ? raw.team.filter((s) => typeof s === "string") : [];
577
577
  const install = raw.install === "global" ? "global" : "project";
578
578
  return { scope, team, install };
@@ -704,18 +704,25 @@ function parseFrontmatter(text) {
704
704
  const head = text.slice(4, end).trim();
705
705
  const body = text.slice(end + 4).replace(/^\r?\n/, "");
706
706
  const fm = { source_sessions: [] };
707
- let mode = "kv";
707
+ let arrayKey = null;
708
708
  for (const raw of head.split(/\r?\n/)) {
709
- if (mode === "sources") {
709
+ if (arrayKey) {
710
710
  const m2 = raw.match(/^\s+-\s+(.+)$/);
711
711
  if (m2) {
712
- fm.source_sessions.push(m2[1].trim());
712
+ const arr = fm[arrayKey] ?? [];
713
+ arr.push(m2[1].trim());
714
+ fm[arrayKey] = arr;
713
715
  continue;
714
716
  }
715
- mode = "kv";
717
+ arrayKey = null;
716
718
  }
717
719
  if (raw.startsWith("source_sessions:")) {
718
- mode = "sources";
720
+ arrayKey = "source_sessions";
721
+ continue;
722
+ }
723
+ if (raw.startsWith("contributors:")) {
724
+ arrayKey = "contributors";
725
+ fm.contributors = [];
719
726
  continue;
720
727
  }
721
728
  const m = raw.match(/^([a-zA-Z_]+):\s*(.*)$/);
@@ -930,11 +937,19 @@ function buildPullSql(args) {
930
937
  where.push(`name = '${esc(args.skillName)}'`);
931
938
  }
932
939
  const whereClause = where.length > 0 ? ` WHERE ${where.join(" AND ")}` : "";
933
- 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`;
940
+ const contributorsCol = args.includeContributors === false ? "" : "contributors, ";
941
+ 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`;
942
+ }
943
+ function isMissingContributorsColumnError(message) {
944
+ if (!message)
945
+ return false;
946
+ return /contributors.*(?:does not exist|not found|unknown)/i.test(message) || /(?:does not exist|unknown column).*contributors/i.test(message);
934
947
  }
935
948
  function isMissingTableError(message) {
936
949
  if (!message)
937
950
  return false;
951
+ if (/\bcolumn\b/i.test(message))
952
+ return false;
938
953
  return /Table does not exist|relation .* does not exist|no such table/i.test(message);
939
954
  }
940
955
  function resolvePullDestination(install, cwd) {
@@ -1030,11 +1045,16 @@ function selectLatestPerName(rows) {
1030
1045
  }
1031
1046
  function renderSkillFile(row) {
1032
1047
  const sources = parseSourceSessions(row.source_sessions);
1048
+ const author = typeof row.author === "string" && row.author.length > 0 ? row.author : void 0;
1049
+ const contributors = parseContributors(row.contributors);
1050
+ const renderedContributors = contributors.length > 0 ? contributors : author ? [author] : [];
1033
1051
  const fm = {
1034
1052
  name: String(row.name ?? ""),
1035
1053
  description: String(row.description ?? ""),
1036
1054
  trigger: typeof row.trigger_text === "string" && row.trigger_text.length > 0 ? String(row.trigger_text) : void 0,
1055
+ author,
1037
1056
  source_sessions: sources,
1057
+ contributors: renderedContributors,
1038
1058
  version: Number(row.version ?? 1),
1039
1059
  created_by_agent: String(row.source_agent ?? "unknown"),
1040
1060
  created_at: String(row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
@@ -1059,15 +1079,35 @@ function parseSourceSessions(v) {
1059
1079
  }
1060
1080
  return [];
1061
1081
  }
1082
+ function parseContributors(v) {
1083
+ if (Array.isArray(v))
1084
+ return v.map(String);
1085
+ if (typeof v === "string") {
1086
+ try {
1087
+ const parsed = JSON.parse(v);
1088
+ if (Array.isArray(parsed))
1089
+ return parsed.map(String);
1090
+ } catch {
1091
+ }
1092
+ }
1093
+ return [];
1094
+ }
1062
1095
  function renderFrontmatter(fm) {
1063
1096
  const lines = ["---"];
1064
1097
  lines.push(`name: ${fm.name}`);
1065
1098
  lines.push(`description: ${JSON.stringify(fm.description)}`);
1066
1099
  if (fm.trigger)
1067
1100
  lines.push(`trigger: ${JSON.stringify(fm.trigger)}`);
1101
+ if (fm.author)
1102
+ lines.push(`author: ${fm.author}`);
1068
1103
  lines.push(`source_sessions:`);
1069
1104
  for (const s of fm.source_sessions)
1070
1105
  lines.push(` - ${s}`);
1106
+ if (fm.contributors && fm.contributors.length > 0) {
1107
+ lines.push(`contributors:`);
1108
+ for (const c of fm.contributors)
1109
+ lines.push(` - ${c}`);
1110
+ }
1071
1111
  lines.push(`version: ${fm.version}`);
1072
1112
  lines.push(`created_by_agent: ${fm.created_by_agent}`);
1073
1113
  lines.push(`created_at: ${fm.created_at}`);
@@ -1107,10 +1147,19 @@ async function runPull(opts) {
1107
1147
  try {
1108
1148
  rows = await opts.query(sql);
1109
1149
  } catch (e) {
1110
- if (isMissingTableError(e?.message))
1150
+ if (isMissingTableError(e?.message)) {
1111
1151
  rows = [];
1112
- else
1152
+ } else if (isMissingContributorsColumnError(e?.message)) {
1153
+ const legacySql = buildPullSql({
1154
+ tableName: opts.tableName,
1155
+ users: opts.users,
1156
+ skillName: opts.skillName,
1157
+ includeContributors: false
1158
+ });
1159
+ rows = await opts.query(legacySql);
1160
+ } else {
1113
1161
  throw e;
1162
+ }
1114
1163
  }
1115
1164
  const latest = selectLatestPerName(rows);
1116
1165
  const root = resolvePullDestination(opts.install, opts.cwd);
@@ -1314,7 +1363,7 @@ SKILLS (skillify) \u2014 mine + share reusable skills across the org:
1314
1363
  - hivemind skillify unpull --user <email> \u2014 remove only that author's pulls
1315
1364
  - hivemind skillify unpull --not-mine \u2014 remove all pulls except your own
1316
1365
  - hivemind skillify unpull --dry-run \u2014 preview without touching disk
1317
- - hivemind skillify scope <me|team|org> \u2014 sharing scope for new skills
1366
+ - hivemind skillify scope <me|team> \u2014 sharing scope for new skills
1318
1367
  - hivemind skillify install <project|global> \u2014 default install location
1319
1368
  - hivemind skillify team add|remove|list <name> \u2014 manage team list`;
1320
1369
  async function createPlaceholder(api, table, sessionId, cwd, userName, orgName, workspaceId, pluginVersion) {