sinapse-ai 1.25.0 → 1.25.2

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 (118) hide show
  1. package/.claude/settings.json +170 -0
  2. package/.codex/catalog.json +1 -1
  3. package/.codex/scripts/sinapse-codex.js +0 -5
  4. package/.codex/scripts/sync-codex-native.js +4 -37
  5. package/.codex/scripts/validate-codex-native.js +13 -23
  6. package/.codex/tasks/onboard-sinapse-codex.md +1 -1
  7. package/.sinapse-ai/data/entity-registry.yaml +44 -18
  8. package/.sinapse-ai/install-manifest.yaml +3 -3
  9. package/CHANGELOG.md +8 -14
  10. package/README.en.md +38 -30
  11. package/README.md +33 -25
  12. package/bin/cli.js +8 -1
  13. package/bin/commands/help.js +3 -2
  14. package/bin/commands/install.js +25 -14
  15. package/bin/commands/status.js +4 -3
  16. package/bin/commands/uninstall.js +216 -51
  17. package/bin/commands/update.js +8 -4
  18. package/bin/lib/global-provider-adapters.js +73 -10
  19. package/bin/lib/provider-parity.js +22 -0
  20. package/bin/postinstall.js +11 -11
  21. package/docs/framework/codex-parity/codex-integration-process.md +6 -7
  22. package/docs/framework/codex-parity/codex-native-runtime-v2.md +8 -8
  23. package/docs/framework/codex-parity/codex-parity-program.md +4 -4
  24. package/docs/framework/codex-parity/codex-total-parity-orchestration-plan.md +1 -1
  25. package/docs/getting-started.md +13 -5
  26. package/docs/guides/codex-config.md +35 -51
  27. package/docs/guides/development-setup.md +1 -2
  28. package/docs/guides/ide-integration.md +28 -29
  29. package/docs/guides/project-status-feature.md +35 -55
  30. package/docs/guides/quality-gates.md +2 -2
  31. package/docs/guides/user-guide.md +21 -10
  32. package/docs/ide-integration.md +2 -2
  33. package/docs/installation/README.md +4 -4
  34. package/docs/installation/faq.md +84 -70
  35. package/docs/installation/linux.md +11 -14
  36. package/docs/installation/macos.md +10 -4
  37. package/docs/installation/troubleshooting.md +11 -5
  38. package/docs/installation/v4-quick-start.md +14 -14
  39. package/docs/installation/windows.md +11 -15
  40. package/docs/pt/contributing.md +1 -1
  41. package/docs/pt/guides/development-setup.md +1 -2
  42. package/docs/pt/guides/project-status-feature.md +35 -55
  43. package/docs/pt/guides/quality-gates.md +2 -2
  44. package/docs/pt/guides/user-guide.md +21 -11
  45. package/docs/pt/ide-integration.md +6 -6
  46. package/docs/pt/installation/README.md +2 -4
  47. package/docs/pt/installation/faq.md +54 -89
  48. package/docs/pt/installation/linux.md +9 -27
  49. package/docs/pt/installation/macos.md +8 -12
  50. package/docs/pt/installation/troubleshooting.md +11 -10
  51. package/docs/pt/installation/v4-quick-start.md +17 -18
  52. package/docs/pt/installation/windows.md +8 -21
  53. package/docs/pt/platforms/claude-code.md +4 -4
  54. package/docs/sinapse-agent-flows/analyst-system.md +1 -1
  55. package/docs/sinapse-agent-flows/architect-system.md +1 -1
  56. package/docs/sinapse-agent-flows/data-engineer-system.md +1 -1
  57. package/docs/sinapse-agent-flows/dev-system.md +1 -1
  58. package/docs/sinapse-agent-flows/pm-system.md +1 -1
  59. package/docs/sinapse-agent-flows/qa-system.md +1 -1
  60. package/docs/sinapse-agent-flows/sm-system.md +1 -1
  61. package/docs/sinapse-agent-flows/snps-orqx-system.md +1 -1
  62. package/docs/sinapse-agent-flows/squad-creator-system.md +2 -2
  63. package/docs/sinapse-agent-flows/ux-design-expert-system.md +1 -1
  64. package/docs/troubleshooting.md +9 -7
  65. package/package.json +21 -16
  66. package/packages/installer/src/config/ide-configs.js +1 -1
  67. package/packages/installer/src/installer/sinapse-ai-installer.js +282 -10
  68. package/packages/installer/src/migrations/legacy-claude-agent-command-hashes.json +36 -0
  69. package/packages/installer/src/migrations/legacy-codex-native-skill-hashes.json +42 -0
  70. package/packages/installer/src/updater/index.js +24 -7
  71. package/packages/installer/src/wizard/ide-config-generator.js +4 -0
  72. package/packages/installer/src/wizard/index.js +6 -2
  73. package/scripts/validate-article-vii.js +305 -20
  74. package/scripts/validate-codex-command-registry.js +101 -0
  75. package/scripts/validate-install-docs.js +87 -3
  76. package/scripts/validate-provider-adapters.js +117 -1
  77. package/squads/claude-code-mastery/agents/skill-craftsman.md +2 -2
  78. package/squads/claude-code-mastery/knowledge-base/skill-creation-patterns.md +1 -1
  79. package/squads/squad-design/agents/ux-designer.md +1 -1
  80. package/.codex/skills/sinapse/SKILL.md +0 -10
  81. package/.codex/skills/sinapse-agent/SKILL.md +0 -22
  82. package/.codex/skills/sinapse-analyst/SKILL.md +0 -29
  83. package/.codex/skills/sinapse-animations/SKILL.md +0 -28
  84. package/.codex/skills/sinapse-architect/SKILL.md +0 -31
  85. package/.codex/skills/sinapse-brand/SKILL.md +0 -28
  86. package/.codex/skills/sinapse-claude/SKILL.md +0 -28
  87. package/.codex/skills/sinapse-cloning/SKILL.md +0 -28
  88. package/.codex/skills/sinapse-commercial/SKILL.md +0 -28
  89. package/.codex/skills/sinapse-content/SKILL.md +0 -28
  90. package/.codex/skills/sinapse-copy/SKILL.md +0 -28
  91. package/.codex/skills/sinapse-council/SKILL.md +0 -28
  92. package/.codex/skills/sinapse-courses/SKILL.md +0 -28
  93. package/.codex/skills/sinapse-cyber/SKILL.md +0 -28
  94. package/.codex/skills/sinapse-data-engineer/SKILL.md +0 -31
  95. package/.codex/skills/sinapse-design/SKILL.md +0 -28
  96. package/.codex/skills/sinapse-dev/SKILL.md +0 -31
  97. package/.codex/skills/sinapse-devops/SKILL.md +0 -31
  98. package/.codex/skills/sinapse-finance/SKILL.md +0 -28
  99. package/.codex/skills/sinapse-growth/SKILL.md +0 -28
  100. package/.codex/skills/sinapse-loop/SKILL.md +0 -35
  101. package/.codex/skills/sinapse-orqx/SKILL.md +0 -44
  102. package/.codex/skills/sinapse-paidmedia/SKILL.md +0 -28
  103. package/.codex/skills/sinapse-pm/SKILL.md +0 -31
  104. package/.codex/skills/sinapse-po/SKILL.md +0 -31
  105. package/.codex/skills/sinapse-product/SKILL.md +0 -28
  106. package/.codex/skills/sinapse-qa/SKILL.md +0 -31
  107. package/.codex/skills/sinapse-research/SKILL.md +0 -28
  108. package/.codex/skills/sinapse-sm/SKILL.md +0 -27
  109. package/.codex/skills/sinapse-spec-driven/SKILL.md +0 -35
  110. package/.codex/skills/sinapse-squad-creator/SKILL.md +0 -31
  111. package/.codex/skills/sinapse-storytelling/SKILL.md +0 -28
  112. package/.codex/skills/sinapse-swarm/SKILL.md +0 -28
  113. package/.codex/skills/sinapse-ux-design-expert/SKILL.md +0 -24
  114. package/.codex/skills/snps/SKILL.md +0 -10
  115. package/.codex/skills/snps-orqx/SKILL.md +0 -10
  116. /package/{.codex → .agents}/skills/react-bits-frontend/SKILL.md +0 -0
  117. /package/{.codex → .agents}/skills/react-bits-frontend/agents/openai.yaml +0 -0
  118. /package/{.codex → .agents}/skills/react-bits-frontend/references/corpus-map.md +0 -0
