sinapse-ai 1.25.1 → 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.
Files changed (36) hide show
  1. package/.sinapse-ai/data/entity-registry.yaml +39 -14
  2. package/.sinapse-ai/install-manifest.yaml +3 -3
  3. package/CHANGELOG.md +2 -8
  4. package/README.en.md +38 -30
  5. package/README.md +33 -25
  6. package/bin/commands/install.js +6 -2
  7. package/bin/commands/update.js +6 -2
  8. package/bin/lib/command-generator.js +60 -36
  9. package/bin/lib/global-provider-adapters.js +244 -49
  10. package/bin/lib/provider-contract.js +23 -0
  11. package/bin/lib/provider-parity.js +60 -9
  12. package/docs/getting-started.md +13 -5
  13. package/docs/guides/codex-config.md +35 -51
  14. package/docs/guides/ide-integration.md +22 -23
  15. package/docs/guides/project-status-feature.md +35 -55
  16. package/docs/guides/user-guide.md +21 -10
  17. package/docs/installation/README.md +4 -4
  18. package/docs/installation/faq.md +84 -70
  19. package/docs/installation/linux.md +11 -14
  20. package/docs/installation/macos.md +10 -4
  21. package/docs/installation/troubleshooting.md +10 -4
  22. package/docs/installation/v4-quick-start.md +13 -13
  23. package/docs/installation/windows.md +11 -15
  24. package/docs/pt/guides/project-status-feature.md +35 -55
  25. package/docs/pt/guides/user-guide.md +21 -11
  26. package/docs/pt/installation/README.md +2 -4
  27. package/docs/pt/installation/faq.md +54 -89
  28. package/docs/pt/installation/linux.md +9 -27
  29. package/docs/pt/installation/macos.md +8 -12
  30. package/docs/pt/installation/troubleshooting.md +10 -8
  31. package/docs/pt/installation/v4-quick-start.md +16 -16
  32. package/docs/pt/installation/windows.md +8 -21
  33. package/docs/troubleshooting.md +9 -7
  34. package/package.json +1 -1
  35. package/scripts/validate-article-vii.js +305 -20
  36. package/scripts/validate-install-docs.js +87 -3
@@ -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 };
@@ -27,14 +27,22 @@ Se algo falhar: `npx sinapse-ai doctor --fix` corrige automaticamente.
27
27
 
28
28
  ## Ativar primeiro agente
29
29
 
30
- No prompt do Claude Code ou Codex:
30
+ No Claude Code:
31
31
 
32
- ```
32
+ ```text
33
33
  @developer
34
34
  *help
35
35
  ```
36
36
 
37
- `@developer` ativa o agente de implementação. `*help` lista comandos disponíveis.
37
+ No Codex, use o roteador ou a ativação direta:
38
+
39
+ ```text
40
+ $snps
41
+ $sinapse-agent developer
42
+ ```
43
+
44
+ `@developer` e `*help` pertencem à superfície do Claude Code. No Codex, `$snps`
45
+ roteia a solicitação e `$sinapse-agent developer` ativa o agente de implementação.
38
46
 
39
47
  ## Próximos passos
40
48
 
@@ -61,9 +69,9 @@ npx sinapse-ai uninstall # remove tudo
61
69
  ## Conceitos chave (em 30 segundos)
62
70
 
63
71
  - **Squad**: equipe de agentes especializados (ex: squad-design tem 15 agentes de design/UX)
64
- - **Agente**: persona de IA com expertise e comandos específicos (`@developer`, `@architect`, etc.)
72
+ - **Agente**: persona de IA com especialização e comandos específicos (`@developer` no Claude Code; `$sinapse-agent developer` no Codex)
65
73
  - **Hook**: enforcement runtime que bloqueia violações da Constitution (ex: PR sem story)
66
- - **Constitution**: 10 artigos que governam o framework (ver `.sinapse-ai/constitution.md`)
74
+ - **Constitution**: 11 artigos que governam o framework (ver `.sinapse-ai/constitution.md`)
67
75
  - **Story**: arquivo `.md` que documenta requisitos antes do código (Documentation-First)
68
76
 
69
77
  ---