sinapse-ai 1.25.2 → 1.25.3

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.
@@ -1,7 +1,7 @@
1
1
  metadata:
2
2
  version: 1.0.0
3
- lastUpdated: '2026-07-15T08:35:27.084Z'
4
- entityCount: 817
3
+ lastUpdated: '2026-07-15T14:30:07.448Z'
4
+ entityCount: 818
5
5
  checksumAlgorithm: sha256
6
6
  resolutionRate: 100
7
7
  entities:
@@ -166,8 +166,8 @@ entities:
166
166
  score: 0.7
167
167
  constraints: []
168
168
  extensionPoints: []
169
- checksum: sha256:1f385982ca84a8232f3f6b1fce4257ca3971a5fd24830b5389477f9a5a8e943a
170
- lastVerified: '2026-07-15T08:12:47.676Z'
169
+ checksum: sha256:86db5a2578b6a0b4849fde4e0f4a197a41560a3a213b247f0edd8522d857ffea
170
+ lastVerified: '2026-07-15T13:41:11.702Z'
171
171
  local:
172
172
  path: bin/commands/local.js
173
173
  layer: L1
@@ -276,8 +276,8 @@ entities:
276
276
  score: 0.7
277
277
  constraints: []
278
278
  extensionPoints: []
279
- checksum: sha256:b51998a172a91e134330847185c078d317ed9be2da6746de3af18d209b7e08d9
280
- lastVerified: '2026-07-15T08:12:47.677Z'
279
+ checksum: sha256:05056e197ecb29e6f63a1d86d9d77d944e3083021bf632b48a76eb4cb2223b95
280
+ lastVerified: '2026-07-15T13:41:11.703Z'
281
281
  command-generator:
282
282
  path: bin/lib/command-generator.js
283
283
  layer: L1
@@ -299,6 +299,7 @@ entities:
299
299
  - constants
300
300
  - squads
301
301
  - fs-utils
302
+ - provider-contract
302
303
  externalDeps: []
303
304
  plannedDeps: []
304
305
  lifecycle: production
@@ -306,8 +307,8 @@ entities:
306
307
  score: 0.7
307
308
  constraints: []
308
309
  extensionPoints: []
309
- checksum: sha256:ed3f86d0349e4a709240728c0872bf44c3ebb2f1b8f3522c83e2f43c9b8c5647
310
- lastVerified: '2026-07-15T02:37:43.455Z'
310
+ checksum: sha256:837f10f3ce227b1f3ff9b3d4c9e824e8928cd67ec07b0dcebd78e85468eed003
311
+ lastVerified: '2026-07-15T13:41:11.703Z'
311
312
  constants:
312
313
  path: bin/lib/constants.js
313
314
  layer: L1
@@ -413,7 +414,8 @@ entities:
413
414
  usedBy:
414
415
  - install
415
416
  - update
416
- dependencies: []
417
+ dependencies:
418
+ - provider-contract
417
419
  externalDeps: []
418
420
  plannedDeps: []
419
421
  lifecycle: production
@@ -421,8 +423,8 @@ entities:
421
423
  score: 0.7
422
424
  constraints: []
423
425
  extensionPoints: []
424
- checksum: sha256:d28b19935a9c75408df9da7430fc3ca9568bdea0cfc1ae3c01d0025ad19fc2a3
425
- lastVerified: '2026-07-15T07:09:56.286Z'
426
+ checksum: sha256:898fbe5a00cf1bb77bc8e8a34ee40337145912374a2e809789a02d5345fdd679
427
+ lastVerified: '2026-07-15T14:30:07.273Z'
426
428
  header:
427
429
  path: bin/lib/header.js
428
430
  layer: L1
@@ -475,6 +477,28 @@ entities:
475
477
  extensionPoints: []
476
478
  checksum: sha256:dc7cc1109f2bbdb7b3c5927df0de2418925842bdacdc0ba82815bfa2783d0b42
477
479
  lastVerified: '2026-06-22T02:41:18.152Z'
480
+ provider-contract:
481
+ path: bin/lib/provider-contract.js
482
+ layer: L1
483
+ type: script
484
+ purpose: Entity at bin\lib\provider-contract.js
485
+ keywords:
486
+ - provider
487
+ - contract
488
+ usedBy:
489
+ - command-generator
490
+ - global-provider-adapters
491
+ - provider-parity
492
+ dependencies: []
493
+ externalDeps: []
494
+ plannedDeps: []
495
+ lifecycle: production
496
+ adaptability:
497
+ score: 0.7
498
+ constraints: []
499
+ extensionPoints: []
500
+ checksum: sha256:a18d271a73b2f371213af4d9503e302e9237920057afd521b723c96efd28fda0
501
+ lastVerified: '2026-07-15T13:41:11.704Z'
478
502
  provider-parity:
479
503
  path: bin/lib/provider-parity.js
480
504
  layer: L1
@@ -486,7 +510,8 @@ entities:
486
510
  usedBy:
487
511
  - install
488
512
  - update
489
- dependencies: []
513
+ dependencies:
514
+ - provider-contract
490
515
  externalDeps: []
491
516
  plannedDeps: []
492
517
  lifecycle: production
@@ -494,8 +519,8 @@ entities:
494
519
  score: 0.7
495
520
  constraints: []
496
521
  extensionPoints: []
497
- checksum: sha256:592206326a2a3c5a3d89068d9ef065d698562aef40e6096d971fb793cea11c97
498
- lastVerified: '2026-07-15T08:35:26.859Z'
522
+ checksum: sha256:5f0ff4517425fef01683f035afef1d139aede2fe47956773a398e7c761ace42d
523
+ lastVerified: '2026-07-15T13:41:11.704Z'
499
524
  setup-statusline:
500
525
  path: bin/lib/setup-statusline.js
501
526
  layer: L1
@@ -7,7 +7,7 @@
7
7
  # - SHA256 hashes for change detection
8
8
  # - File types for categorization
9
9
  #
10
- version: 1.25.2
10
+ version: 1.25.3
11
11
  generator: scripts/generate-install-manifest.js
12
12
  file_count: 1156
13
13
  files:
@@ -1368,9 +1368,9 @@ files:
1368
1368
  type: data
1369
1369
  size: 9671
1370
1370
  - path: data/entity-registry.yaml
1371
- hash: sha256:ed0919b2bc3479beb4e46d3ccfbbbda66e46161c7eebc9018b9f0c8e5278d7f1
1371
+ hash: sha256:08edc9d3cde1bf956cce462004670a3d2924a691a16c27bdfd2bf1ce4de037f8
1372
1372
  type: data
1373
- size: 559991
1373
+ size: 560701
1374
1374
  - path: data/learned-patterns.yaml
1375
1375
  hash: sha256:1a4cd045c087b9dfd7046ff1464a9d2edb85fba77cf0b6fba14f4bb9004c741e
1376
1376
  type: data
package/CHANGELOG.md CHANGED
@@ -1,16 +1,8 @@
1
- ## [1.25.2](https://github.com/caioimori/sinapse-ai/compare/1.25.1...1.25.2) (2026-07-15)
1
+ ## [1.25.3](https://github.com/caioimori/sinapse-ai/compare/1.25.2...1.25.3) (2026-07-15)
2
2
 
3
3
  ### Bug Fixes
4
4
 
5
- * **docs:** align public Claude and Codex guidance [Story Cross-Provider] ([8c005ec](https://github.com/caioimori/sinapse-ai/commit/8c005ec4f0c5702131564cb5d3d52fee8ed85b50))
6
- * **docs:** close public provider guidance gaps [Story Cross-Provider] ([43f76e2](https://github.com/caioimori/sinapse-ai/commit/43f76e2b405ffae61598d8999f043a1d24ea24a5))
7
-
8
- ### Documentation
9
-
10
- * harden provider backup guidance [Story cross-provider-cli-quality] ([5f4bffd](https://github.com/caioimori/sinapse-ai/commit/5f4bffd351c7ac1c0ee5cde2d7388e34409e0566))
11
- * **story:** normalize validation terminology [Story sinapse-only-catalog-cleanup] ([7e0ecb9](https://github.com/caioimori/sinapse-ai/commit/7e0ecb9fb01a6626ccc7c665d262c38c61196120))
12
- * **story:** reconcile post-release evidence [Story sinapse-only-catalog-cleanup] ([3c10c65](https://github.com/caioimori/sinapse-ai/commit/3c10c65fcfdca77ec0d1c1af6a2831af39b0adf0))
13
- * **story:** record SINAPSE 1.25.1 release [Story sinapse-only-catalog-cleanup] ([ba9f776](https://github.com/caioimori/sinapse-ai/commit/ba9f77607879c6efe8511e38cfeab6c71d2335c4))
5
+ * **installer:** restore provider adapter parity ([#385](https://github.com/caioimori/sinapse-ai/issues/385)) ([4c5c9fb](https://github.com/caioimori/sinapse-ai/commit/4c5c9fb1a60e5d8610da08f25f6f01a67a85a063))
14
6
 
15
7
  # Changelog
16
8
 
@@ -560,7 +560,7 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
560
560
  // Phase 2: Generate agent commands (shared with `update` via command-generator).
561
561
  logger.always(`\n${CYAN}Phase 2:${NC} Generating agent commands`);
562
562
  const commandStagingDir = getGlobalCommandStagingDir({ llmChoice, sinapseHome: SINAPSE_HOME, claudeCommandsDir: CLAUDE_COMMANDS_DIR });
563
- const { writtenAgents, totalAgents } = regenerateAgentCommands({
563
+ const { writtenAgents, canonicalAgents } = regenerateAgentCommands({
564
564
  sinapseHome: SINAPSE_HOME,
565
565
  commandsDir: commandStagingDir,
566
566
  squads,
@@ -571,7 +571,11 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
571
571
 
572
572
  // Phase 2b: Install global agents based on LLM choice
573
573
  const globalAdapters = deliverGlobalProviderAdapters({ llmChoice, home: HOME, commandsDir: commandStagingDir });
574
- const activeAgentCount = assertProviderAdapterParity(llmChoice, globalAdapters, totalAgents);
574
+ const activeAgentCount = assertProviderAdapterParity(
575
+ llmChoice,
576
+ globalAdapters,
577
+ canonicalAgents,
578
+ );
575
579
  const installedAgentFilenames = new Set([...globalAdapters.claude, ...globalAdapters.codex]);
576
580
  const installedIdes = [];
577
581
  if (globalAdapters.claude.length) installedIdes.push('claude-code');
@@ -170,7 +170,7 @@ async function cmdUpdateGlobal() {
170
170
  logger.always(`\n${CYAN}Phase 2:${NC} Regenerating commands`);
171
171
  const commandStagingDir = getGlobalCommandStagingDir({ llmChoice, sinapseHome: SINAPSE_HOME, claudeCommandsDir: CLAUDE_COMMANDS_DIR });
172
172
  rmDirSync(commandStagingDir);
173
- const { writtenAgents, totalAgents } = regenerateAgentCommands({
173
+ const { writtenAgents, canonicalAgents, totalAgents } = regenerateAgentCommands({
174
174
  sinapseHome: SINAPSE_HOME,
175
175
  commandsDir: commandStagingDir,
176
176
  squads,
@@ -181,7 +181,11 @@ async function cmdUpdateGlobal() {
181
181
 
182
182
  // Phase 2b: Install global agents based on LLM choice
183
183
  const globalAdapters = deliverGlobalProviderAdapters({ llmChoice, home: HOME, commandsDir: commandStagingDir });
184
- const activeAgentCount = assertProviderAdapterParity(llmChoice, globalAdapters, totalAgents);
184
+ const activeAgentCount = assertProviderAdapterParity(
185
+ llmChoice,
186
+ globalAdapters,
187
+ canonicalAgents,
188
+ );
185
189
  if (globalAdapters.claude.length) logger.always(` ${GREEN}OK${NC} Claude Code global agents (${globalAdapters.claude.length})`);
186
190
  if (globalAdapters.codex.length) logger.always(` ${GREEN}OK${NC} Codex global agents (${globalAdapters.codex.length} TOML, ${globalAdapters.skills.length} skills)`);
187
191
 
@@ -20,6 +20,7 @@ const { toForwardSlash, atomicWriteFileSync } = require('./fs-utils');
20
20
  // sync-counts uses so the master-stub headline can never drift from README/persona
21
21
  // (Constitution Article VII). Fails open to 0 if the dir is unavailable.
22
22
  const CORE_AGENTS_DIR = path.join(ROOT, '.sinapse-ai', 'development', 'agents');
23
+ const { MASTER_ALIAS_ENTRY_POINTS } = require('./provider-contract');
23
24
 
24
25
  function countCoreAgents(dir = CORE_AGENTS_DIR) {
25
26
  try {
@@ -207,7 +208,7 @@ If the task requires expertise outside this squad:
207
208
  * @param {string} [deps.commandsDir] - Target commands dir.
208
209
  * @param {Array} deps.squads - Squad descriptors ({ name, agents, ... }).
209
210
  * @param {string} [deps.sinapseMasterDest] - Path to the installed sinapse/ master squad.
210
- * @returns {{ writtenAgents: Set<string>, totalAgents: number }}
211
+ * @returns {{ writtenAgents: Set<string>, canonicalAgents: Array<{ id: string, file: string, origin: string }>, aliasEntryPoints: string[], totalAgents: number }}
211
212
  */
212
213
  function regenerateAgentCommands(deps = {}) {
213
214
  const {
@@ -222,20 +223,21 @@ function regenerateAgentCommands(deps = {}) {
222
223
  throw new Error('regenerateAgentCommands: `squads` array is required');
223
224
  }
224
225
 
225
- fs.mkdirSync(commandsDir, { recursive: true });
226
-
227
- // Clear old commands (dir may not exist on a fresh install — guarded).
228
- try {
229
- for (const f of fs.readdirSync(commandsDir)) {
230
- fs.unlinkSync(path.join(commandsDir, f));
231
- }
232
- } catch { /* dir may not exist yet */ }
233
-
234
226
  const sinapseBase = toForwardSlash(sinapseHome);
235
227
  const writtenAgents = new Set();
228
+ const catalogById = new Map();
229
+ const definitions = [];
230
+
231
+ function registerDefinition(definition) {
232
+ const previous = catalogById.get(definition.id);
233
+ if (previous) {
234
+ throw new Error(`Duplicate canonical agent ID "${definition.id}" from ${previous.origin} and ${definition.origin}`);
235
+ }
236
+ catalogById.set(definition.id, definition);
237
+ definitions.push(definition);
238
+ }
236
239
 
237
- // 1. A command/subagent for EVERY agent (not only -orqx), so every specialist
238
- // is invokable via @agent and /SINAPSE:agents:agent.
240
+ // Inventory every canonical definition before clearing or writing commands.
239
241
  for (const squad of squads) {
240
242
  const squadPath = `${sinapseBase}/${squad.name}`;
241
243
  const agentsDir = path.join(sinapseHome, squad.name, 'agents');
@@ -244,10 +246,14 @@ function regenerateAgentCommands(deps = {}) {
244
246
  const squadAgents = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
245
247
  for (const file of squadAgents) {
246
248
  const agentId = file.replace('.md', '');
247
- const meta = extractAgentMeta(path.join(agentsDir, file));
248
- const cmdContent = generateCommandMd(agentId, meta.name, meta.icon, squad.name, squadPath, file);
249
- atomicWriteFileSync(path.join(commandsDir, `${agentId}.md`), cmdContent);
250
- writtenAgents.add(file);
249
+ registerDefinition({
250
+ id: agentId,
251
+ file,
252
+ origin: path.join(agentsDir, file),
253
+ squadName: squad.name,
254
+ squadPath,
255
+ hasManifest: true,
256
+ });
251
257
  }
252
258
  }
253
259
 
@@ -257,29 +263,37 @@ function regenerateAgentCommands(deps = {}) {
257
263
  if (fs.existsSync(coreAgentsDir)) {
258
264
  const coreBase = toForwardSlash(coreDevelopmentDest);
259
265
  for (const file of fs.readdirSync(coreAgentsDir).filter(f => f.endsWith('.md'))) {
260
- if (file === 'README.md' || file === 'MEMORY.md' || file.startsWith('_') || writtenAgents.has(file)) continue;
266
+ if (file === 'README.md' || file === 'MEMORY.md' || file.startsWith('_')) continue;
261
267
  const agentId = file.replace('.md', '');
262
- const meta = extractAgentMeta(path.join(coreAgentsDir, file));
263
- const cmdContent = generateCommandMd(agentId, meta.name, meta.icon, 'core', coreBase, file, { hasManifest: false });
264
- atomicWriteFileSync(path.join(commandsDir, `${agentId}.md`), cmdContent);
265
- writtenAgents.add(file);
268
+ registerDefinition({
269
+ id: agentId,
270
+ file,
271
+ origin: path.join(coreAgentsDir, file),
272
+ squadName: 'core',
273
+ squadPath: coreBase,
274
+ hasManifest: false,
275
+ });
266
276
  }
267
277
  }
268
278
 
269
- // 2. Commands for sinapse/ master squad agents.
270
- if (fs.existsSync(sinapseMasterDest)) {
271
- const masterAgentsDir = path.join(sinapseMasterDest, 'agents');
272
- if (fs.existsSync(masterAgentsDir)) {
273
- for (const file of fs.readdirSync(masterAgentsDir).filter(f => f.endsWith('.md'))) {
274
- if (writtenAgents.has(file)) continue;
275
- const agentId = file.replace('.md', '');
276
- const meta = extractAgentMeta(path.join(masterAgentsDir, file));
277
- const squadPath = `${sinapseBase}/sinapse`;
278
- const cmdContent = generateCommandMd(agentId, meta.name, meta.icon, 'sinapse', squadPath, file);
279
- atomicWriteFileSync(path.join(commandsDir, `${agentId}.md`), cmdContent);
280
- writtenAgents.add(file);
281
- }
282
- }
279
+ fs.mkdirSync(commandsDir, { recursive: true });
280
+ for (const file of fs.readdirSync(commandsDir)) {
281
+ fs.unlinkSync(path.join(commandsDir, file));
282
+ }
283
+
284
+ for (const definition of definitions) {
285
+ const meta = extractAgentMeta(definition.origin);
286
+ const cmdContent = generateCommandMd(
287
+ definition.id,
288
+ meta.name,
289
+ meta.icon,
290
+ definition.squadName,
291
+ definition.squadPath,
292
+ definition.file,
293
+ { hasManifest: definition.hasManifest },
294
+ );
295
+ atomicWriteFileSync(path.join(commandsDir, definition.file), cmdContent);
296
+ writtenAgents.add(definition.file);
283
297
  }
284
298
 
285
299
  // 3. Master entry points (@sinapse, @snps, @sinapse-orqx, @snps-orqx) → rich
@@ -301,7 +315,16 @@ function regenerateAgentCommands(deps = {}) {
301
315
  }
302
316
  }
303
317
 
304
- return { writtenAgents, totalAgents: writtenAgents.size };
318
+ const aliasEntryPoints = [...MASTER_ALIAS_ENTRY_POINTS]
319
+ .filter((id) => writtenAgents.has(`${id}.md`));
320
+ const canonicalAgents = definitions.map(({ id, file, origin }) => ({ id, file, origin }));
321
+
322
+ return {
323
+ writtenAgents,
324
+ canonicalAgents,
325
+ aliasEntryPoints,
326
+ totalAgents: canonicalAgents.length,
327
+ };
305
328
  }
306
329
 
307
330
  module.exports = {
@@ -309,4 +332,5 @@ module.exports = {
309
332
  generateMasterStub,
310
333
  regenerateAgentCommands,
311
334
  countCoreAgents,
335
+ MASTER_ALIAS_ENTRY_POINTS,
312
336
  };
@@ -2,17 +2,73 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const crypto = require('crypto');
6
+ const {
7
+ MASTER_ALIAS_ENTRY_POINTS,
8
+ GLOBAL_PROVIDER_SKILL_IDS,
9
+ SUPREME_ORCHESTRATOR_ID,
10
+ SUPREME_PUBLIC_ID,
11
+ } = require('./provider-contract');
5
12
 
6
13
  const NOFOLLOW_READ_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
7
14
 
15
+ function isPathInside(root, candidate) {
16
+ const relative = path.relative(root, candidate);
17
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
18
+ }
19
+
20
+ function ensureSafeDirectoryWithinHome(home, directory) {
21
+ const absoluteHome = path.resolve(home);
22
+ const absoluteDirectory = path.resolve(directory);
23
+ if (!isPathInside(absoluteHome, absoluteDirectory)) {
24
+ throw new Error(`Refusing provider write outside HOME: ${absoluteDirectory}`);
25
+ }
26
+ fs.mkdirSync(absoluteHome, { recursive: true });
27
+ const homeStat = fs.lstatSync(absoluteHome);
28
+ if (homeStat.isSymbolicLink() || !homeStat.isDirectory()) {
29
+ throw new Error(`Refusing provider write through unsafe HOME: ${absoluteHome}`);
30
+ }
31
+ const realHome = fs.realpathSync.native(absoluteHome);
32
+ let current = absoluteHome;
33
+ const relative = path.relative(absoluteHome, absoluteDirectory);
34
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
35
+ current = path.join(current, segment);
36
+ if (!fs.existsSync(current)) fs.mkdirSync(current);
37
+ const stat = fs.lstatSync(current);
38
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
39
+ throw new Error(`Refusing provider write through link/reparse path: ${current}`);
40
+ }
41
+ const realCurrent = fs.realpathSync.native(current);
42
+ if (!isPathInside(realHome, realCurrent)) {
43
+ throw new Error(`Refusing provider write outside HOME through reparse path: ${current}`);
44
+ }
45
+ }
46
+ return absoluteDirectory;
47
+ }
48
+
49
+ function assertSafeFileWithinHome(home, filePath) {
50
+ ensureSafeDirectoryWithinHome(home, path.dirname(filePath));
51
+ if (fs.existsSync(filePath)) {
52
+ const stat = fs.lstatSync(filePath);
53
+ if (stat.isSymbolicLink() || !stat.isFile()) {
54
+ throw new Error(`Refusing provider write through unsafe file: ${filePath}`);
55
+ }
56
+ return stat;
57
+ }
58
+ return null;
59
+ }
60
+
8
61
  function readRegularFileNoFollowSync(filePath, encoding = null) {
9
62
  let handle;
10
63
  try {
11
64
  handle = fs.openSync(filePath, NOFOLLOW_READ_FLAGS);
12
- if (!fs.fstatSync(handle).isFile()) return null;
65
+ const opened = fs.fstatSync(handle);
66
+ const after = fs.lstatSync(filePath);
67
+ if (!opened.isFile() || after.isSymbolicLink() || !after.isFile()) return null;
68
+ if (after.dev !== opened.dev || after.ino !== opened.ino) return null;
13
69
  return fs.readFileSync(handle, encoding || undefined);
14
70
  } catch (error) {
15
- if (['ENOENT', 'ELOOP', 'EISDIR'].includes(error.code)) return null;
71
+ if (['ENOENT', 'ELOOP', 'EISDIR', 'EINVAL', 'UNKNOWN'].includes(error.code)) return null;
16
72
  throw error;
17
73
  } finally {
18
74
  if (handle !== undefined) fs.closeSync(handle);
@@ -36,8 +92,11 @@ function renderCodexToml(name, description, instructions) {
36
92
 
37
93
  function markGlobalAgent(markdown, fileName = '') {
38
94
  let content = markdown;
39
- if (fileName === 'snps-orqx.md') {
40
- content = content.replace(/^name:\s*snps-orqx\s*$/m, 'name: sinapse-orqx');
95
+ if (fileName === `${SUPREME_ORCHESTRATOR_ID}.md`) {
96
+ content = content.replace(
97
+ new RegExp(`^name:\\s*${SUPREME_ORCHESTRATOR_ID}\\s*$`, 'm'),
98
+ `name: ${SUPREME_PUBLIC_ID}`,
99
+ );
41
100
  }
42
101
  if (content.includes('SINAPSE-MANAGED:global-agent')) return content;
43
102
  return `${content.trimEnd()}\n\n<!-- SINAPSE-MANAGED:global-agent -->\n`;
@@ -54,137 +113,273 @@ function renderGlobalSkill(skillId) {
54
113
  generic ? '# SINAPSE Global Agent Activator' : '# SINAPSE Global Orchestrator', '',
55
114
  generic
56
115
  ? 'Resolve the requested ID through the native agent catalog of the active provider; reject unknown IDs without guessing.'
57
- : 'Resolve this alias to the `sinapse-orqx` custom agent and preserve its delegation-only authority.',
116
+ : `Activate the public adapter \`${SUPREME_PUBLIC_ID}\`, backed by canonical source ID \`${SUPREME_ORCHESTRATOR_ID}\`, and preserve its delegation-only authority.`,
58
117
  'Canonical global persona and task sources live under `~/.sinapse/`.',
59
118
  'Use native Codex delegation and never start a nested CLI.', '',
60
119
  ].join('\n');
61
120
  }
62
121
 
63
- function writeFileAtomically(filePath, content) {
64
- const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
122
+ function captureParentBinding(home, parentPath) {
123
+ ensureSafeDirectoryWithinHome(home, parentPath);
124
+ const handle = fs.openSync(parentPath, fs.constants.O_RDONLY);
125
+ const opened = fs.fstatSync(handle);
126
+ const current = fs.lstatSync(parentPath);
127
+ if (!opened.isDirectory() || current.isSymbolicLink() || !current.isDirectory()
128
+ || opened.dev !== current.dev || opened.ino !== current.ino) {
129
+ fs.closeSync(handle);
130
+ throw new Error(`Refusing provider write through unstable parent: ${parentPath}`);
131
+ }
132
+ return { handle, dev: opened.dev, ino: opened.ino, realPath: fs.realpathSync.native(parentPath) };
133
+ }
134
+
135
+ function assertParentBinding(home, parentPath, binding) {
136
+ ensureSafeDirectoryWithinHome(home, parentPath);
137
+ const current = fs.lstatSync(parentPath);
138
+ const opened = fs.fstatSync(binding.handle);
139
+ const realPath = fs.realpathSync.native(parentPath);
140
+ if (current.isSymbolicLink() || !current.isDirectory() || !opened.isDirectory()
141
+ || current.dev !== binding.dev || current.ino !== binding.ino
142
+ || opened.dev !== binding.dev || opened.ino !== binding.ino
143
+ || realPath !== binding.realPath) {
144
+ throw new Error(`Refusing provider publish after parent path changed: ${parentPath}`);
145
+ }
146
+ }
147
+
148
+ function unlinkFileWithBinding(home, filePath, binding, expected, beforeDelete) {
149
+ if (typeof beforeDelete === 'function') beforeDelete({ filePath, parentPath: path.dirname(filePath) });
150
+ assertParentBinding(home, path.dirname(filePath), binding);
151
+ let current;
152
+ try {
153
+ current = fs.lstatSync(filePath);
154
+ } catch (error) {
155
+ if (error.code === 'ENOENT') return false;
156
+ throw error;
157
+ }
158
+ if (current.isSymbolicLink() || !current.isFile()
159
+ || current.dev !== expected.dev || current.ino !== expected.ino) {
160
+ throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
161
+ }
162
+ fs.unlinkSync(filePath);
163
+ assertParentBinding(home, path.dirname(filePath), binding);
164
+ return true;
165
+ }
166
+
167
+ function writeFileAtomically(filePath, content, home = path.dirname(filePath), options = {}) {
168
+ const destinationIdentity = assertSafeFileWithinHome(home, filePath);
169
+ const parentPath = path.dirname(filePath);
170
+ const binding = captureParentBinding(home, parentPath);
171
+ const temporaryPath = path.join(
172
+ parentPath,
173
+ `.${path.basename(filePath)}.${crypto.randomBytes(24).toString('hex')}.tmp`,
174
+ );
65
175
  let handle;
176
+ let temporaryIdentity;
177
+ let temporaryExists = false;
66
178
  try {
67
179
  handle = fs.openSync(temporaryPath, 'wx', 0o600);
180
+ temporaryIdentity = fs.fstatSync(handle);
181
+ temporaryExists = true;
68
182
  fs.writeFileSync(handle, content, 'utf8');
69
183
  fs.fsyncSync(handle);
70
184
  fs.closeSync(handle);
71
185
  handle = undefined;
72
- fs.renameSync(temporaryPath, filePath);
186
+ if (typeof options.beforePublish === 'function') {
187
+ options.beforePublish({ filePath, parentPath, temporaryPath });
188
+ }
189
+ assertParentBinding(home, parentPath, binding);
190
+ if (options.exclusive) {
191
+ fs.linkSync(temporaryPath, filePath);
192
+ assertParentBinding(home, parentPath, binding);
193
+ unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
194
+ temporaryExists = false;
195
+ } else {
196
+ const currentDestination = assertSafeFileWithinHome(home, filePath);
197
+ if (destinationIdentity && (!currentDestination
198
+ || currentDestination.dev !== destinationIdentity.dev
199
+ || currentDestination.ino !== destinationIdentity.ino)) {
200
+ throw new Error(`Refusing provider overwrite after file identity changed: ${filePath}`);
201
+ }
202
+ if (!destinationIdentity && currentDestination) {
203
+ throw new Error(`Refusing provider overwrite of a newly appeared file: ${filePath}`);
204
+ }
205
+ assertParentBinding(home, parentPath, binding);
206
+ fs.renameSync(temporaryPath, filePath);
207
+ temporaryExists = false;
208
+ }
209
+ assertParentBinding(home, parentPath, binding);
73
210
  } finally {
74
211
  if (handle !== undefined) fs.closeSync(handle);
75
- try { fs.unlinkSync(temporaryPath); } catch { /* already published or cleanup is best-effort */ }
212
+ let parentStillBound = false;
213
+ try {
214
+ assertParentBinding(home, parentPath, binding);
215
+ parentStillBound = true;
216
+ } catch { /* never traverse the temporary pathname after an ancestor swap */ }
217
+ if (parentStillBound && temporaryExists) {
218
+ try {
219
+ unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
220
+ } catch { /* fail closed: leave an untrusted temporary path untouched */ }
221
+ }
222
+ fs.closeSync(binding.handle);
76
223
  }
77
224
  }
78
225
 
79
- function writeGlobalSkillIfManaged(skillPath, content) {
80
- fs.mkdirSync(path.dirname(skillPath), { recursive: true });
226
+ function writeGlobalSkillIfManaged(skillPath, content, home = path.dirname(skillPath), options = {}) {
227
+ ensureSafeDirectoryWithinHome(home, path.dirname(skillPath));
81
228
 
82
229
  let handle;
83
230
  try {
231
+ try {
232
+ const existingStat = fs.lstatSync(skillPath);
233
+ if (existingStat.isSymbolicLink() || !existingStat.isFile()) return false;
234
+ } catch (error) {
235
+ if (error.code !== 'ENOENT') throw error;
236
+ }
84
237
  const noFollow = fs.constants.O_NOFOLLOW || 0;
85
238
  handle = fs.openSync(skillPath, fs.constants.O_RDONLY | noFollow);
86
239
  } catch (error) {
87
240
  if (error.code === 'ELOOP') return false;
88
241
  if (error.code !== 'ENOENT') throw error;
89
242
  try {
90
- handle = fs.openSync(skillPath, 'wx');
243
+ writeFileAtomically(skillPath, content, home, { ...options, exclusive: true });
244
+ return true;
91
245
  } catch (createError) {
92
- if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content);
246
+ if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content, home, options);
93
247
  throw createError;
94
248
  }
95
- try {
96
- fs.writeFileSync(handle, content, 'utf8');
97
- return true;
98
- } finally {
99
- fs.closeSync(handle);
100
- }
101
249
  }
102
250
 
103
251
  try {
104
- if (!fs.fstatSync(handle).isFile()) return false;
252
+ const opened = fs.fstatSync(handle);
253
+ const current = fs.lstatSync(skillPath);
254
+ if (!opened.isFile() || current.isSymbolicLink() || !current.isFile()) return false;
255
+ if (current.dev !== opened.dev || current.ino !== opened.ino) return false;
105
256
  const existing = fs.readFileSync(handle, 'utf8');
106
257
  if (!existing.includes('SINAPSE-MANAGED:global-skill')) return false;
107
258
  fs.closeSync(handle);
108
259
  handle = undefined;
109
- writeFileAtomically(skillPath, content);
260
+ writeFileAtomically(skillPath, content, home);
110
261
  return true;
111
262
  } finally {
112
263
  if (handle !== undefined) fs.closeSync(handle);
113
264
  }
114
265
  }
115
266
 
116
- function removeStaleManagedAgents(targetDir, expectedFiles, extension) {
267
+ function isValidGlobalSkill(skillId, skillPath) {
268
+ const content = readRegularFileNoFollowSync(skillPath, 'utf8');
269
+ return content !== null && content === renderGlobalSkill(skillId);
270
+ }
271
+
272
+ function removeStaleManagedAgents(targetDir, expectedFiles, extension, options = {}) {
117
273
  if (!fs.existsSync(targetDir)) return [];
274
+ const home = options.home || path.dirname(targetDir);
275
+ const binding = captureParentBinding(home, targetDir);
118
276
  const expected = new Set(expectedFiles);
119
277
  const removed = [];
120
- for (const file of fs.readdirSync(targetDir).filter((name) => name.endsWith(extension))) {
121
- if (expected.has(file)) continue;
122
- const filePath = path.join(targetDir, file);
123
- const content = readRegularFileNoFollowSync(filePath, 'utf8');
124
- if (content === null) continue;
125
- const explicitlyManaged = content.includes('SINAPSE-MANAGED:global-agent');
126
- const legacyManaged = content.includes('ACTIVATION-NOTICE: This command activates an agent from sinapse.')
127
- && /\.sinapse[\\/]sinapse[\\/]agents[\\/]/.test(content)
128
- && content.includes('Load the squad manifest');
129
- if (!explicitlyManaged && !legacyManaged) continue;
130
- fs.unlinkSync(filePath);
131
- removed.push(file);
278
+ try {
279
+ for (const file of fs.readdirSync(targetDir).filter((name) => name.endsWith(extension))) {
280
+ if (expected.has(file)) continue;
281
+ const filePath = path.join(targetDir, file);
282
+ const identity = fs.lstatSync(filePath);
283
+ const content = readRegularFileNoFollowSync(filePath, 'utf8');
284
+ if (content === null) continue;
285
+ const explicitlyManaged = content.includes('SINAPSE-MANAGED:global-agent');
286
+ const legacyManaged = content.includes('ACTIVATION-NOTICE: This command activates an agent from sinapse.')
287
+ && /\.sinapse[\\/]sinapse[\\/]agents[\\/]/.test(content)
288
+ && content.includes('Load the squad manifest');
289
+ if (!explicitlyManaged && !legacyManaged) continue;
290
+ if (unlinkFileWithBinding(home, filePath, binding, identity, options.beforeDelete)) removed.push(file);
291
+ }
292
+ } finally {
293
+ fs.closeSync(binding.handle);
132
294
  }
133
295
  return removed;
134
296
  }
135
297
 
136
- function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir }) {
298
+ function removeManagedFileWithBinding(home, filePath, beforeDelete) {
299
+ let identity;
300
+ try {
301
+ identity = fs.lstatSync(filePath);
302
+ } catch (error) {
303
+ if (error.code === 'ENOENT') return false;
304
+ throw error;
305
+ }
306
+ const content = readRegularFileNoFollowSync(filePath, 'utf8');
307
+ if (content === null || !content.includes('SINAPSE-MANAGED:global-agent')) return false;
308
+ const afterRead = fs.lstatSync(filePath);
309
+ if (afterRead.isSymbolicLink() || !afterRead.isFile()
310
+ || afterRead.dev !== identity.dev || afterRead.ino !== identity.ino) {
311
+ throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
312
+ }
313
+ const binding = captureParentBinding(home, path.dirname(filePath));
314
+ try {
315
+ return unlinkFileWithBinding(home, filePath, binding, identity, beforeDelete);
316
+ } finally {
317
+ fs.closeSync(binding.handle);
318
+ }
319
+ }
320
+
321
+ function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir, testHooks = {} }) {
137
322
  let commandFiles = fs.existsSync(commandsDir)
138
323
  ? fs.readdirSync(commandsDir).filter((file) => file.endsWith('.md')).sort()
139
324
  : [];
140
- if (commandFiles.includes('snps-orqx.md')) {
141
- const aliasFiles = new Set(['sinapse.md', 'sinapse-orqx.md', 'snps.md']);
325
+ if (commandFiles.includes(`${SUPREME_ORCHESTRATOR_ID}.md`)) {
326
+ const aliasFiles = new Set(MASTER_ALIAS_ENTRY_POINTS.map((id) => `${id}.md`));
142
327
  commandFiles = commandFiles.filter((file) => !aliasFiles.has(file));
143
328
  }
144
- const written = { claude: [], claudeSkills: [], codex: [], skills: [] };
329
+ const written = {
330
+ claude: [],
331
+ claudeSkills: [],
332
+ claudeAvailableSkills: [],
333
+ codex: [],
334
+ skills: [],
335
+ availableSkills: [],
336
+ };
145
337
 
146
338
  if (llmChoice === 'claude-code' || llmChoice === 'both') {
147
339
  const targetDir = path.join(home, '.claude', 'agents');
148
- fs.mkdirSync(targetDir, { recursive: true });
149
- removeStaleManagedAgents(targetDir, commandFiles, '.md');
340
+ ensureSafeDirectoryWithinHome(home, targetDir);
341
+ removeStaleManagedAgents(targetDir, commandFiles, '.md', { home, beforeDelete: testHooks.beforeStaleAgentDelete });
150
342
  for (const file of commandFiles) {
151
343
  const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
152
- fs.writeFileSync(path.join(targetDir, file), markdown, 'utf8');
344
+ writeFileAtomically(path.join(targetDir, file), markdown, home);
153
345
  written.claude.push(file);
154
346
  }
155
347
  const skillsRoot = path.join(home, '.claude', 'skills');
156
- for (const skillId of ['snps', 'sinapse', 'snps-orqx', 'sinapse-orqx', 'sinapse-agent']) {
348
+ for (const skillId of GLOBAL_PROVIDER_SKILL_IDS) {
157
349
  const skillPath = path.join(skillsRoot, skillId, 'SKILL.md');
158
- if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
350
+ if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId), home, { beforePublish: testHooks.beforeSkillPublish })) {
159
351
  written.claudeSkills.push(skillId);
160
352
  }
353
+ if (isValidGlobalSkill(skillId, skillPath)) written.claudeAvailableSkills.push(skillId);
161
354
  }
162
355
  }
163
356
  if (llmChoice === 'codex' || llmChoice === 'both') {
164
357
  const targetDir = path.join(home, '.codex', 'agents');
165
- fs.mkdirSync(targetDir, { recursive: true });
358
+ ensureSafeDirectoryWithinHome(home, targetDir);
166
359
  removeStaleManagedAgents(
167
360
  targetDir,
168
361
  commandFiles.map((file) => file.replace(/\.md$/, '.toml')),
169
362
  '.toml',
363
+ { home, beforeDelete: testHooks.beforeStaleAgentDelete },
170
364
  );
171
365
  for (const file of commandFiles) {
172
366
  const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
173
367
  const id = file.replace(/\.md$/, '');
174
368
  const metadata = parseAgentMarkdown(markdown, id);
175
369
  const tomlName = `${id}.toml`;
176
- fs.writeFileSync(path.join(targetDir, tomlName), renderCodexToml(metadata.name, metadata.description, markdown), 'utf8');
370
+ writeFileAtomically(path.join(targetDir, tomlName), renderCodexToml(metadata.name, metadata.description, markdown), home);
177
371
  const staleMarkdown = path.join(targetDir, file);
178
- if (fs.existsSync(staleMarkdown)) fs.unlinkSync(staleMarkdown);
372
+ removeManagedFileWithBinding(home, staleMarkdown, testHooks.beforeStaleMarkdownDelete);
179
373
  written.codex.push(tomlName);
180
374
  }
181
375
  const skillsRoot = path.join(home, '.agents', 'skills');
182
- for (const skillId of ['snps', 'sinapse', 'snps-orqx', 'sinapse-orqx', 'sinapse-agent']) {
376
+ for (const skillId of GLOBAL_PROVIDER_SKILL_IDS) {
183
377
  const skillDir = path.join(skillsRoot, skillId);
184
378
  const skillPath = path.join(skillDir, 'SKILL.md');
185
- if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
379
+ if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId), home, { beforePublish: testHooks.beforeSkillPublish })) {
186
380
  written.skills.push(skillId);
187
381
  }
382
+ if (isValidGlobalSkill(skillId, skillPath)) written.availableSkills.push(skillId);
188
383
  }
189
384
  }
190
385
  return written;
@@ -196,4 +391,4 @@ function getGlobalCommandStagingDir({ llmChoice, sinapseHome, claudeCommandsDir
196
391
  return path.join(sinapseHome, '.generated', 'agents');
197
392
  }
198
393
 
199
- module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, writeGlobalSkillIfManaged, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
394
+ module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, readRegularFileNoFollowSync, writeFileAtomically, writeGlobalSkillIfManaged, isValidGlobalSkill, ensureSafeDirectoryWithinHome, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const CANONICAL_AGENT_COUNT = 172;
4
+ const SUPREME_ORCHESTRATOR_ID = 'snps-orqx';
5
+ const SUPREME_PUBLIC_ID = SUPREME_ORCHESTRATOR_ID.replace(/^snps/, 'sinapse');
6
+ const MASTER_ALIAS_ENTRY_POINTS = Object.freeze([
7
+ SUPREME_PUBLIC_ID.replace(/-orqx$/, ''),
8
+ SUPREME_PUBLIC_ID,
9
+ SUPREME_ORCHESTRATOR_ID.replace(/-orqx$/, ''),
10
+ ]);
11
+ const GLOBAL_PROVIDER_SKILL_IDS = Object.freeze([
12
+ ...MASTER_ALIAS_ENTRY_POINTS,
13
+ SUPREME_ORCHESTRATOR_ID,
14
+ 'sinapse-agent',
15
+ ]);
16
+
17
+ module.exports = {
18
+ CANONICAL_AGENT_COUNT,
19
+ SUPREME_ORCHESTRATOR_ID,
20
+ SUPREME_PUBLIC_ID,
21
+ MASTER_ALIAS_ENTRY_POINTS,
22
+ GLOBAL_PROVIDER_SKILL_IDS,
23
+ };
@@ -1,22 +1,73 @@
1
1
  'use strict';
2
2
 
3
- function assertProviderAdapterParity(llmChoice, adapters, canonicalCount) {
3
+ const {
4
+ CANONICAL_AGENT_COUNT,
5
+ GLOBAL_PROVIDER_SKILL_IDS,
6
+ } = require('./provider-contract');
7
+
8
+ function normalizeCanonicalCatalog(canonicalCatalog, extension) {
9
+ if (!canonicalCatalog || typeof canonicalCatalog === 'number') {
10
+ throw new Error('Provider adapter parity requires the canonical agent ID catalog');
11
+ }
12
+ const entries = [...canonicalCatalog].map((entry) => (typeof entry === 'string'
13
+ ? { id: entry.replace(/\.md$/, ''), file: entry }
14
+ : entry));
15
+ const sourceIds = entries.map((entry) => entry.id);
16
+ const sourceSet = new Set(sourceIds);
17
+ if (sourceSet.size !== sourceIds.length) {
18
+ const duplicates = [...sourceSet]
19
+ .filter((id) => sourceIds.filter((item) => item === id).length > 1);
20
+ throw new Error(`Canonical agent catalog contains duplicate IDs: ${duplicates.join(', ')}`);
21
+ }
22
+ if (entries.length !== CANONICAL_AGENT_COUNT) {
23
+ throw new Error(`Canonical agent catalog must contain exactly ${CANONICAL_AGENT_COUNT} unique IDs, found ${entries.length}`);
24
+ }
25
+ const files = entries.map((entry) => entry.file.replace(/\.md$/, extension));
26
+ return { count: CANONICAL_AGENT_COUNT, files: new Set(files) };
27
+ }
28
+
29
+ function describeCatalogDivergence(provider, actualFiles, canonical) {
30
+ const actual = new Set(actualFiles);
31
+ const problems = [];
32
+ if (actualFiles.length !== canonical.count) {
33
+ problems.push(`${provider}: ${actualFiles.length}/${canonical.count}`);
34
+ }
35
+ if (canonical.files) {
36
+ if (actual.size !== actualFiles.length) {
37
+ const duplicates = [...actual].filter((file) => actualFiles.filter((item) => item === file).length > 1);
38
+ problems.push(`${provider} duplicate: ${duplicates.join(', ')}`);
39
+ }
40
+ const missing = [...canonical.files].filter((file) => !actual.has(file));
41
+ const orphaned = [...actual].filter((file) => !canonical.files.has(file));
42
+ if (missing.length) problems.push(`${provider} missing: ${missing.join(', ')}`);
43
+ if (orphaned.length) problems.push(`${provider} orphaned: ${orphaned.join(', ')}`);
44
+ }
45
+ return problems;
46
+ }
47
+
48
+ function assertProviderAdapterParity(llmChoice, adapters, canonicalCatalog) {
4
49
  if (!['claude-code', 'codex', 'both'].includes(llmChoice)) {
5
50
  throw new Error(`Invalid LLM choice for provider parity: ${llmChoice}`);
6
51
  }
7
- const providerCounts = [];
52
+ const problems = [];
53
+ const claudeCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.md');
54
+ const codexCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.toml');
8
55
  if (llmChoice === 'claude-code' || llmChoice === 'both') {
9
- providerCounts.push(['Claude Code', adapters.claude.length]);
56
+ problems.push(...describeCatalogDivergence('Claude Code', adapters.claude, claudeCatalog));
57
+ const claudeSkills = new Set(adapters.claudeAvailableSkills || adapters.claudeSkills || []);
58
+ const missingAliases = GLOBAL_PROVIDER_SKILL_IDS.filter((alias) => !claudeSkills.has(alias));
59
+ if (missingAliases.length) problems.push(`Claude Code alias skills missing: ${missingAliases.join(', ')}`);
10
60
  }
11
61
  if (llmChoice === 'codex' || llmChoice === 'both') {
12
- providerCounts.push(['Codex', adapters.codex.length]);
62
+ problems.push(...describeCatalogDivergence('Codex', adapters.codex, codexCatalog));
63
+ const codexSkills = new Set(adapters.availableSkills || adapters.skills || []);
64
+ const missingAliases = GLOBAL_PROVIDER_SKILL_IDS.filter((alias) => !codexSkills.has(alias));
65
+ if (missingAliases.length) problems.push(`Codex alias skills missing: ${missingAliases.join(', ')}`);
13
66
  }
14
- const divergent = providerCounts.filter(([, count]) => count !== canonicalCount);
15
- if (divergent.length) {
16
- const details = divergent.map(([provider, count]) => `${provider}: ${count}/${canonicalCount}`).join(', ');
17
- throw new Error(`Provider adapter parity failed (${details})`);
67
+ if (problems.length) {
68
+ throw new Error(`Provider adapter parity failed (${problems.join('; ')})`);
18
69
  }
19
- return canonicalCount;
70
+ return claudeCatalog.count;
20
71
  }
21
72
 
22
73
  module.exports = { assertProviderAdapterParity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinapse-ai",
3
- "version": "1.25.2",
3
+ "version": "1.25.3",
4
4
  "description": "SINAPSE AI: Framework de orquestracao de IA — 17 squads, 172 agentes especializados",
5
5
  "bin": {
6
6
  "sinapse": "bin/sinapse.js",