@@ -3,6 +3,8 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const crypto = require('crypto');
7
+ const legacyClaudeCommandHashes = require('../../packages/installer/src/migrations/legacy-claude-agent-command-hashes.json');
6
8
  const { getLogger } = require('../../.sinapse-ai/core/logger');
7
9
  const {
8
10
  HOME,
@@ -58,22 +60,61 @@ async function removeGitHooksConfig(projectDir = process.cwd()) {
58
60
  // and removes only those files (preserving anything the user added by hand).
59
61
  const INSTALLED_AGENTS_MANIFEST = path.join(SINAPSE_HOME, 'installed-agents.json');
60
62
 
61
- function recordInstalledAgents(filenames, ides) {
63
+ function installedAgentsManifestPath(home = HOME) {
64
+ return home === HOME
65
+ ? INSTALLED_AGENTS_MANIFEST
66
+ : path.join(home, '.sinapse', 'installed-agents.json');
67
+ }
68
+
69
+ function digestFile(filePath) {
62
70
  try {
63
- fs.mkdirSync(SINAPSE_HOME, { recursive: true });
64
- fs.writeFileSync(INSTALLED_AGENTS_MANIFEST, JSON.stringify({
65
- version: 1,
71
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function providerAgentPath(home, provider, filename) {
78
+ const providerDir = provider === 'codex'
79
+ ? path.join(home, '.codex', 'agents')
80
+ : path.join(home, '.claude', 'agents');
81
+ return path.join(providerDir, filename);
82
+ }
83
+
84
+ function inferProviderFromAgentDir(dir) {
85
+ const normalized = path.resolve(dir).replace(/\\/g, '/').toLowerCase();
86
+ if (normalized.includes('/.codex/agents')) return 'codex';
87
+ if (normalized.includes('/.claude/agents')) return 'claude-code';
88
+ return null;
89
+ }
90
+
91
+ function recordInstalledAgents(filenames, ides, home = HOME) {
92
+ try {
93
+ const manifestPath = installedAgentsManifestPath(home);
94
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
95
+ const artifacts = [...filenames].map((filename) => {
96
+ const provider = filename.endsWith('.toml') ? 'codex' : 'claude-code';
97
+ return {
98
+ provider,
99
+ filename,
100
+ sha256: digestFile(providerAgentPath(home, provider, filename)),
101
+ };
102
+ });
103
+ fs.writeFileSync(manifestPath, JSON.stringify({
104
+ version: 2,
66
105
  timestamp: new Date().toISOString(),
67
106
  ides,
68
107
  filenames: [...filenames].sort(),
108
+ artifacts: artifacts.sort((a, b) => `${a.provider}/${a.filename}`.localeCompare(`${b.provider}/${b.filename}`)),
69
109
  }, null, 2));
70
110
  } catch { /* non-critical */ }
71
111
  }
72
112
 
73
- function readInstalledAgentsManifest() {
113
+ function readInstalledAgentsManifest(home = HOME) {
74
114
  try {
75
- if (!fs.existsSync(INSTALLED_AGENTS_MANIFEST)) return null;
76
- const raw = fs.readFileSync(INSTALLED_AGENTS_MANIFEST, 'utf8');
115
+ const manifestPath = installedAgentsManifestPath(home);
116
+ if (!fs.existsSync(manifestPath)) return null;
117
+ const raw = fs.readFileSync(manifestPath, 'utf8');
77
118
  const parsed = JSON.parse(raw);
78
119
  if (!parsed || !Array.isArray(parsed.filenames)) return null;
79
120
  return parsed;
@@ -85,35 +126,36 @@ function readInstalledAgentsManifest() {
85
126
  function removeInstalledAgentsFrom(dir, manifestOverride = null) {
86
127
  if (!fs.existsSync(dir)) return { removed: 0, existed: false };
87
128
  let removed = 0;
129
+ const manifest = manifestOverride || readInstalledAgentsManifest();
130
+ const provider = inferProviderFromAgentDir(dir);
88
131
  let candidates;
89
132
 
90
- const manifest = manifestOverride || readInstalledAgentsManifest();
91
- if (manifest && Array.isArray(manifest.filenames)) {
92
- // Manifest path surgical removal of exactly what install wrote.
93
- candidates = manifest.filenames;
133
+ if (manifest?.version >= 2 && Array.isArray(manifest.artifacts)) {
134
+ candidates = manifest.artifacts.filter((artifact) => {
135
+ if (!artifact || typeof artifact.filename !== 'string') return false;
136
+ return provider ? artifact.provider === provider : true;
137
+ });
94
138
  } else {
95
- // Backward-compat fallback for installs predating the manifest
96
- // (rc.10 and earlier). Heuristic: SINAPSE-authored files end in
97
- // `-orqx.md`, OR start with `name: sinapse-` in their frontmatter.
98
- try {
99
- candidates = fs.readdirSync(dir).filter((f) => {
100
- if (f.endsWith('.toml')) {
101
- try { return /SINAPSE/i.test(fs.readFileSync(path.join(dir, f), 'utf8').slice(0, 4000)); } catch { return false; }
102
- }
103
- if (!f.endsWith('.md')) return false;
104
- if (/-orqx\.md$/.test(f)) return true;
105
- try {
106
- const head = fs.readFileSync(path.join(dir, f), 'utf8').slice(0, 600);
107
- return /^\s*name:\s*sinapse-/im.test(head);
108
- } catch { return false; }
109
- });
110
- } catch { return { removed: 0, existed: true }; }
139
+ // Legacy manifests did not record provider or digests. Ownership must be
140
+ // proven by the marker in the actual provider directory before deletion.
141
+ const filenames = Array.isArray(manifest?.filenames)
142
+ ? manifest.filenames
143
+ : fs.readdirSync(dir);
144
+ candidates = filenames.map((filename) => ({ filename, sha256: null, legacy: true }));
111
145
  }
112
146
 
113
- for (const f of candidates) {
114
- const filepath = path.join(dir, f);
147
+ for (const { filename, sha256, legacy } of candidates) {
148
+ const filepath = path.join(dir, filename);
115
149
  if (!fs.existsSync(filepath)) continue;
116
150
  try {
151
+ if (sha256) {
152
+ if (digestFile(filepath) !== sha256) continue;
153
+ } else {
154
+ // A v2 entry without a digest does not prove the installer wrote it.
155
+ if (!legacy) continue;
156
+ const content = fs.readFileSync(filepath, 'utf8');
157
+ if (!content.includes('SINAPSE-MANAGED:global-agent')) continue;
158
+ }
117
159
  fs.unlinkSync(filepath);
118
160
  removed++;
119
161
  } catch { /* best-effort */ }
@@ -128,44 +170,166 @@ function removeOrqxAgentsFrom(dir) {
128
170
  return removeInstalledAgentsFrom(dir);
129
171
  }
130
172
 
131
- const MANAGED_GLOBAL_SKILL_IDS = ['snps', 'sinapse', 'snps-orqx', 'sinapse-agent'];
173
+ const MANAGED_GLOBAL_SKILL_IDS = ['snps', 'sinapse', 'snps-orqx', 'sinapse-orqx', 'sinapse-agent'];
132
174
  const MANAGED_GLOBAL_SKILL_MARKER = 'SINAPSE-MANAGED:global-skill';
133
175
 
134
- function removeManagedGlobalSkills(home = HOME) {
176
+ function removeManagedGlobalSkills(home = HOME, options = {}) {
135
177
  let removed = 0;
136
- for (const skillId of MANAGED_GLOBAL_SKILL_IDS) {
137
- const skillDir = path.join(home, '.agents', 'skills', skillId);
138
- const skillPath = path.join(skillDir, 'SKILL.md');
139
- if (!fs.existsSync(skillPath)) continue;
140
- try {
141
- const content = fs.readFileSync(skillPath, 'utf8');
142
- if (!content.includes(MANAGED_GLOBAL_SKILL_MARKER)) continue;
143
- fs.unlinkSync(skillPath);
144
- if (fs.readdirSync(skillDir).length === 0) fs.rmdirSync(skillDir);
145
- removed++;
146
- } catch { /* best-effort */ }
178
+ const providers = options.providers || ['codex', 'claude-code'];
179
+ const skillRoots = [];
180
+ if (providers.includes('codex')) skillRoots.push(path.join(home, '.agents', 'skills'));
181
+ if (providers.includes('claude-code')) skillRoots.push(path.join(home, '.claude', 'skills'));
182
+ for (const skillsRoot of skillRoots) {
183
+ for (const skillId of MANAGED_GLOBAL_SKILL_IDS) {
184
+ const skillDir = path.join(skillsRoot, skillId);
185
+ const skillPath = path.join(skillDir, 'SKILL.md');
186
+ if (!fs.existsSync(skillPath)) continue;
187
+ try {
188
+ const content = fs.readFileSync(skillPath, 'utf8');
189
+ if (!content.includes(MANAGED_GLOBAL_SKILL_MARKER)) continue;
190
+ fs.unlinkSync(skillPath);
191
+ if (fs.readdirSync(skillDir).length === 0) fs.rmdirSync(skillDir);
192
+ removed++;
193
+ } catch { /* best-effort */ }
194
+ }
147
195
  }
148
196
  return { removed };
149
197
  }
150
198
 
151
199
  function reconcileInstalledAgents(home, desiredFilenames) {
152
- const manifest = readInstalledAgentsManifest();
153
- if (!manifest) return { removed: 0 };
200
+ const manifest = readInstalledAgentsManifest(home);
154
201
  const desired = new Set(desiredFilenames);
202
+ const desiredByProvider = {
203
+ 'claude-code': new Set([...desired].filter((name) => name.endsWith('.md'))),
204
+ codex: new Set([...desired].filter((name) => name.endsWith('.toml'))),
205
+ };
155
206
  let removed = 0;
156
- for (const filename of manifest.filenames) {
157
- if (desired.has(filename)) continue;
158
- const providerDir = filename.endsWith('.toml')
159
- ? path.join(home, '.codex', 'agents')
160
- : path.join(home, '.claude', 'agents');
161
- const filepath = path.join(providerDir, filename);
207
+ const legacyArtifacts = (manifest?.filenames || []).map((filename) => ({
208
+ provider: filename.endsWith('.toml') ? 'codex' : 'claude-code',
209
+ filename,
210
+ sha256: null,
211
+ }));
212
+ const manifestArtifacts = Array.isArray(manifest?.artifacts) ? manifest.artifacts : legacyArtifacts;
213
+ const manifestOwned = new Set(manifestArtifacts.map(({ provider, filename }) => `${provider}/${filename}`));
214
+ const versionedOwned = new Set(Array.isArray(manifest?.artifacts)
215
+ ? manifestArtifacts.map(({ provider, filename }) => `${provider}/${filename}`)
216
+ : []);
217
+ const managedMarker = 'SINAPSE-MANAGED:global-agent';
218
+ for (const { provider, filename, sha256 } of manifestArtifacts) {
219
+ if (!desiredByProvider[provider] || desiredByProvider[provider].has(filename)) continue;
220
+ const filepath = providerAgentPath(home, provider, filename);
162
221
  if (!fs.existsSync(filepath)) continue;
163
222
  try {
223
+ const content = fs.readFileSync(filepath, 'utf8');
224
+ const digestMatches = Boolean(sha256) && digestFile(filepath) === sha256;
225
+ const legacyMarkerMatches = !Array.isArray(manifest?.artifacts) && content.includes(managedMarker);
226
+ if (sha256 ? !digestMatches : !legacyMarkerMatches) continue;
164
227
  fs.unlinkSync(filepath);
165
228
  removed++;
166
229
  } catch { /* best-effort */ }
167
230
  }
168
- return { removed };
231
+
232
+ for (const [provider, providerDir, extension] of [
233
+ ['claude-code', path.join(home, '.claude', 'agents'), '.md'],
234
+ ['codex', path.join(home, '.codex', 'agents'), '.toml'],
235
+ ['codex', path.join(home, '.codex', 'agents'), '.md'],
236
+ ]) {
237
+ if (!fs.existsSync(providerDir)) continue;
238
+ for (const filename of fs.readdirSync(providerDir).filter((name) => name.endsWith(extension))) {
239
+ if (desiredByProvider[provider].has(filename)) continue;
240
+ const filepath = path.join(providerDir, filename);
241
+ try {
242
+ const content = fs.readFileSync(filepath, 'utf8');
243
+ if (versionedOwned.has(`${provider}/${filename}`)) continue;
244
+ if (!manifestOwned.has(`${provider}/${filename}`) && !content.includes(managedMarker)) continue;
245
+ // A manifest entry alone is not proof of current ownership: users can
246
+ // replace a formerly managed file. Only marked artifacts reach this
247
+ // sweep; exact recorded content was handled by the digest path above.
248
+ if (!content.includes(managedMarker)) continue;
249
+ fs.unlinkSync(filepath);
250
+ removed++;
251
+ } catch { /* best-effort */ }
252
+ }
253
+ }
254
+
255
+ const legacyCommands = path.join(home, '.claude', 'commands', 'SINAPSE', 'agents');
256
+ let legacyCommandDirectoryRemoved = false;
257
+ let legacyCommandsRemoved = 0;
258
+ let legacyCommandsPreserved = 0;
259
+ if (fs.existsSync(legacyCommands)) {
260
+ for (const entry of fs.readdirSync(legacyCommands, { withFileTypes: true })) {
261
+ const filePath = path.join(legacyCommands, entry.name);
262
+ if (!entry.isFile()) {
263
+ legacyCommandsPreserved++;
264
+ continue;
265
+ }
266
+ try {
267
+ const content = fs.readFileSync(filePath);
268
+ const digest = crypto.createHash('sha256').update(content).digest('hex');
269
+ const knownHashes = legacyClaudeCommandHashes.files[entry.name] || [];
270
+ if (!knownHashes.includes(digest)) {
271
+ legacyCommandsPreserved++;
272
+ continue;
273
+ }
274
+ fs.unlinkSync(filePath);
275
+ legacyCommandsRemoved++;
276
+ } catch {
277
+ legacyCommandsPreserved++;
278
+ }
279
+ }
280
+ try {
281
+ if (fs.readdirSync(legacyCommands).length === 0) {
282
+ fs.rmdirSync(legacyCommands);
283
+ legacyCommandDirectoryRemoved = true;
284
+ }
285
+ } catch { /* best-effort */ }
286
+ }
287
+ return {
288
+ removed,
289
+ legacyCommandDirectoryRemoved,
290
+ legacyCommandsRemoved,
291
+ legacyCommandsPreserved,
292
+ };
293
+ }
294
+
295
+ function hasManagedInstalledAgents(home = HOME) {
296
+ const manifest = readInstalledAgentsManifest(home);
297
+ if (!manifest) {
298
+ for (const [providerDir, extensions] of [
299
+ [path.join(home, '.claude', 'agents'), new Set(['.md'])],
300
+ [path.join(home, '.codex', 'agents'), new Set(['.toml', '.md'])],
301
+ ]) {
302
+ try {
303
+ for (const filename of fs.readdirSync(providerDir)) {
304
+ if (!extensions.has(path.extname(filename))) continue;
305
+ try {
306
+ const content = fs.readFileSync(path.join(providerDir, filename), 'utf8');
307
+ if (content.includes('SINAPSE-MANAGED:global-agent')) return true;
308
+ } catch { /* unreadable or non-regular entries are not evidence */ }
309
+ }
310
+ } catch { /* missing or unreadable provider dir */ }
311
+ }
312
+ return false;
313
+ }
314
+ const versioned = Array.isArray(manifest?.artifacts);
315
+ const artifacts = versioned
316
+ ? manifest.artifacts
317
+ : (manifest?.filenames || []).map((filename) => ({
318
+ provider: filename.endsWith('.toml') ? 'codex' : 'claude-code',
319
+ filename,
320
+ sha256: null,
321
+ }));
322
+ return artifacts.some(({ provider, filename, sha256 }) => {
323
+ const filePath = providerAgentPath(home, provider, filename);
324
+ try {
325
+ const content = fs.readFileSync(filePath, 'utf8');
326
+ return sha256
327
+ ? digestFile(filePath) === sha256
328
+ : !versioned && content.includes('SINAPSE-MANAGED:global-agent');
329
+ } catch {
330
+ return false;
331
+ }
332
+ });
169
333
  }
170
334
 
171
335
  // Story 10.40 — Strip SINAPSE-owned keys from ~/.claude/settings.json without
@@ -312,6 +476,7 @@ module.exports = {
312
476
  removeOrqxAgentsFrom,
313
477
  removeManagedGlobalSkills,
314
478
  reconcileInstalledAgents,
479
+ hasManagedInstalledAgents,
315
480
  cleanClaudeSettingsJson,
316
481
  removeGitHooksConfig,
317
482
  INSTALLED_AGENTS_MANIFEST,
@@ -36,6 +36,7 @@ const {
36
36
  } = require('./uninstall');
37
37
  const { regenerateAgentCommands } = require('../lib/command-generator');
38
38
  const { deliverGlobalProviderAdapters, getGlobalCommandStagingDir } = require('../lib/global-provider-adapters');
39
+ const { assertProviderAdapterParity } = require('../lib/provider-parity');
39
40
  const { execSync } = require('child_process');
40
41
 
41
42
  // Query the latest version published to npm. Returns null when npm is unreachable.
@@ -180,12 +181,14 @@ async function cmdUpdateGlobal() {
180
181
 
181
182
  // Phase 2b: Install global agents based on LLM choice
182
183
  const globalAdapters = deliverGlobalProviderAdapters({ llmChoice, home: HOME, commandsDir: commandStagingDir });
184
+ const activeAgentCount = assertProviderAdapterParity(llmChoice, globalAdapters, totalAgents);
183
185
  if (globalAdapters.claude.length) logger.always(` ${GREEN}OK${NC} Claude Code global agents (${globalAdapters.claude.length})`);
184
186
  if (globalAdapters.codex.length) logger.always(` ${GREEN}OK${NC} Codex global agents (${globalAdapters.codex.length} TOML, ${globalAdapters.skills.length} skills)`);
185
187
 
186
188
  const installedAgentFilenames = new Set([...globalAdapters.claude, ...globalAdapters.codex]);
187
189
  reconcileInstalledAgents(HOME, installedAgentFilenames);
188
- if (llmChoice === 'claude-code') removeManagedGlobalSkills(HOME);
190
+ if (llmChoice === 'claude-code') removeManagedGlobalSkills(HOME, { providers: ['codex'] });
191
+ if (llmChoice === 'codex') removeManagedGlobalSkills(HOME, { providers: ['claude-code'] });
189
192
  const installedIdes = [];
190
193
  if (globalAdapters.claude.length) installedIdes.push('claude-code');
191
194
  if (globalAdapters.codex.length) installedIdes.push('codex');
@@ -203,7 +206,8 @@ async function cmdUpdateGlobal() {
203
206
  meta.updatedAt = new Date().toISOString();
204
207
  meta.version = VERSION;
205
208
  meta.squads = squads.length;
206
- meta.commands = writtenAgents.size;
209
+ meta.agents = activeAgentCount;
210
+ meta.commands = activeAgentCount;
207
211
  meta.llm = llmChoice;
208
212
  fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
209
213
 
@@ -246,9 +250,9 @@ async function cmdUpdateGlobal() {
246
250
  logger.always(`${GREEN} SINAPSE AI atualizado para v${VERSION}!${NC}`);
247
251
  logger.always(`${GREEN}══════════════════════════════════════════════════════════════${NC}`);
248
252
  logger.always('');
249
- logger.always(` ${BOLD}${squads.length} squads${NC} | ${BOLD}${totalAgents} agents${NC} | ${BOLD}${writtenAgents.size} command files${NC}`);
253
+ logger.always(` ${BOLD}${squads.length} squads${NC} | ${BOLD}${activeAgentCount} agents${NC} | ${BOLD}${activeAgentCount} native adapters${NC}`);
250
254
  logger.always(` ${startCmd}`);
251
255
  logger.always('');
252
256
  }
253
257
 
254
- module.exports = { cmdUpdateGlobal };
258
+ module.exports = { cmdUpdateGlobal, assertProviderAdapterParity };
@@ -3,6 +3,22 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
+ const NOFOLLOW_READ_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
7
+
8
+ function readRegularFileNoFollowSync(filePath, encoding = null) {
9
+ let handle;
10
+ try {
11
+ handle = fs.openSync(filePath, NOFOLLOW_READ_FLAGS);
12
+ if (!fs.fstatSync(handle).isFile()) return null;
13
+ return fs.readFileSync(handle, encoding || undefined);
14
+ } catch (error) {
15
+ if (['ENOENT', 'ELOOP', 'EISDIR'].includes(error.code)) return null;
16
+ throw error;
17
+ } finally {
18
+ if (handle !== undefined) fs.closeSync(handle);
19
+ }
20
+ }
21
+
6
22
  function parseAgentMarkdown(content, fallbackName) {
7
23
  const name = content.match(/^name:\s*([^\r\n]+)$/m)?.[1]?.trim() || fallbackName;
8
24
  const raw = content.match(/^description:\s*(.+)$/m)?.[1]?.trim() || `SINAPSE ${name} agent`;
@@ -18,6 +34,15 @@ function renderCodexToml(name, description, instructions) {
18
34
  ].join('\n');
19
35
  }
20
36
 
37
+ function markGlobalAgent(markdown, fileName = '') {
38
+ let content = markdown;
39
+ if (fileName === 'snps-orqx.md') {
40
+ content = content.replace(/^name:\s*snps-orqx\s*$/m, 'name: sinapse-orqx');
41
+ }
42
+ if (content.includes('SINAPSE-MANAGED:global-agent')) return content;
43
+ return `${content.trimEnd()}\n\n<!-- SINAPSE-MANAGED:global-agent -->\n`;
44
+ }
45
+
21
46
  function renderGlobalSkill(skillId) {
22
47
  const generic = skillId === 'sinapse-agent';
23
48
  return [
@@ -28,7 +53,7 @@ function renderGlobalSkill(skillId) {
28
53
  '<!-- SINAPSE-MANAGED:global-skill -->', '',
29
54
  generic ? '# SINAPSE Global Agent Activator' : '# SINAPSE Global Orchestrator', '',
30
55
  generic
31
- ? 'Use the requested ID from `~/.codex/agents/<id>.toml`; reject unknown IDs without guessing.'
56
+ ? 'Resolve the requested ID through the native agent catalog of the active provider; reject unknown IDs without guessing.'
32
57
  : 'Resolve this alias to the `sinapse-orqx` custom agent and preserve its delegation-only authority.',
33
58
  'Canonical global persona and task sources live under `~/.sinapse/`.',
34
59
  'Use native Codex delegation and never start a nested CLI.', '',
@@ -88,25 +113,63 @@ function writeGlobalSkillIfManaged(skillPath, content) {
88
113
  }
89
114
  }
90
115
 
116
+ function removeStaleManagedAgents(targetDir, expectedFiles, extension) {
117
+ if (!fs.existsSync(targetDir)) return [];
118
+ const expected = new Set(expectedFiles);
119
+ 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);
132
+ }
133
+ return removed;
134
+ }
135
+
91
136
  function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir }) {
92
- const commandFiles = fs.existsSync(commandsDir)
137
+ let commandFiles = fs.existsSync(commandsDir)
93
138
  ? fs.readdirSync(commandsDir).filter((file) => file.endsWith('.md')).sort()
94
139
  : [];
95
- const written = { claude: [], codex: [], skills: [] };
140
+ if (commandFiles.includes('snps-orqx.md')) {
141
+ const aliasFiles = new Set(['sinapse.md', 'sinapse-orqx.md', 'snps.md']);
142
+ commandFiles = commandFiles.filter((file) => !aliasFiles.has(file));
143
+ }
144
+ const written = { claude: [], claudeSkills: [], codex: [], skills: [] };
96
145
 
97
146
  if (llmChoice === 'claude-code' || llmChoice === 'both') {
98
147
  const targetDir = path.join(home, '.claude', 'agents');
99
148
  fs.mkdirSync(targetDir, { recursive: true });
149
+ removeStaleManagedAgents(targetDir, commandFiles, '.md');
100
150
  for (const file of commandFiles) {
101
- fs.copyFileSync(path.join(commandsDir, file), path.join(targetDir, file));
151
+ const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
152
+ fs.writeFileSync(path.join(targetDir, file), markdown, 'utf8');
102
153
  written.claude.push(file);
103
154
  }
155
+ const skillsRoot = path.join(home, '.claude', 'skills');
156
+ for (const skillId of ['snps', 'sinapse', 'snps-orqx', 'sinapse-orqx', 'sinapse-agent']) {
157
+ const skillPath = path.join(skillsRoot, skillId, 'SKILL.md');
158
+ if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
159
+ written.claudeSkills.push(skillId);
160
+ }
161
+ }
104
162
  }
105
163
  if (llmChoice === 'codex' || llmChoice === 'both') {
106
164
  const targetDir = path.join(home, '.codex', 'agents');
107
165
  fs.mkdirSync(targetDir, { recursive: true });
166
+ removeStaleManagedAgents(
167
+ targetDir,
168
+ commandFiles.map((file) => file.replace(/\.md$/, '.toml')),
169
+ '.toml',
170
+ );
108
171
  for (const file of commandFiles) {
109
- const markdown = fs.readFileSync(path.join(commandsDir, file), 'utf8');
172
+ const markdown = markGlobalAgent(fs.readFileSync(path.join(commandsDir, file), 'utf8'), file);
110
173
  const id = file.replace(/\.md$/, '');
111
174
  const metadata = parseAgentMarkdown(markdown, id);
112
175
  const tomlName = `${id}.toml`;
@@ -116,7 +179,7 @@ function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir }) {
116
179
  written.codex.push(tomlName);
117
180
  }
118
181
  const skillsRoot = path.join(home, '.agents', 'skills');
119
- for (const skillId of ['snps', 'sinapse', 'snps-orqx', 'sinapse-agent']) {
182
+ for (const skillId of ['snps', 'sinapse', 'snps-orqx', 'sinapse-orqx', 'sinapse-agent']) {
120
183
  const skillDir = path.join(skillsRoot, skillId);
121
184
  const skillPath = path.join(skillDir, 'SKILL.md');
122
185
  if (writeGlobalSkillIfManaged(skillPath, renderGlobalSkill(skillId))) {
@@ -128,9 +191,9 @@ function deliverGlobalProviderAdapters({ llmChoice, home, commandsDir }) {
128
191
  }
129
192
 
130
193
  function getGlobalCommandStagingDir({ llmChoice, sinapseHome, claudeCommandsDir }) {
131
- return llmChoice === 'codex'
132
- ? path.join(sinapseHome, '.generated', 'agents')
133
- : claudeCommandsDir;
194
+ void llmChoice;
195
+ void claudeCommandsDir;
196
+ return path.join(sinapseHome, '.generated', 'agents');
134
197
  }
135
198
 
136
- module.exports = { parseAgentMarkdown, renderCodexToml, renderGlobalSkill, writeGlobalSkillIfManaged, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
199
+ module.exports = { parseAgentMarkdown, renderCodexToml, markGlobalAgent, renderGlobalSkill, writeGlobalSkillIfManaged, removeStaleManagedAgents, deliverGlobalProviderAdapters, getGlobalCommandStagingDir };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ function assertProviderAdapterParity(llmChoice, adapters, canonicalCount) {
4
+ if (!['claude-code', 'codex', 'both'].includes(llmChoice)) {
5
+ throw new Error(`Invalid LLM choice for provider parity: ${llmChoice}`);
6
+ }
7
+ const providerCounts = [];
8
+ if (llmChoice === 'claude-code' || llmChoice === 'both') {
9
+ providerCounts.push(['Claude Code', adapters.claude.length]);
10
+ }
11
+ if (llmChoice === 'codex' || llmChoice === 'both') {
12
+ providerCounts.push(['Codex', adapters.codex.length]);
13
+ }
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})`);
18
+ }
19
+ return canonicalCount;
20
+ }
21
+
22
+ module.exports = { assertProviderAdapterParity };
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Execution order (see Story A.1 AC 4):
19
19
  * 1. env guard (SINAPSE_SKIP_POSTINSTALL / CI)
20
- * 2. npm run sync:ide -- --ide claude-code
20
+ * 2. npm run sync:providers
21
21
  * 3. mkdir -p .sinapse/handoffs
22
22
  * 4. mkdir -p .sinapse/scratchpad
23
23
  * 5. sinapse doctor --quiet (exit code 2 = abort, 0/1 = OK)
@@ -30,7 +30,7 @@
30
30
  *
31
31
  * Exit codes:
32
32
  * 0 success OR non-critical warning (framework operational)
33
- * 2 critical failure (sync:ide failed, or doctor exit >= 2)
33
+ * 2 critical failure (provider sync failed, or doctor exit >= 2)
34
34
  *
35
35
  * Note: exit 1 is NOT used. npm treats any non-zero postinstall exit as
36
36
  * `command failed`, which scares users when the install is actually operational.
@@ -273,7 +273,7 @@ function run(command, args, opts = {}) {
273
273
  * Detect if we're running inside a global npm install.
274
274
  * Global installs land in /usr/local/lib/node_modules (Mac/Linux) or
275
275
  * %APPDATA%\npm\node_modules (Windows). Agent sync to those paths is pointless
276
- * — sync:ide must run inside a real project (per-project install).
276
+ * — sync:providers must run inside a real project (per-project install).
277
277
  */
278
278
  function isGlobalInstall() {
279
279
  if (process.env.npm_config_global === 'true') return true;
@@ -287,8 +287,8 @@ function isGlobalInstall() {
287
287
  }
288
288
 
289
289
  /**
290
- * Step 1: sync:ide --ide claude-code.
291
- * Copies agent definitions to .claude/commands/SINAPSE/agents/ so Claude Code can resolve @developer, etc.
290
+ * Step 1: synchronize native Claude and Codex provider adapters.
291
+ * Writes Claude agents to .claude/agents and Codex skills to .agents/skills.
292
292
  *
293
293
  * Skipped on global installs (no project to sync to). EACCES is treated as a
294
294
  * non-critical warning — the user can run `sinapse install` inside their project
@@ -296,17 +296,17 @@ function isGlobalInstall() {
296
296
  */
297
297
  function stepSyncIde() {
298
298
  if (isGlobalInstall()) {
299
- verboseLog(`${c.dim}sync:ide pulado (install global — rode 'sinapse install' no seu projeto)${c.reset}`);
299
+ verboseLog(`${c.dim}sync:providers pulado (install global — rode 'sinapse install' no seu projeto)${c.reset}`);
300
300
  return { ok: true, critical: false, skipped: true };
301
301
  }
302
- verboseLog(`${c.cyan}›${c.reset} Sincronizando agents para Claude Code...`);
303
- const result = run('npm', ['run', 'sync:ide', '--silent', '--', '--ide', 'claude-code', '--quiet']);
302
+ verboseLog(`${c.cyan}›${c.reset} Sincronizando adapters para Claude Code e Codex...`);
303
+ const result = run('npm', ['run', 'sync:providers', '--silent']);
304
304
  if (result.error) {
305
305
  if (result.error.code === 'EACCES' || result.error.code === 'EPERM') {
306
306
  warn("Sem permissão pra sincronizar agents. Rode 'sinapse install' dentro do seu projeto.");
307
307
  return { ok: false, critical: false };
308
308
  }
309
- warn(`sync:ide falhou ao iniciar: ${result.error.message} — rode 'sinapse install' no projeto pra completar.`);
309
+ warn(`sync:providers falhou ao iniciar: ${result.error.message} — rode 'sinapse install' no projeto pra completar.`);
310
310
  return { ok: false, critical: false };
311
311
  }
312
312
  if (result.status !== 0) {
@@ -315,7 +315,7 @@ function stepSyncIde() {
315
315
  warn("Sem permissão pra sincronizar agents (npm global sem write). Rode 'sinapse install' no seu projeto.");
316
316
  return { ok: false, critical: false };
317
317
  }
318
- warn(`sync:ide saiu com código ${result.status} — rode 'sinapse install' no projeto pra completar.`);
318
+ warn(`sync:providers saiu com código ${result.status} — rode 'sinapse install' no projeto pra completar.`);
319
319
  return { ok: false, critical: false };
320
320
  }
321
321
  verboseLog(`${c.green}✓${c.reset} Agents sincronizados`);
@@ -380,7 +380,7 @@ function stepGenerateSynapse() {
380
380
  return { ok: true, critical: false, skipped: true };
381
381
  }
382
382
  // Run in-process (not a subprocess) — faster, and keeps the shared run()
383
- // sequence (sync:ide → doctor) intact for callers/tests. The wrapper's
383
+ // sequence (sync:providers → doctor) intact for callers/tests. The wrapper's
384
384
  // generate() is tolerant and never throws; preserve our own exit code since
385
385
  // the underlying generator sets process.exitCode=1 on a miss.
386
386
  const savedExit = process.exitCode;