sinapse-ai 1.25.2 → 1.26.0

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.
@@ -2,17 +2,74 @@
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
+ GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
10
+ SUPREME_ORCHESTRATOR_ID,
11
+ SUPREME_PUBLIC_ID,
12
+ } = require('./provider-contract');
5
13
 
6
14
  const NOFOLLOW_READ_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
7
15
 
16
+ function isPathInside(root, candidate) {
17
+ const relative = path.relative(root, candidate);
18
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
19
+ }
20
+
21
+ function ensureSafeDirectoryWithinHome(home, directory) {
22
+ const absoluteHome = path.resolve(home);
23
+ const absoluteDirectory = path.resolve(directory);
24
+ if (!isPathInside(absoluteHome, absoluteDirectory)) {
25
+ throw new Error(`Refusing provider write outside HOME: ${absoluteDirectory}`);
26
+ }
27
+ fs.mkdirSync(absoluteHome, { recursive: true });
28
+ const homeStat = fs.lstatSync(absoluteHome);
29
+ if (homeStat.isSymbolicLink() || !homeStat.isDirectory()) {
30
+ throw new Error(`Refusing provider write through unsafe HOME: ${absoluteHome}`);
31
+ }
32
+ const realHome = fs.realpathSync.native(absoluteHome);
33
+ let current = absoluteHome;
34
+ const relative = path.relative(absoluteHome, absoluteDirectory);
35
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
36
+ current = path.join(current, segment);
37
+ if (!fs.existsSync(current)) fs.mkdirSync(current);
38
+ const stat = fs.lstatSync(current);
39
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
40
+ throw new Error(`Refusing provider write through link/reparse path: ${current}`);
41
+ }
42
+ const realCurrent = fs.realpathSync.native(current);
43
+ if (!isPathInside(realHome, realCurrent)) {
44
+ throw new Error(`Refusing provider write outside HOME through reparse path: ${current}`);
45
+ }
46
+ }
47
+ return absoluteDirectory;
48
+ }
49
+
50
+ function assertSafeFileWithinHome(home, filePath) {
51
+ ensureSafeDirectoryWithinHome(home, path.dirname(filePath));
52
+ if (fs.existsSync(filePath)) {
53
+ const stat = fs.lstatSync(filePath);
54
+ if (stat.isSymbolicLink() || !stat.isFile()) {
55
+ throw new Error(`Refusing provider write through unsafe file: ${filePath}`);
56
+ }
57
+ return stat;
58
+ }
59
+ return null;
60
+ }
61
+
8
62
  function readRegularFileNoFollowSync(filePath, encoding = null) {
9
63
  let handle;
10
64
  try {
11
65
  handle = fs.openSync(filePath, NOFOLLOW_READ_FLAGS);
12
- if (!fs.fstatSync(handle).isFile()) return null;
66
+ const opened = fs.fstatSync(handle);
67
+ const after = fs.lstatSync(filePath);
68
+ if (!opened.isFile() || after.isSymbolicLink() || !after.isFile()) return null;
69
+ if (after.dev !== opened.dev || after.ino !== opened.ino) return null;
13
70
  return fs.readFileSync(handle, encoding || undefined);
14
71
  } catch (error) {
15
- if (['ENOENT', 'ELOOP', 'EISDIR'].includes(error.code)) return null;
72
+ if (['ENOENT', 'ELOOP', 'EISDIR', 'EINVAL', 'UNKNOWN'].includes(error.code)) return null;
16
73
  throw error;
17
74
  } finally {
18
75
  if (handle !== undefined) fs.closeSync(handle);
@@ -36,14 +93,21 @@ function renderCodexToml(name, description, instructions) {
36
93
 
37
94
  function markGlobalAgent(markdown, fileName = '') {
38
95
  let content = markdown;
39
- if (fileName === 'snps-orqx.md') {
40
- content = content.replace(/^name:\s*snps-orqx\s*$/m, 'name: sinapse-orqx');
96
+ if (fileName === `${SUPREME_ORCHESTRATOR_ID}.md`) {
97
+ content = content.replace(
98
+ new RegExp(`^name:\\s*${SUPREME_ORCHESTRATOR_ID}\\s*$`, 'm'),
99
+ `name: ${SUPREME_PUBLIC_ID}`,
100
+ );
41
101
  }
42
102
  if (content.includes('SINAPSE-MANAGED:global-agent')) return content;
43
103
  return `${content.trimEnd()}\n\n<!-- SINAPSE-MANAGED:global-agent -->\n`;
44
104
  }
45
105
 
46
- function renderGlobalSkill(skillId) {
106
+ function renderGlobalSkill(skillId, sourceContent = null) {
107
+ if (skillId === 'react-bits-frontend') {
108
+ if (!sourceContent) throw new Error('React Bits global skill source is required');
109
+ return `${String(sourceContent).trimEnd()}\n\n<!-- SINAPSE-MANAGED:global-skill -->\n`;
110
+ }
47
111
  const generic = skillId === 'sinapse-agent';
48
112
  return [
49
113
  '---',
@@ -54,137 +118,280 @@ function renderGlobalSkill(skillId) {
54
118
  generic ? '# SINAPSE Global Agent Activator' : '# SINAPSE Global Orchestrator', '',
55
119
  generic
56
120
  ? '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.',
121
+ : `Activate the public adapter \`${SUPREME_PUBLIC_ID}\`, backed by canonical source ID \`${SUPREME_ORCHESTRATOR_ID}\`, and preserve its delegation-only authority.`,
58
122
  'Canonical global persona and task sources live under `~/.sinapse/`.',
59
123
  'Use native Codex delegation and never start a nested CLI.', '',
60
124
  ].join('\n');
61
125
  }
62
126
 
63
- function writeFileAtomically(filePath, content) {
64
- const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
127
+ function captureParentBinding(home, parentPath) {
128
+ ensureSafeDirectoryWithinHome(home, parentPath);
129
+ const handle = fs.openSync(parentPath, fs.constants.O_RDONLY);
130
+ const opened = fs.fstatSync(handle);
131
+ const current = fs.lstatSync(parentPath);
132
+ if (!opened.isDirectory() || current.isSymbolicLink() || !current.isDirectory()
133
+ || opened.dev !== current.dev || opened.ino !== current.ino) {
134
+ fs.closeSync(handle);
135
+ throw new Error(`Refusing provider write through unstable parent: ${parentPath}`);
136
+ }
137
+ return { handle, dev: opened.dev, ino: opened.ino, realPath: fs.realpathSync.native(parentPath) };
138
+ }
139
+
140
+ function assertParentBinding(home, parentPath, binding) {
141
+ ensureSafeDirectoryWithinHome(home, parentPath);
142
+ const current = fs.lstatSync(parentPath);
143
+ const opened = fs.fstatSync(binding.handle);
144
+ const realPath = fs.realpathSync.native(parentPath);
145
+ if (current.isSymbolicLink() || !current.isDirectory() || !opened.isDirectory()
146
+ || current.dev !== binding.dev || current.ino !== binding.ino
147
+ || opened.dev !== binding.dev || opened.ino !== binding.ino
148
+ || realPath !== binding.realPath) {
149
+ throw new Error(`Refusing provider publish after parent path changed: ${parentPath}`);
150
+ }
151
+ }
152
+
153
+ function unlinkFileWithBinding(home, filePath, binding, expected, beforeDelete) {
154
+ if (typeof beforeDelete === 'function') beforeDelete({ filePath, parentPath: path.dirname(filePath) });
155
+ assertParentBinding(home, path.dirname(filePath), binding);
156
+ let current;
157
+ try {
158
+ current = fs.lstatSync(filePath);
159
+ } catch (error) {
160
+ if (error.code === 'ENOENT') return false;
161
+ throw error;
162
+ }
163
+ if (current.isSymbolicLink() || !current.isFile()
164
+ || current.dev !== expected.dev || current.ino !== expected.ino) {
165
+ throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
166
+ }
167
+ fs.unlinkSync(filePath);
168
+ assertParentBinding(home, path.dirname(filePath), binding);
169
+ return true;
170
+ }
171
+
172
+ function writeFileAtomically(filePath, content, home = path.dirname(filePath), options = {}) {
173
+ const destinationIdentity = assertSafeFileWithinHome(home, filePath);
174
+ const parentPath = path.dirname(filePath);
175
+ const binding = captureParentBinding(home, parentPath);
176
+ const temporaryPath = path.join(
177
+ parentPath,
178
+ `.${path.basename(filePath)}.${crypto.randomBytes(24).toString('hex')}.tmp`,
179
+ );
65
180
  let handle;
181
+ let temporaryIdentity;
182
+ let temporaryExists = false;
66
183
  try {
67
184
  handle = fs.openSync(temporaryPath, 'wx', 0o600);
185
+ temporaryIdentity = fs.fstatSync(handle);
186
+ temporaryExists = true;
68
187
  fs.writeFileSync(handle, content, 'utf8');
69
188
  fs.fsyncSync(handle);
70
189
  fs.closeSync(handle);
71
190
  handle = undefined;
72
- fs.renameSync(temporaryPath, filePath);
191
+ if (typeof options.beforePublish === 'function') {
192
+ options.beforePublish({ filePath, parentPath, temporaryPath });
193
+ }
194
+ assertParentBinding(home, parentPath, binding);
195
+ if (options.exclusive) {
196
+ fs.linkSync(temporaryPath, filePath);
197
+ assertParentBinding(home, parentPath, binding);
198
+ unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
199
+ temporaryExists = false;
200
+ } else {
201
+ const currentDestination = assertSafeFileWithinHome(home, filePath);
202
+ if (destinationIdentity && (!currentDestination
203
+ || currentDestination.dev !== destinationIdentity.dev
204
+ || currentDestination.ino !== destinationIdentity.ino)) {
205
+ throw new Error(`Refusing provider overwrite after file identity changed: ${filePath}`);
206
+ }
207
+ if (!destinationIdentity && currentDestination) {
208
+ throw new Error(`Refusing provider overwrite of a newly appeared file: ${filePath}`);
209
+ }
210
+ assertParentBinding(home, parentPath, binding);
211
+ fs.renameSync(temporaryPath, filePath);
212
+ temporaryExists = false;
213
+ }
214
+ assertParentBinding(home, parentPath, binding);
73
215
  } finally {
74
216
  if (handle !== undefined) fs.closeSync(handle);
75
- try { fs.unlinkSync(temporaryPath); } catch { /* already published or cleanup is best-effort */ }
217
+ let parentStillBound = false;
218
+ try {
219
+ assertParentBinding(home, parentPath, binding);
220
+ parentStillBound = true;
221
+ } catch { /* never traverse the temporary pathname after an ancestor swap */ }
222
+ if (parentStillBound && temporaryExists) {
223
+ try {
224
+ unlinkFileWithBinding(home, temporaryPath, binding, temporaryIdentity, options.beforeTempCleanup);
225
+ } catch { /* fail closed: leave an untrusted temporary path untouched */ }
226
+ }
227
+ fs.closeSync(binding.handle);
76
228
  }
77
229
  }
78
230
 
79
- function writeGlobalSkillIfManaged(skillPath, content) {
80
- fs.mkdirSync(path.dirname(skillPath), { recursive: true });
231
+ function writeGlobalSkillIfManaged(skillPath, content, home = path.dirname(skillPath), options = {}) {
232
+ ensureSafeDirectoryWithinHome(home, path.dirname(skillPath));
81
233
 
82
234
  let handle;
83
235
  try {
236
+ try {
237
+ const existingStat = fs.lstatSync(skillPath);
238
+ if (existingStat.isSymbolicLink() || !existingStat.isFile()) return false;
239
+ } catch (error) {
240
+ if (error.code !== 'ENOENT') throw error;
241
+ }
84
242
  const noFollow = fs.constants.O_NOFOLLOW || 0;
85
243
  handle = fs.openSync(skillPath, fs.constants.O_RDONLY | noFollow);
86
244
  } catch (error) {
87
245
  if (error.code === 'ELOOP') return false;
88
246
  if (error.code !== 'ENOENT') throw error;
89
247
  try {
90
- handle = fs.openSync(skillPath, 'wx');
248
+ writeFileAtomically(skillPath, content, home, { ...options, exclusive: true });
249
+ return true;
91
250
  } catch (createError) {
92
- if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content);
251
+ if (createError.code === 'EEXIST') return writeGlobalSkillIfManaged(skillPath, content, home, options);
93
252
  throw createError;
94
253
  }
95
- try {
96
- fs.writeFileSync(handle, content, 'utf8');
97
- return true;
98
- } finally {
99
- fs.closeSync(handle);
100
- }
101
254
  }
102
255
 
103
256
  try {
104
- if (!fs.fstatSync(handle).isFile()) return false;
257
+ const opened = fs.fstatSync(handle);
258
+ const current = fs.lstatSync(skillPath);
259
+ if (!opened.isFile() || current.isSymbolicLink() || !current.isFile()) return false;
260
+ if (current.dev !== opened.dev || current.ino !== opened.ino) return false;
105
261
  const existing = fs.readFileSync(handle, 'utf8');
106
262
  if (!existing.includes('SINAPSE-MANAGED:global-skill')) return false;
107
263
  fs.closeSync(handle);
108
264
  handle = undefined;
109
- writeFileAtomically(skillPath, content);
265
+ writeFileAtomically(skillPath, content, home);
110
266
  return true;
111
267
  } finally {
112
268
  if (handle !== undefined) fs.closeSync(handle);
113
269
  }
114
270
  }
115
271
 
116
- function removeStaleManagedAgents(targetDir, expectedFiles, extension) {
272
+ function isValidGlobalSkill(skillId, skillPath, sourceContent = null) {
273
+ const content = readRegularFileNoFollowSync(skillPath, 'utf8');
274
+ return content !== null && content === renderGlobalSkill(skillId, sourceContent);
275
+ }
276
+
277
+ function removeStaleManagedAgents(targetDir, expectedFiles, extension, options = {}) {
117
278
  if (!fs.existsSync(targetDir)) return [];
279
+ const home = options.home || path.dirname(targetDir);
280
+ const binding = captureParentBinding(home, targetDir);
118
281
  const expected = new Set(expectedFiles);
119
282
  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);
283
+ try {
284
+ for (const file of fs.readdirSync(targetDir).filter((name) => name.endsWith(extension))) {
285
+ if (expected.has(file)) continue;
286
+ const filePath = path.join(targetDir, file);
287
+ const identity = fs.lstatSync(filePath);
288
+ const content = readRegularFileNoFollowSync(filePath, 'utf8');
289
+ if (content === null) continue;
290
+ const explicitlyManaged = content.includes('SINAPSE-MANAGED:global-agent');
291
+ const legacyManaged = content.includes('ACTIVATION-NOTICE: This command activates an agent from sinapse.')
292
+ && /\.sinapse[\\/]sinapse[\\/]agents[\\/]/.test(content)
293
+ && content.includes('Load the squad manifest');
294
+ if (!explicitlyManaged && !legacyManaged) continue;
295
+ if (unlinkFileWithBinding(home, filePath, binding, identity, options.beforeDelete)) removed.push(file);
296
+ }
297
+ } finally {
298
+ fs.closeSync(binding.handle);
132
299
  }
133
300
  return removed;
134
301
  }
135
302
 
136
- function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir }) {
303
+ function removeManagedFileWithBinding(home, filePath, beforeDelete) {
304
+ let identity;
305
+ try {
306
+ identity = fs.lstatSync(filePath);
307
+ } catch (error) {
308
+ if (error.code === 'ENOENT') return false;
309
+ throw error;
310
+ }
311
+ const content = readRegularFileNoFollowSync(filePath, 'utf8');
312
+ if (content === null || !content.includes('SINAPSE-MANAGED:global-agent')) return false;
313
+ const afterRead = fs.lstatSync(filePath);
314
+ if (afterRead.isSymbolicLink() || !afterRead.isFile()
315
+ || afterRead.dev !== identity.dev || afterRead.ino !== identity.ino) {
316
+ throw new Error(`Refusing provider delete after file identity changed: ${filePath}`);
317
+ }
318
+ const binding = captureParentBinding(home, path.dirname(filePath));
319
+ try {
320
+ return unlinkFileWithBinding(home, filePath, binding, identity, beforeDelete);
321
+ } finally {
322
+ fs.closeSync(binding.handle);
323
+ }
324
+ }
325
+
326
+ function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir, reactBitsSkillPath, testHooks = {} }) {
137
327
  let commandFiles = fs.existsSync(commandsDir)
138
328
  ? fs.readdirSync(commandsDir).filter((file) => file.endsWith('.md')).sort()
139
329
  : [];
140
- if (commandFiles.includes('snps-orqx.md')) {
141
- const aliasFiles = new Set(['sinapse.md', 'sinapse-orqx.md', 'snps.md']);
330
+ if (commandFiles.includes(`${SUPREME_ORCHESTRATOR_ID}.md`)) {
331
+ const aliasFiles = new Set(MASTER_ALIAS_ENTRY_POINTS.map((id) => `${id}.md`));
142
332
  commandFiles = commandFiles.filter((file) => !aliasFiles.has(file));
143
333
  }
144
- const written = { claude: [], claudeSkills: [], codex: [], skills: [] };
334
+ const written = {
335
+ claude: [],
336
+ claudeSkills: [],
337
+ claudeAvailableSkills: [],
338
+ codex: [],
339
+ skills: [],
340
+ availableSkills: [],
341
+ };
342
+ let reactBitsSkillContent = null;
343
+ const providerSkillIds = [...GLOBAL_PROVIDER_SKILL_IDS];
344
+ if (reactBitsSkillPath) {
345
+ reactBitsSkillContent = readRegularFileNoFollowSync(reactBitsSkillPath, 'utf8');
346
+ if (reactBitsSkillContent === null) throw new Error(`React Bits global skill is missing: ${reactBitsSkillPath}`);
347
+ providerSkillIds.push(...GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS);
348
+ }
145
349
 
146
350
  if (llmChoice === 'claude-code' || llmChoice === 'both') {
147
351
  const targetDir = path.join(home, '.claude', 'agents');
148
- fs.mkdirSync(targetDir, { recursive: true });
149
- removeStaleManagedAgents(targetDir, commandFiles, '.md');
352
+ ensureSafeDirectoryWithinHome(home, targetDir);
353
+ removeStaleManagedAgents(targetDir, commandFiles, '.md', { home, beforeDelete: testHooks.beforeStaleAgentDelete });
150
354
  for (const file of commandFiles) {
151
355
  const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
152
- fs.writeFileSync(path.join(targetDir, file), markdown, 'utf8');
356
+ writeFileAtomically(path.join(targetDir, file), markdown, home);
153
357
  written.claude.push(file);
154
358
  }
155
359
  const skillsRoot = path.join(home, '.claude', 'skills');
156
- for (const skillId of ['snps', 'sinapse', 'snps-orqx', 'sinapse-orqx', 'sinapse-agent']) {
360
+ for (const skillId of providerSkillIds) {
157
361
  const skillPath = path.join(skillsRoot, skillId, 'SKILL.md');
158
- if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
362
+ if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId, reactBitsSkillContent), home, { beforePublish: testHooks.beforeSkillPublish })) {
159
363
  written.claudeSkills.push(skillId);
160
364
  }
365
+ if (isValidGlobalSkill(skillId, skillPath, reactBitsSkillContent)) written.claudeAvailableSkills.push(skillId);
161
366
  }
162
367
  }
163
368
  if (llmChoice === 'codex' || llmChoice === 'both') {
164
369
  const targetDir = path.join(home, '.codex', 'agents');
165
- fs.mkdirSync(targetDir, { recursive: true });
370
+ ensureSafeDirectoryWithinHome(home, targetDir);
166
371
  removeStaleManagedAgents(
167
372
  targetDir,
168
373
  commandFiles.map((file) => file.replace(/\.md$/, '.toml')),
169
374
  '.toml',
375
+ { home, beforeDelete: testHooks.beforeStaleAgentDelete },
170
376
  );
171
377
  for (const file of commandFiles) {
172
378
  const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
173
379
  const id = file.replace(/\.md$/, '');
174
380
  const metadata = parseAgentMarkdown(markdown, id);
175
381
  const tomlName = `${id}.toml`;
176
- fs.writeFileSync(path.join(targetDir, tomlName), renderCodexToml(metadata.name, metadata.description, markdown), 'utf8');
382
+ writeFileAtomically(path.join(targetDir, tomlName), renderCodexToml(metadata.name, metadata.description, markdown), home);
177
383
  const staleMarkdown = path.join(targetDir, file);
178
- if (fs.existsSync(staleMarkdown)) fs.unlinkSync(staleMarkdown);
384
+ removeManagedFileWithBinding(home, staleMarkdown, testHooks.beforeStaleMarkdownDelete);
179
385
  written.codex.push(tomlName);
180
386
  }
181
387
  const skillsRoot = path.join(home, '.agents', 'skills');
182
- for (const skillId of ['snps', 'sinapse', 'snps-orqx', 'sinapse-orqx', 'sinapse-agent']) {
388
+ for (const skillId of providerSkillIds) {
183
389
  const skillDir = path.join(skillsRoot, skillId);
184
390
  const skillPath = path.join(skillDir, 'SKILL.md');
185
- if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
391
+ if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId, reactBitsSkillContent), home, { beforePublish: testHooks.beforeSkillPublish })) {
186
392
  written.skills.push(skillId);
187
393
  }
394
+ if (isValidGlobalSkill(skillId, skillPath, reactBitsSkillContent)) written.availableSkills.push(skillId);
188
395
  }
189
396
  }
190
397
  return written;
@@ -196,4 +403,4 @@ function getGlobalCommandStagingDir({ llmChoice, sinapseHome, claudeCommandsDir
196
403
  return path.join(sinapseHome, '.generated', 'agents');
197
404
  }
198
405
 
199
- module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, writeGlobalSkillIfManaged, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
406
+ module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, readRegularFileNoFollowSync, writeFileAtomically, writeGlobalSkillIfManaged, isValidGlobalSkill, ensureSafeDirectoryWithinHome, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
@@ -0,0 +1,25 @@
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
+ const GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS = Object.freeze(['react-bits-frontend']);
17
+
18
+ module.exports = {
19
+ CANONICAL_AGENT_COUNT,
20
+ SUPREME_ORCHESTRATOR_ID,
21
+ SUPREME_PUBLIC_ID,
22
+ MASTER_ALIAS_ENTRY_POINTS,
23
+ GLOBAL_PROVIDER_SKILL_IDS,
24
+ GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
25
+ };
@@ -1,22 +1,79 @@
1
1
  'use strict';
2
2
 
3
- function assertProviderAdapterParity(llmChoice, adapters, canonicalCount) {
3
+ const {
4
+ CANONICAL_AGENT_COUNT,
5
+ GLOBAL_PROVIDER_SKILL_IDS,
6
+ GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
7
+ } = require('./provider-contract');
8
+
9
+ const REQUIRED_GLOBAL_PROVIDER_SKILL_IDS = Object.freeze([
10
+ ...GLOBAL_PROVIDER_SKILL_IDS,
11
+ ...GLOBAL_SUPPLEMENTAL_PROVIDER_SKILL_IDS,
12
+ ]);
13
+
14
+ function normalizeCanonicalCatalog(canonicalCatalog, extension) {
15
+ if (!canonicalCatalog || typeof canonicalCatalog === 'number') {
16
+ throw new Error('Provider adapter parity requires the canonical agent ID catalog');
17
+ }
18
+ const entries = [...canonicalCatalog].map((entry) => (typeof entry === 'string'
19
+ ? { id: entry.replace(/\.md$/, ''), file: entry }
20
+ : entry));
21
+ const sourceIds = entries.map((entry) => entry.id);
22
+ const sourceSet = new Set(sourceIds);
23
+ if (sourceSet.size !== sourceIds.length) {
24
+ const duplicates = [...sourceSet]
25
+ .filter((id) => sourceIds.filter((item) => item === id).length > 1);
26
+ throw new Error(`Canonical agent catalog contains duplicate IDs: ${duplicates.join(', ')}`);
27
+ }
28
+ if (entries.length !== CANONICAL_AGENT_COUNT) {
29
+ throw new Error(`Canonical agent catalog must contain exactly ${CANONICAL_AGENT_COUNT} unique IDs, found ${entries.length}`);
30
+ }
31
+ const files = entries.map((entry) => entry.file.replace(/\.md$/, extension));
32
+ return { count: CANONICAL_AGENT_COUNT, files: new Set(files) };
33
+ }
34
+
35
+ function describeCatalogDivergence(provider, actualFiles, canonical) {
36
+ const actual = new Set(actualFiles);
37
+ const problems = [];
38
+ if (actualFiles.length !== canonical.count) {
39
+ problems.push(`${provider}: ${actualFiles.length}/${canonical.count}`);
40
+ }
41
+ if (canonical.files) {
42
+ if (actual.size !== actualFiles.length) {
43
+ const duplicates = [...actual].filter((file) => actualFiles.filter((item) => item === file).length > 1);
44
+ problems.push(`${provider} duplicate: ${duplicates.join(', ')}`);
45
+ }
46
+ const missing = [...canonical.files].filter((file) => !actual.has(file));
47
+ const orphaned = [...actual].filter((file) => !canonical.files.has(file));
48
+ if (missing.length) problems.push(`${provider} missing: ${missing.join(', ')}`);
49
+ if (orphaned.length) problems.push(`${provider} orphaned: ${orphaned.join(', ')}`);
50
+ }
51
+ return problems;
52
+ }
53
+
54
+ function assertProviderAdapterParity(llmChoice, adapters, canonicalCatalog) {
4
55
  if (!['claude-code', 'codex', 'both'].includes(llmChoice)) {
5
56
  throw new Error(`Invalid LLM choice for provider parity: ${llmChoice}`);
6
57
  }
7
- const providerCounts = [];
58
+ const problems = [];
59
+ const claudeCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.md');
60
+ const codexCatalog = normalizeCanonicalCatalog(canonicalCatalog, '.toml');
8
61
  if (llmChoice === 'claude-code' || llmChoice === 'both') {
9
- providerCounts.push(['Claude Code', adapters.claude.length]);
62
+ problems.push(...describeCatalogDivergence('Claude Code', adapters.claude, claudeCatalog));
63
+ const claudeSkills = new Set(adapters.claudeAvailableSkills || adapters.claudeSkills || []);
64
+ const missingSkills = REQUIRED_GLOBAL_PROVIDER_SKILL_IDS.filter((skillId) => !claudeSkills.has(skillId));
65
+ if (missingSkills.length) problems.push(`Claude Code global skills missing: ${missingSkills.join(', ')}`);
10
66
  }
11
67
  if (llmChoice === 'codex' || llmChoice === 'both') {
12
- providerCounts.push(['Codex', adapters.codex.length]);
68
+ problems.push(...describeCatalogDivergence('Codex', adapters.codex, codexCatalog));
69
+ const codexSkills = new Set(adapters.availableSkills || adapters.skills || []);
70
+ const missingSkills = REQUIRED_GLOBAL_PROVIDER_SKILL_IDS.filter((skillId) => !codexSkills.has(skillId));
71
+ if (missingSkills.length) problems.push(`Codex global skills missing: ${missingSkills.join(', ')}`);
13
72
  }
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})`);
73
+ if (problems.length) {
74
+ throw new Error(`Provider adapter parity failed (${problems.join('; ')})`);
18
75
  }
19
- return canonicalCount;
76
+ return claudeCatalog.count;
20
77
  }
21
78
 
22
- module.exports = { assertProviderAdapterParity };
79
+ module.exports = { assertProviderAdapterParity, REQUIRED_GLOBAL_PROVIDER_SKILL_IDS };
@@ -32,7 +32,7 @@ Both providers expose native lifecycle surfaces. Their registration models diffe
32
32
  | Measured surface | Claude Code | Codex CLI |
33
33
  | --- | --- | --- |
34
34
  | Native agents | 172 | 172 |
35
- | Installed skills | 36 | 37 |
35
+ | Installed skills | 37 | 37 |
36
36
  | Registered hooks | 20 native registrations | 9 lifecycle events through the bridge |
37
37
  | Activation | `@agent-name` | `$snps` or `$sinapse-agent <id>` |
38
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinapse-ai",
3
- "version": "1.25.2",
3
+ "version": "1.26.0",
4
4
  "description": "SINAPSE AI: Framework de orquestracao de IA — 17 squads, 172 agentes especializados",
5
5
  "bin": {
6
6
  "sinapse": "bin/sinapse.js",
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const REACT_BITS_CORPUS_RELATIVE_PATH = path.join('docs', 'framework', 'react-bits');
7
+ const REACT_BITS_SKILL_RELATIVE_PATH = path.join('.agents', 'skills', 'react-bits-frontend', 'SKILL.md');
8
+ const REQUIRED_REACT_BITS_CORPUS_FILES = Object.freeze([
9
+ 'animations.md',
10
+ 'audit-findings.md',
11
+ 'backgrounds.md',
12
+ 'catalog-summary.json',
13
+ 'components.md',
14
+ 'implementation-playbook.md',
15
+ 'index.md',
16
+ 'inventory.json',
17
+ 'text-animations.md',
18
+ ]);
19
+
20
+ function isRegularFile(filePath) {
21
+ try {
22
+ return fs.lstatSync(filePath).isFile();
23
+ } catch (error) {
24
+ if (error.code === 'ENOENT') return false;
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ function listFilesRecursively(directory, root = directory) {
30
+ if (!fs.existsSync(directory)) return [];
31
+ return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
32
+ const sourcePath = path.join(directory, entry.name);
33
+ if (entry.isDirectory()) return listFilesRecursively(sourcePath, root);
34
+ return isRegularFile(sourcePath) ? [path.relative(root, sourcePath)] : [];
35
+ });
36
+ }
37
+
38
+ function getMissingReactBitsCorpusFiles(root) {
39
+ return REQUIRED_REACT_BITS_CORPUS_FILES.filter((file) => !isRegularFile(
40
+ path.join(root, REACT_BITS_CORPUS_RELATIVE_PATH, file),
41
+ ));
42
+ }
43
+
44
+ /**
45
+ * Copy only the shipped React Bits corpus. Existing destination-only files are
46
+ * intentionally left untouched so a project or global installation never
47
+ * deletes user research alongside the managed corpus.
48
+ */
49
+ function copyReactBitsCorpusSync(sourceRoot, destinationRoot) {
50
+ const source = path.join(sourceRoot, REACT_BITS_CORPUS_RELATIVE_PATH);
51
+ const destination = path.join(destinationRoot, REACT_BITS_CORPUS_RELATIVE_PATH);
52
+ const missingFiles = getMissingReactBitsCorpusFiles(sourceRoot);
53
+ if (missingFiles.length > 0) {
54
+ throw new Error(`Incomplete React Bits corpus source: ${missingFiles.join(', ')}`);
55
+ }
56
+ const files = listFilesRecursively(source);
57
+ for (const relativePath of files) {
58
+ const target = path.join(destination, relativePath);
59
+ fs.mkdirSync(path.dirname(target), { recursive: true });
60
+ fs.copyFileSync(path.join(source, relativePath), target);
61
+ }
62
+ return files.map((relativePath) => path.join(REACT_BITS_CORPUS_RELATIVE_PATH, relativePath));
63
+ }
64
+
65
+ function hasCompleteReactBitsCorpus(root) {
66
+ return getMissingReactBitsCorpusFiles(root).length === 0;
67
+ }
68
+
69
+ /** Copy the corpus plus its canonical skill into a global SINAPSE home. */
70
+ function copyReactBitsCapabilitySync(sourceRoot, destinationRoot) {
71
+ const files = copyReactBitsCorpusSync(sourceRoot, destinationRoot);
72
+ const sourceSkill = path.join(sourceRoot, REACT_BITS_SKILL_RELATIVE_PATH);
73
+ const destinationSkill = path.join(destinationRoot, REACT_BITS_SKILL_RELATIVE_PATH);
74
+ if (!fs.existsSync(sourceSkill)) throw new Error(`Missing React Bits skill: ${sourceSkill}`);
75
+ fs.mkdirSync(path.dirname(destinationSkill), { recursive: true });
76
+ fs.copyFileSync(sourceSkill, destinationSkill);
77
+ return { files, skillPath: destinationSkill };
78
+ }
79
+
80
+ module.exports = {
81
+ REACT_BITS_CORPUS_RELATIVE_PATH,
82
+ REACT_BITS_SKILL_RELATIVE_PATH,
83
+ REQUIRED_REACT_BITS_CORPUS_FILES,
84
+ isRegularFile,
85
+ listFilesRecursively,
86
+ getMissingReactBitsCorpusFiles,
87
+ copyReactBitsCorpusSync,
88
+ copyReactBitsCapabilitySync,
89
+ hasCompleteReactBitsCorpus,
90
+ };