@theronap/cortex-mcp 0.9.73 → 0.9.74

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.
Files changed (2) hide show
  1. package/lib/skills.mjs +30 -2
  2. package/package.json +1 -1
package/lib/skills.mjs CHANGED
@@ -160,16 +160,37 @@ export function installSkills(opts = {}) {
160
160
  return summary
161
161
  }
162
162
 
163
- // Decide what to install from a served org-skill list: drop invalid names and bundled collisions
164
- // (bundled wins — core skills are inalterable). Pure, so it's unit-testable without a network.
163
+ // Decide what to install from a served org-skill list: drop invalid names, bundled collisions
164
+ // (bundled wins — core skills are inalterable) and cross-brain collisions. Pure, so it's
165
+ // unit-testable without a network.
165
166
  export function planOrgInstall(served, bundledNames) {
166
167
  const bundled = new Set(bundledNames)
167
168
  const install = []
168
169
  const skipped = []
170
+
171
+ // /api/skills now fans out across EVERY brain the caller belongs to (ADR-0022 acrossMyBrains), so
172
+ // for the first time two brains can serve the SAME skill name. Installing both would write one
173
+ // file twice and silently leave whichever landed last — an executable body from a brain the user
174
+ // never chose. There is no principled winner, so a cross-brain collision installs NEITHER and says
175
+ // so, matching how a bundled collision already resolves: when in doubt, do not install.
176
+ const byName = new Map()
177
+ for (const s of served ?? []) {
178
+ const n = (s?.name ?? '').trim()
179
+ if (!byName.has(n)) byName.set(n, [])
180
+ byName.get(n).push(s)
181
+ }
182
+
169
183
  for (const s of served ?? []) {
170
184
  const name = (s?.name ?? '').trim()
171
185
  if (!NAME_RE.test(name) || typeof s?.body_md !== 'string' || !s.body_md.trim()) { skipped.push({ name: name || '(unnamed)', why: 'invalid' }); continue }
172
186
  if (bundled.has(name)) { skipped.push({ name, why: 'collides with a bundled core skill' }); continue }
187
+ const dupes = byName.get(name) ?? []
188
+ if (dupes.length > 1) {
189
+ // Name the brains so the owner knows which to rename. `brain` is the tag acrossMyBrains adds.
190
+ const brains = [...new Set(dupes.map((d) => d?.brain).filter(Boolean))]
191
+ skipped.push({ name, why: `published by ${dupes.length} brains (${brains.join(', ') || 'unknown'}) — rename one; installing neither` })
192
+ continue
193
+ }
173
194
  install.push({ name, source: s.body_md })
174
195
  }
175
196
  return { install, skipped }
@@ -201,6 +222,13 @@ export async function syncOrgSkills(opts = {}) {
201
222
  if (res.ok) {
202
223
  served = (await res.json())?.skills ?? []
203
224
  summary.source = 'server'
225
+ } else {
226
+ // ⚠ DO NOT MAKE THIS SILENT AGAIN. `if (res.ok)` alone is how a 100% failure hid for weeks:
227
+ // /api/skills answered 409 to every multi-brain caller, this fell through to the cache below,
228
+ // and the only log line ('unreachable and no cache') does NOT print when a cache exists. So
229
+ // org skills silently stopped updating and nothing anywhere said so. Fail-soft is right —
230
+ // SessionStart must not break — but fail-soft is not fail-quiet.
231
+ log(` · org skills: server said ${res.status} — using last-good cache. Run \`doctor\` if this persists.`)
204
232
  }
205
233
  } catch { /* fall through to cache */ }
206
234
  if (!served && Array.isArray(cache?.skills)) { served = cache.skills; summary.source = 'cache' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.73",
3
+ "version": "0.9.74",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {