sinapse-ai 1.25.0 → 1.25.1

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 (100) 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 -16
  10. package/bin/cli.js +8 -1
  11. package/bin/commands/help.js +3 -2
  12. package/bin/commands/install.js +25 -14
  13. package/bin/commands/status.js +4 -3
  14. package/bin/commands/uninstall.js +216 -51
  15. package/bin/commands/update.js +8 -4
  16. package/bin/lib/global-provider-adapters.js +73 -10
  17. package/bin/lib/provider-parity.js +22 -0
  18. package/bin/postinstall.js +11 -11
  19. package/docs/framework/codex-parity/codex-integration-process.md +6 -7
  20. package/docs/framework/codex-parity/codex-native-runtime-v2.md +8 -8
  21. package/docs/framework/codex-parity/codex-parity-program.md +4 -4
  22. package/docs/framework/codex-parity/codex-total-parity-orchestration-plan.md +1 -1
  23. package/docs/guides/codex-config.md +2 -2
  24. package/docs/guides/development-setup.md +1 -2
  25. package/docs/guides/ide-integration.md +6 -6
  26. package/docs/guides/quality-gates.md +2 -2
  27. package/docs/ide-integration.md +2 -2
  28. package/docs/installation/faq.md +1 -1
  29. package/docs/installation/troubleshooting.md +2 -2
  30. package/docs/installation/v4-quick-start.md +1 -1
  31. package/docs/pt/contributing.md +1 -1
  32. package/docs/pt/guides/development-setup.md +1 -2
  33. package/docs/pt/guides/quality-gates.md +2 -2
  34. package/docs/pt/ide-integration.md +6 -6
  35. package/docs/pt/installation/faq.md +1 -1
  36. package/docs/pt/installation/troubleshooting.md +2 -3
  37. package/docs/pt/installation/v4-quick-start.md +1 -2
  38. package/docs/pt/platforms/claude-code.md +4 -4
  39. package/docs/sinapse-agent-flows/analyst-system.md +1 -1
  40. package/docs/sinapse-agent-flows/architect-system.md +1 -1
  41. package/docs/sinapse-agent-flows/data-engineer-system.md +1 -1
  42. package/docs/sinapse-agent-flows/dev-system.md +1 -1
  43. package/docs/sinapse-agent-flows/pm-system.md +1 -1
  44. package/docs/sinapse-agent-flows/qa-system.md +1 -1
  45. package/docs/sinapse-agent-flows/sm-system.md +1 -1
  46. package/docs/sinapse-agent-flows/snps-orqx-system.md +1 -1
  47. package/docs/sinapse-agent-flows/squad-creator-system.md +2 -2
  48. package/docs/sinapse-agent-flows/ux-design-expert-system.md +1 -1
  49. package/package.json +21 -16
  50. package/packages/installer/src/config/ide-configs.js +1 -1
  51. package/packages/installer/src/installer/sinapse-ai-installer.js +282 -10
  52. package/packages/installer/src/migrations/legacy-claude-agent-command-hashes.json +36 -0
  53. package/packages/installer/src/migrations/legacy-codex-native-skill-hashes.json +42 -0
  54. package/packages/installer/src/updater/index.js +24 -7
  55. package/packages/installer/src/wizard/ide-config-generator.js +4 -0
  56. package/packages/installer/src/wizard/index.js +6 -2
  57. package/scripts/validate-codex-command-registry.js +101 -0
  58. package/scripts/validate-provider-adapters.js +117 -1
  59. package/squads/claude-code-mastery/agents/skill-craftsman.md +2 -2
  60. package/squads/claude-code-mastery/knowledge-base/skill-creation-patterns.md +1 -1
  61. package/squads/squad-design/agents/ux-designer.md +1 -1
  62. package/.codex/skills/sinapse/SKILL.md +0 -10
  63. package/.codex/skills/sinapse-agent/SKILL.md +0 -22
  64. package/.codex/skills/sinapse-analyst/SKILL.md +0 -29
  65. package/.codex/skills/sinapse-animations/SKILL.md +0 -28
  66. package/.codex/skills/sinapse-architect/SKILL.md +0 -31
  67. package/.codex/skills/sinapse-brand/SKILL.md +0 -28
  68. package/.codex/skills/sinapse-claude/SKILL.md +0 -28
  69. package/.codex/skills/sinapse-cloning/SKILL.md +0 -28
  70. package/.codex/skills/sinapse-commercial/SKILL.md +0 -28
  71. package/.codex/skills/sinapse-content/SKILL.md +0 -28
  72. package/.codex/skills/sinapse-copy/SKILL.md +0 -28
  73. package/.codex/skills/sinapse-council/SKILL.md +0 -28
  74. package/.codex/skills/sinapse-courses/SKILL.md +0 -28
  75. package/.codex/skills/sinapse-cyber/SKILL.md +0 -28
  76. package/.codex/skills/sinapse-data-engineer/SKILL.md +0 -31
  77. package/.codex/skills/sinapse-design/SKILL.md +0 -28
  78. package/.codex/skills/sinapse-dev/SKILL.md +0 -31
  79. package/.codex/skills/sinapse-devops/SKILL.md +0 -31
  80. package/.codex/skills/sinapse-finance/SKILL.md +0 -28
  81. package/.codex/skills/sinapse-growth/SKILL.md +0 -28
  82. package/.codex/skills/sinapse-loop/SKILL.md +0 -35
  83. package/.codex/skills/sinapse-orqx/SKILL.md +0 -44
  84. package/.codex/skills/sinapse-paidmedia/SKILL.md +0 -28
  85. package/.codex/skills/sinapse-pm/SKILL.md +0 -31
  86. package/.codex/skills/sinapse-po/SKILL.md +0 -31
  87. package/.codex/skills/sinapse-product/SKILL.md +0 -28
  88. package/.codex/skills/sinapse-qa/SKILL.md +0 -31
  89. package/.codex/skills/sinapse-research/SKILL.md +0 -28
  90. package/.codex/skills/sinapse-sm/SKILL.md +0 -27
  91. package/.codex/skills/sinapse-spec-driven/SKILL.md +0 -35
  92. package/.codex/skills/sinapse-squad-creator/SKILL.md +0 -31
  93. package/.codex/skills/sinapse-storytelling/SKILL.md +0 -28
  94. package/.codex/skills/sinapse-swarm/SKILL.md +0 -28
  95. package/.codex/skills/sinapse-ux-design-expert/SKILL.md +0 -24
  96. package/.codex/skills/snps/SKILL.md +0 -10
  97. package/.codex/skills/snps-orqx/SKILL.md +0 -10
  98. /package/{.codex → .agents}/skills/react-bits-frontend/SKILL.md +0 -0
  99. /package/{.codex → .agents}/skills/react-bits-frontend/agents/openai.yaml +0 -0
  100. /package/{.codex → .agents}/skills/react-bits-frontend/references/corpus-map.md +0 -0
@@ -49,6 +49,7 @@ const {
49
49
  regenerateAgentCommands,
50
50
  } = require('../lib/command-generator');
51
51
  const { deliverGlobalProviderAdapters, getGlobalCommandStagingDir } = require('../lib/global-provider-adapters');
52
+ const { assertProviderAdapterParity } = require('../lib/provider-parity');
52
53
  // Follow-up #13 — wire the transactional backup/rollback engine into the
53
54
  // installer so an in-place UPGRADE (upsert) that fails mid-flight is restored
54
55
  // to its previous state instead of leaving ~/.sinapse half-updated.
@@ -64,6 +65,11 @@ async function cmdInstallGlobal(opts = {}) {
64
65
  // Story 10.20 — Upsert detection
65
66
  const force = Boolean(opts.force);
66
67
  const reconfigure = Boolean(opts.reconfigure);
68
+ const globalOnly = Boolean(opts.globalOnly);
69
+ const requestedLlm = opts.llm || null;
70
+ if (requestedLlm && !['claude-code', 'codex', 'both'].includes(requestedLlm)) {
71
+ throw new Error(`Invalid --llm value: ${requestedLlm}. Use claude-code, codex, or both.`);
72
+ }
67
73
  const existing = force ? { upsert: false } : detectExistingInstall();
68
74
  const isUpsert = existing.upsert;
69
75
 
@@ -126,7 +132,9 @@ async function cmdInstallGlobal(opts = {}) {
126
132
  // Story 10.35: --reconfigure forces prompt even in upsert mode
127
133
  let llmChoice;
128
134
  let llmWasReused = false;
129
- if (isUpsert && !reconfigure && existing.llm) {
135
+ if (requestedLlm) {
136
+ llmChoice = requestedLlm;
137
+ } else if (isUpsert && !reconfigure && existing.llm) {
130
138
  llmChoice = existing.llm;
131
139
  llmWasReused = true;
132
140
  const ideLabel = Array.isArray(llmChoice) ? llmChoice.join(', ') : String(llmChoice);
@@ -222,7 +230,7 @@ async function cmdInstallGlobal(opts = {}) {
222
230
  // project-wizard WARN must NOT revert an otherwise-good global install.
223
231
  const tx = createInstallTransaction();
224
232
  const txPaths = buildTransactionPaths(llmChoice);
225
- const { writtenAgents, totalAgents, totalDelta, squadsRefreshed, squadsAdded, meta } =
233
+ const { activeAgentCount, totalDelta, squadsRefreshed, squadsAdded, meta } =
226
234
  await runFatalPhasesTransactional({
227
235
  tx,
228
236
  isUpsert,
@@ -269,7 +277,10 @@ async function cmdInstallGlobal(opts = {}) {
269
277
 
270
278
  // Phase 8: Install project-local files (.sinapse-ai/, .claude/, .env)
271
279
  logger.always(`\n${CYAN}Phase 8:${NC} Installing project files in current directory`);
272
- try {
280
+ const skipProjectInstall = globalOnly || path.resolve(process.cwd()) === path.resolve(ROOT);
281
+ if (skipProjectInstall) {
282
+ logger.always(` ${YELLOW}SKIP${NC} Project files (${globalOnly ? '--global-only' : 'source checkout detected'})`);
283
+ } else try {
273
284
  const wizardPath = path.join(ROOT, 'packages', 'installer', 'src', 'wizard', 'index.js');
274
285
  if (fs.existsSync(wizardPath)) {
275
286
  const { runWizard: executeWizard } = require(wizardPath);
@@ -348,7 +359,7 @@ async function cmdInstallGlobal(opts = {}) {
348
359
  logger.always(`${YELLOW}══════════════════════════════════════════════════════════════${NC}`);
349
360
  }
350
361
  logger.always('');
351
- logger.always(` ${BOLD}${squads.length} squads${NC} | ${BOLD}${totalAgents} agents${NC} | ${BOLD}${writtenAgents.size} command files${NC}`);
362
+ logger.always(` ${BOLD}${squads.length} squads${NC} | ${BOLD}${activeAgentCount} agents${NC} | ${BOLD}${activeAgentCount} native adapters${NC}`);
352
363
  logger.always(` ${startCmd}`);
353
364
  logger.always('');
354
365
  logger.always(` ${BOLD}Try an agent:${NC}`);
@@ -402,6 +413,9 @@ function buildTransactionPaths(llmChoice) {
402
413
  CLAUDE_COMMANDS_DIR, // Phase 2 — generated /SINAPSE:agents:* command files
403
414
  ];
404
415
  if (llmChoice === 'claude-code' || llmChoice === 'both') dirs.push(path.join(HOME, '.claude', 'agents'));
416
+ // A Claude-only provider switch removes stale Codex aliases during the fatal
417
+ // phases, so that cleanup must be covered by the same upgrade rollback.
418
+ if (llmChoice === 'claude-code') dirs.push(path.join(HOME, '.agents', 'skills'));
405
419
  if (llmChoice === 'codex' || llmChoice === 'both') {
406
420
  dirs.push(path.join(HOME, '.codex', 'agents'));
407
421
  dirs.push(path.join(HOME, '.agents', 'skills'));
@@ -478,7 +492,7 @@ async function runFatalPhasesTransactional({ tx, isUpsert, paths, runPhases, onR
478
492
  * @param {string} ctx.llmChoice
479
493
  * @param {Object} ctx.existing - detectExistingInstall() result
480
494
  * @param {Object} ctx.logger
481
- * @returns {{ writtenAgents: Set, totalAgents: number, totalDelta: Object, squadsRefreshed: number, squadsAdded: number, meta: Object }}
495
+ * @returns {{ writtenAgents: Set, activeAgentCount: number, totalDelta: Object, squadsRefreshed: number, squadsAdded: number, meta: Object }}
482
496
  */
483
497
  function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing, logger }) {
484
498
  // Phase 1: Copy squads to ~/.sinapse/
@@ -486,7 +500,6 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
486
500
  fs.mkdirSync(SINAPSE_HOME, { recursive: true });
487
501
 
488
502
  const squadsSrcBase = fs.existsSync(squadsDir) ? squadsDir : ROOT;
489
- let totalAgents = 0;
490
503
  const totalDelta = { added: 0, updated: 0, unchanged: 0, removed: 0 };
491
504
  let squadsRefreshed = 0;
492
505
  let squadsAdded = 0;
@@ -507,7 +520,6 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
507
520
  copyDirSync(src, dest);
508
521
  logger.always(` ${GREEN}OK${NC} ${squad.name} (${squad.agents} agents)`);
509
522
  }
510
- totalAgents += squad.agents;
511
523
  }
512
524
 
513
525
  // Copy sinapse/ orqx squad
@@ -525,7 +537,6 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
525
537
  copyDirSync(sinapseMasterSrc, sinapseMasterDest);
526
538
  }
527
539
  const masterAgents = getAgentFiles(sinapseMasterDest).length;
528
- totalAgents += masterAgents;
529
540
  logger.always(` ${GREEN}OK${NC} sinapse (master, ${masterAgents} agents)`);
530
541
  }
531
542
 
@@ -543,14 +554,13 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
543
554
  copyDirSync(coreDevelopmentSrc, coreDevelopmentDest);
544
555
  }
545
556
  const coreAgents = getAgentFiles(coreDevelopmentDest).length;
546
- totalAgents += coreAgents;
547
557
  logger.always(` ${GREEN}OK${NC} core development (${coreAgents} agents)`);
548
558
  }
549
559
 
550
560
  // Phase 2: Generate agent commands (shared with `update` via command-generator).
551
561
  logger.always(`\n${CYAN}Phase 2:${NC} Generating agent commands`);
552
562
  const commandStagingDir = getGlobalCommandStagingDir({ llmChoice, sinapseHome: SINAPSE_HOME, claudeCommandsDir: CLAUDE_COMMANDS_DIR });
553
- const { writtenAgents } = regenerateAgentCommands({
563
+ const { writtenAgents, totalAgents } = regenerateAgentCommands({
554
564
  sinapseHome: SINAPSE_HOME,
555
565
  commandsDir: commandStagingDir,
556
566
  squads,
@@ -561,6 +571,7 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
561
571
 
562
572
  // Phase 2b: Install global agents based on LLM choice
563
573
  const globalAdapters = deliverGlobalProviderAdapters({ llmChoice, home: HOME, commandsDir: commandStagingDir });
574
+ const activeAgentCount = assertProviderAdapterParity(llmChoice, globalAdapters, totalAgents);
564
575
  const installedAgentFilenames = new Set([...globalAdapters.claude, ...globalAdapters.codex]);
565
576
  const installedIdes = [];
566
577
  if (globalAdapters.claude.length) installedIdes.push('claude-code');
@@ -571,7 +582,7 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
571
582
  // Audit 1 P0 (UN-1) — record manifest so uninstall can remove every file
572
583
  // we wrote (not just `*-orqx.md`). Idempotent: re-install overwrites.
573
584
  reconcileInstalledAgents(HOME, installedAgentFilenames);
574
- if (llmChoice === 'claude-code') removeManagedGlobalSkills(HOME);
585
+ if (llmChoice === 'claude-code') removeManagedGlobalSkills(HOME, { providers: ['codex'] });
575
586
  recordInstalledAgents(installedAgentFilenames, installedIdes);
576
587
 
577
588
  // Phase 3: Generate squad-awareness.md
@@ -595,8 +606,8 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
595
606
  ? existing.prevMeta.installedAt
596
607
  : nowIso,
597
608
  squads: squads.length,
598
- agents: totalAgents,
599
- commands: writtenAgents.size,
609
+ agents: activeAgentCount,
610
+ commands: activeAgentCount,
600
611
  llm: llmChoice,
601
612
  platform: process.platform,
602
613
  };
@@ -605,7 +616,7 @@ function installFatalPhases({ squads, squadsDir, isUpsert, llmChoice, existing,
605
616
  }
606
617
  atomicWriteFileSync(path.join(SINAPSE_HOME, 'metadata.json'), JSON.stringify(meta, null, 2));
607
618
 
608
- return { writtenAgents, totalAgents, totalDelta, squadsRefreshed, squadsAdded, meta };
619
+ return { writtenAgents, activeAgentCount, totalDelta, squadsRefreshed, squadsAdded, meta };
609
620
  }
610
621
 
611
622
  function generateSquadAwareness(sinapseDir, squads) {
@@ -5,9 +5,9 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { getLogger } = require('../../.sinapse-ai/core/logger');
7
7
  const {
8
+ HOME,
8
9
  ROOT,
9
10
  SINAPSE_HOME,
10
- CLAUDE_COMMANDS_DIR,
11
11
  BIN_DIR,
12
12
  CYAN,
13
13
  GREEN,
@@ -18,12 +18,13 @@ const {
18
18
  } = require('../lib/constants');
19
19
  const { header } = require('../lib/header');
20
20
  const { getSquads } = require('../lib/squads');
21
+ const { hasManagedInstalledAgents } = require('./uninstall');
21
22
 
22
23
  function verifyInstall() {
23
24
  const logger = getLogger();
24
25
  const checks = [
25
26
  [fs.existsSync(SINAPSE_HOME), `${getSquads(SINAPSE_HOME).length} squads in ~/.sinapse/`],
26
- [fs.existsSync(CLAUDE_COMMANDS_DIR), 'Commands in ~/.claude/commands/SINAPSE/agents/'],
27
+ [hasManagedInstalledAgents(HOME), 'Native provider agents installed'],
27
28
  [fs.existsSync(path.join(BIN_DIR, 'sinapse')), 'Launcher at ~/bin/sinapse'],
28
29
  [fs.existsSync(path.join(SINAPSE_HOME, '.claude', 'rules', 'squad-awareness.md')), 'squad-awareness.md'],
29
30
  ];
@@ -52,7 +53,7 @@ function cmdList() {
52
53
 
53
54
  logger.always('');
54
55
  logger.always(` ${BOLD}Total: ${GREEN}${totalAgents} agents${NC} | ${YELLOW}${totalTasks} tasks${NC}`);
55
- logger.always(` ${BOLD}Invoke: ${CYAN}/SINAPSE:agents:{agent-id}${NC}`);
56
+ logger.always(` ${BOLD}Invoke: ${CYAN}$sinapse-agent <agent-id>${NC}`);
56
57
  logger.always('');
57
58
  }
58
59
 
@@ -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 };