sinapse-ai 7.7.3 → 7.7.4

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 (50) hide show
  1. package/.codex/catalog.json +157 -0
  2. package/.codex/command-registry.json +441 -0
  3. package/.codex/scripts/generate-codex-greeting.js +101 -0
  4. package/.codex/scripts/resolve-codex-command.js +147 -0
  5. package/.codex/skills/sinapse-analyst/SKILL.md +5 -4
  6. package/.codex/skills/sinapse-architect/SKILL.md +5 -4
  7. package/.codex/skills/sinapse-data-engineer/SKILL.md +5 -4
  8. package/.codex/skills/sinapse-dev/SKILL.md +5 -4
  9. package/.codex/skills/sinapse-devops/SKILL.md +5 -4
  10. package/.codex/skills/sinapse-orqx/SKILL.md +10 -15
  11. package/.codex/skills/sinapse-pm/SKILL.md +5 -4
  12. package/.codex/skills/sinapse-po/SKILL.md +4 -3
  13. package/.codex/skills/sinapse-qa/SKILL.md +12 -11
  14. package/.codex/skills/sinapse-sm/SKILL.md +5 -4
  15. package/.codex/skills/sinapse-squad-creator/SKILL.md +5 -4
  16. package/.codex/skills/sinapse-ux-design-expert/SKILL.md +5 -4
  17. package/.codex/tasks/convene-sinapse-council.md +28 -0
  18. package/.codex/tasks/create-sinapse-strategic-brief.md +29 -0
  19. package/.codex/tasks/onboard-sinapse-codex.md +34 -0
  20. package/.codex/tasks/plan-sinapse-initiative.md +33 -0
  21. package/.codex/tasks/resolve-sinapse-conflict.md +28 -0
  22. package/.codex/tasks/route-sinapse-request.md +33 -0
  23. package/.codex/tasks/status-sinapse-capabilities.md +28 -0
  24. package/.sinapse-ai/core-config.yaml +1 -1
  25. package/.sinapse-ai/data/entity-registry.yaml +848 -751
  26. package/.sinapse-ai/data/registry-update-log.jsonl +10 -0
  27. package/.sinapse-ai/infrastructure/scripts/codex-parity/catalog.js +123 -0
  28. package/.sinapse-ai/infrastructure/scripts/codex-skills-sync/index.js +60 -11
  29. package/.sinapse-ai/infrastructure/scripts/codex-skills-sync/validate.js +44 -16
  30. package/.sinapse-ai/infrastructure/scripts/sync-codex-local-first.js +156 -0
  31. package/.sinapse-ai/infrastructure/scripts/validate-codex-command-registry.js +264 -0
  32. package/.sinapse-ai/infrastructure/scripts/validate-codex-integration.js +15 -6
  33. package/.sinapse-ai/infrastructure/scripts/validate-codex-sync.js +156 -0
  34. package/.sinapse-ai/infrastructure/scripts/validate-parity.js +3 -1
  35. package/.sinapse-ai/infrastructure/scripts/validate-paths.js +8 -10
  36. package/.sinapse-ai/infrastructure/templates/safe-collab/README.md +8 -0
  37. package/.sinapse-ai/install-manifest.yaml +35 -19
  38. package/.sinapse-ai/project-config.yaml +1 -1
  39. package/bin/utils/collab-start.js +267 -0
  40. package/bin/utils/git-branch-guard.js +76 -0
  41. package/bin/utils/pre-push-safety.js +110 -0
  42. package/bin/utils/staged-secret-scan.js +108 -0
  43. package/docs/codex-parity-program.md +670 -0
  44. package/docs/codex-total-parity-orchestration-plan.md +301 -0
  45. package/docs/codex-workflow-task-parity.md +87 -0
  46. package/docs/collaboration-autonomy-plan.md +243 -0
  47. package/docs/guides/framework-contributor-mode.md +310 -0
  48. package/docs/guides/parallel-collaboration-source-of-truth.md +481 -0
  49. package/package.json +11 -3
  50. package/packages/installer/tests/unit/entity-registry-bootstrap.test.js +2 -2
@@ -557,3 +557,13 @@
557
557
  {"timestamp":"2026-04-02T01:18:15.933Z","action":"change","path":".sinapse-ai/core/doctor/checks/constitution-consistency.js","trigger":"watcher"}
558
558
  {"timestamp":"2026-04-02T01:18:15.934Z","action":"change","path":".sinapse-ai/core/health-check/checks/project/constitution-consistency.js","trigger":"watcher"}
559
559
  {"timestamp":"2026-04-02T01:18:15.934Z","action":"change","path":".sinapse-ai/product/templates/ide-rules/claude-rules.md","trigger":"watcher"}
560
+ {"timestamp":"2026-04-02T23:48:52.329Z","action":"change","path":".sinapse-ai/core-config.yaml","trigger":"watcher"}
561
+ {"timestamp":"2026-04-02T23:48:52.331Z","action":"add","path":".sinapse-ai/infrastructure/scripts/codex-parity/catalog.js","trigger":"watcher"}
562
+ {"timestamp":"2026-04-02T23:48:52.331Z","action":"change","path":".sinapse-ai/infrastructure/scripts/codex-skills-sync/index.js","trigger":"watcher"}
563
+ {"timestamp":"2026-04-02T23:48:52.331Z","action":"change","path":".sinapse-ai/infrastructure/scripts/codex-skills-sync/validate.js","trigger":"watcher"}
564
+ {"timestamp":"2026-04-02T23:48:52.332Z","action":"add","path":".sinapse-ai/infrastructure/scripts/sync-codex-local-first.js","trigger":"watcher"}
565
+ {"timestamp":"2026-04-02T23:48:52.332Z","action":"add","path":".sinapse-ai/infrastructure/scripts/validate-codex-command-registry.js","trigger":"watcher"}
566
+ {"timestamp":"2026-04-02T23:48:52.333Z","action":"change","path":".sinapse-ai/infrastructure/scripts/validate-codex-integration.js","trigger":"watcher"}
567
+ {"timestamp":"2026-04-02T23:48:52.333Z","action":"add","path":".sinapse-ai/infrastructure/scripts/validate-codex-sync.js","trigger":"watcher"}
568
+ {"timestamp":"2026-04-02T23:48:52.333Z","action":"change","path":".sinapse-ai/infrastructure/scripts/validate-parity.js","trigger":"watcher"}
569
+ {"timestamp":"2026-04-02T23:48:52.333Z","action":"change","path":".sinapse-ai/infrastructure/scripts/validate-paths.js","trigger":"watcher"}
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const DEFAULT_CONFIG = {
8
+ version: 1,
9
+ catalogMode: 'canonical',
10
+ canonicalAgentsDir: '.sinapse-ai/development/agents',
11
+ codexAgentsDir: '.codex/agents',
12
+ skillsDir: '.codex/skills',
13
+ expectedSkillIds: null,
14
+ generatedSkillMap: {},
15
+ canonicalSkillMap: {},
16
+ pathConventions: {
17
+ allowedSourcePrefixes: ['.sinapse-ai/development/agents/'],
18
+ allowedGreetingScriptPrefixes: ['.sinapse-ai/development/scripts/generate-greeting.js'],
19
+ greetingOptionalSourcePrefixes: [],
20
+ requiredFallbackBySourcePrefix: {},
21
+ },
22
+ };
23
+
24
+ function isPlainObject(value) {
25
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
26
+ }
27
+
28
+ function mergeConfig(base, override) {
29
+ const result = { ...base };
30
+ for (const [key, value] of Object.entries(override || {})) {
31
+ if (Array.isArray(value)) {
32
+ result[key] = [...value];
33
+ continue;
34
+ }
35
+ if (isPlainObject(value) && isPlainObject(base[key])) {
36
+ result[key] = mergeConfig(base[key], value);
37
+ continue;
38
+ }
39
+ result[key] = value;
40
+ }
41
+ return result;
42
+ }
43
+
44
+ function getCatalogConfigPath(projectRoot = process.cwd()) {
45
+ return path.join(projectRoot, '.codex', 'catalog.json');
46
+ }
47
+
48
+ function loadCodexCatalogConfig(projectRoot = process.cwd()) {
49
+ const configPath = getCatalogConfigPath(projectRoot);
50
+ if (!fs.existsSync(configPath)) {
51
+ return mergeConfig(DEFAULT_CONFIG, {});
52
+ }
53
+
54
+ const raw = fs.readFileSync(configPath, 'utf8');
55
+ const parsed = JSON.parse(raw);
56
+ return mergeConfig(DEFAULT_CONFIG, parsed);
57
+ }
58
+
59
+ function getConfiguredSkillIds(config) {
60
+ if (!Array.isArray(config.expectedSkillIds) || config.expectedSkillIds.length === 0) {
61
+ return null;
62
+ }
63
+ return [...config.expectedSkillIds].sort();
64
+ }
65
+
66
+ function getGeneratedSkillSpec(agentId, config) {
67
+ return config.generatedSkillMap?.[agentId] || null;
68
+ }
69
+
70
+ function hasAllowedPrefix(content, prefixes = []) {
71
+ return prefixes.some((prefix) => content.includes(prefix));
72
+ }
73
+
74
+ function detectMatchedSourcePrefix(content, prefixes = []) {
75
+ let matchedPrefix = null;
76
+ let matchedIndex = Number.POSITIVE_INFINITY;
77
+
78
+ for (const prefix of prefixes) {
79
+ const index = content.indexOf(prefix);
80
+ if (index === -1) continue;
81
+ if (index < matchedIndex) {
82
+ matchedIndex = index;
83
+ matchedPrefix = prefix;
84
+ }
85
+ }
86
+
87
+ return matchedPrefix;
88
+ }
89
+
90
+ function validateSkillActivationPaths(content, filePath, config) {
91
+ const rules = config.pathConventions || DEFAULT_CONFIG.pathConventions;
92
+ const errors = [];
93
+
94
+ const matchedSourcePrefix = detectMatchedSourcePrefix(content, rules.allowedSourcePrefixes);
95
+ if (!matchedSourcePrefix) {
96
+ errors.push(`${filePath} missing approved source path`);
97
+ return errors;
98
+ }
99
+
100
+ const requiredFallback = rules.requiredFallbackBySourcePrefix?.[matchedSourcePrefix];
101
+ if (requiredFallback && !content.includes(requiredFallback)) {
102
+ errors.push(`${filePath} missing fallback activation path "${requiredFallback}"`);
103
+ }
104
+
105
+ const greetingOptional = (rules.greetingOptionalSourcePrefixes || [])
106
+ .some((prefix) => matchedSourcePrefix.startsWith(prefix));
107
+
108
+ if (!greetingOptional && !hasAllowedPrefix(content, rules.allowedGreetingScriptPrefixes)) {
109
+ errors.push(`${filePath} missing approved greeting script path`);
110
+ }
111
+
112
+ return errors;
113
+ }
114
+
115
+ module.exports = {
116
+ DEFAULT_CONFIG,
117
+ mergeConfig,
118
+ getCatalogConfigPath,
119
+ loadCodexCatalogConfig,
120
+ getConfiguredSkillIds,
121
+ getGeneratedSkillSpec,
122
+ validateSkillActivationPaths,
123
+ };
@@ -10,6 +10,10 @@ const {
10
10
  normalizeCommands,
11
11
  getVisibleCommands,
12
12
  } = require('../ide-sync/agent-parser');
13
+ const {
14
+ loadCodexCatalogConfig,
15
+ getGeneratedSkillSpec,
16
+ } = require('../codex-parity/catalog');
13
17
 
14
18
  function getCodexHome() {
15
19
  return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
@@ -43,7 +47,28 @@ function getSkillId(agentId) {
43
47
  return `sinapse-${id}`;
44
48
  }
45
49
 
46
- function buildSkillContent(agentData) {
50
+ function resolveSkillSpec(agentData, config) {
51
+ const override = getGeneratedSkillSpec(agentData.id, config) || {};
52
+ const sourceOfTruth = override.sourceOfTruth || `.sinapse-ai/development/agents/${agentData.filename}`;
53
+ const fallbackSource = override.fallbackSource || (
54
+ sourceOfTruth === `.sinapse-ai/development/agents/${agentData.filename}`
55
+ ? `.codex/agents/${agentData.filename}`
56
+ : null
57
+ );
58
+
59
+ return {
60
+ skillId: override.skillId || getSkillId(agentData.id),
61
+ sourceOfTruth,
62
+ fallbackSource,
63
+ canonicalReference: override.canonicalReference || null,
64
+ greetingScript: override.greetingScript || '.sinapse-ai/development/scripts/generate-greeting.js',
65
+ greetingAgentId: override.greetingAgentId || agentData.id,
66
+ };
67
+ }
68
+
69
+ function buildSkillContent(agentData, options = {}) {
70
+ const config = options.config || loadCodexCatalogConfig(options.projectRoot || process.cwd());
71
+ const spec = resolveSkillSpec(agentData, config);
47
72
  const agent = agentData.agent || {};
48
73
  const name = agent.name || agentData.id;
49
74
  const title = agent.title || 'SINAPSE Agent';
@@ -57,8 +82,26 @@ function buildSkillContent(agentData) {
57
82
  .map(c => `- \`*${c.name}\` - ${c.description || 'No description'}`)
58
83
  .join('\n');
59
84
 
60
- const skillName = getSkillId(agentData.id);
85
+ const skillName = spec.skillId;
61
86
  const description = trimText(`${title} (${name}). ${whenToUse}`, 180);
87
+ const sourceOfTruthLine = spec.fallbackSource
88
+ ? `1. Load \`${spec.sourceOfTruth}\` as source of truth (fallback: \`${spec.fallbackSource}\`)`
89
+ : `1. Load \`${spec.sourceOfTruth}\` as source of truth`;
90
+ const canonicalReferenceLine = spec.canonicalReference
91
+ ? `2. Keep \`${spec.canonicalReference}\` as the shared parity reference.`
92
+ : null;
93
+ const greetingStepNumber = canonicalReferenceLine ? 3 : 2;
94
+ const personaStepNumber = canonicalReferenceLine ? 4 : 3;
95
+ const stayInPersonaStepNumber = canonicalReferenceLine ? 5 : 4;
96
+
97
+ const activationLines = [
98
+ `${sourceOfTruthLine}.`,
99
+ canonicalReferenceLine,
100
+ `${greetingStepNumber}. Generate greeting via \`node ${spec.greetingScript} ${spec.greetingAgentId}\` and show it first.`,
101
+ `${personaStepNumber}. Adopt this agent persona and command system.`,
102
+ `${stayInPersonaStepNumber}. If a starred command is invoked in Codex, resolve it via \`node .codex/scripts/resolve-codex-command.js ${skillName} <command>\` when a registry mapping exists.`,
103
+ `${stayInPersonaStepNumber + 1}. Stay in this persona until the user asks to switch or exit.`,
104
+ ].filter(Boolean);
62
105
 
63
106
  return `---
64
107
  name: ${skillName}
@@ -71,10 +114,7 @@ description: ${description}
71
114
  ${whenToUse}
72
115
 
73
116
  ## Activation Protocol
74
- 1. Load \`.sinapse-ai/development/agents/${agentData.filename}\` as source of truth (fallback: \`.codex/agents/${agentData.filename}\`).
75
- 2. Adopt this agent persona and command system.
76
- 3. Generate greeting via \`node .sinapse-ai/development/scripts/generate-greeting.js ${agentData.id}\` and show it first.
77
- 4. Stay in this persona until the user asks to switch or exit.
117
+ ${activationLines.join('\n')}
78
118
 
79
119
  ## Starter Commands
80
120
  ${commands || '- `*help` - List available commands'}
@@ -86,11 +126,13 @@ ${commands || '- `*help` - List available commands'}
86
126
  `;
87
127
  }
88
128
 
89
- function buildSkillPlan(agents, skillsDir) {
129
+ function buildSkillPlan(agents, skillsDir, options = {}) {
130
+ const config = options.config || loadCodexCatalogConfig(options.projectRoot || process.cwd());
90
131
  return agents
91
132
  .filter(a => !a.error || a.error === 'YAML parse failed, using fallback extraction')
92
133
  .map(agentData => {
93
- const skillId = getSkillId(agentData.id);
134
+ const spec = resolveSkillSpec(agentData, config);
135
+ const skillId = spec.skillId;
94
136
  const targetDir = path.join(skillsDir, skillId);
95
137
  const targetFile = path.join(targetDir, 'SKILL.md');
96
138
  return {
@@ -98,7 +140,7 @@ function buildSkillPlan(agents, skillsDir) {
98
140
  skillId,
99
141
  targetDir,
100
142
  targetFile,
101
- content: buildSkillContent(agentData),
143
+ content: buildSkillContent(agentData, { config, projectRoot: options.projectRoot }),
102
144
  };
103
145
  });
104
146
  }
@@ -121,15 +163,22 @@ function syncSkills(options = {}) {
121
163
  if (resolved.globalOnly) {
122
164
  resolved.global = true;
123
165
  }
166
+ const config = resolved.config || loadCodexCatalogConfig(resolved.projectRoot);
124
167
  const agents = parseAllAgents(resolved.sourceDir);
125
- const plan = buildSkillPlan(agents, resolved.localSkillsDir);
168
+ const plan = buildSkillPlan(agents, resolved.localSkillsDir, {
169
+ config,
170
+ projectRoot: resolved.projectRoot,
171
+ });
126
172
 
127
173
  if (!resolved.globalOnly) {
128
174
  writeSkillPlan(plan, resolved);
129
175
  }
130
176
 
131
177
  if (resolved.global) {
132
- const globalPlan = buildSkillPlan(agents, resolved.globalSkillsDir);
178
+ const globalPlan = buildSkillPlan(agents, resolved.globalSkillsDir, {
179
+ config,
180
+ projectRoot: resolved.projectRoot,
181
+ });
133
182
  writeSkillPlan(globalPlan, resolved);
134
183
  }
135
184
 
@@ -6,6 +6,11 @@ const path = require('path');
6
6
 
7
7
  const { parseAllAgents } = require('../ide-sync/agent-parser');
8
8
  const { getSkillId } = require('./index');
9
+ const {
10
+ loadCodexCatalogConfig,
11
+ getConfiguredSkillIds,
12
+ validateSkillActivationPaths,
13
+ } = require('../codex-parity/catalog');
9
14
 
10
15
  function getDefaultOptions() {
11
16
  const projectRoot = process.cwd();
@@ -32,30 +37,38 @@ function isParsableAgent(agent) {
32
37
  return !agent.error || agent.error === 'YAML parse failed, using fallback extraction';
33
38
  }
34
39
 
35
- function validateSkillContent(content, expected) {
40
+ function validateSkillContent(content, expected, config) {
36
41
  const issues = [];
37
42
  const requiredChecks = [
38
43
  { ok: content.includes(`name: ${expected.skillId}`), reason: `missing frontmatter name "${expected.skillId}"` },
39
- {
40
- ok: content.includes(`.sinapse-ai/development/agents/${expected.filename}`),
41
- reason: `missing canonical agent path "${expected.filename}"`,
42
- },
43
- {
44
- ok: content.includes(`generate-greeting.js ${expected.agentId}`),
45
- reason: `missing canonical greeting command for "${expected.agentId}"`,
46
- },
47
44
  {
48
45
  ok: content.includes('source of truth'),
49
46
  reason: 'missing source-of-truth activation note',
50
47
  },
51
48
  ];
52
49
 
50
+ if (expected.filename) {
51
+ requiredChecks.push({
52
+ ok: content.includes(`.sinapse-ai/development/agents/${expected.filename}`) || content.includes(expected.filename),
53
+ reason: `missing canonical agent path "${expected.filename}"`,
54
+ });
55
+ }
56
+
57
+ if (expected.greetingAgentId && content.includes('.sinapse-ai/development/scripts/generate-greeting.js')) {
58
+ requiredChecks.push({
59
+ ok: content.includes(`generate-greeting.js ${expected.greetingAgentId}`),
60
+ reason: `missing canonical greeting command for "${expected.greetingAgentId}"`,
61
+ });
62
+ }
63
+
53
64
  for (const check of requiredChecks) {
54
65
  if (!check.ok) {
55
66
  issues.push(check.reason);
56
67
  }
57
68
  }
58
69
 
70
+ issues.push(...validateSkillActivationPaths(content, expected.skillId, config));
71
+
59
72
  return issues;
60
73
  }
61
74
 
@@ -63,18 +76,33 @@ function validateCodexSkills(options = {}) {
63
76
  const resolved = { ...getDefaultOptions(), ...options };
64
77
  const errors = [];
65
78
  const warnings = [];
79
+ const config = loadCodexCatalogConfig(resolved.projectRoot);
66
80
 
67
81
  if (!fs.existsSync(resolved.skillsDir)) {
68
82
  errors.push(`Skills directory not found: ${resolved.skillsDir}`);
69
83
  return { ok: false, checked: 0, expected: 0, errors, warnings, missing: [], orphaned: [] };
70
84
  }
71
85
 
72
- const agents = parseAllAgents(resolved.sourceDir).filter(isParsableAgent);
73
- const expected = agents.map(agent => ({
74
- agentId: agent.id,
75
- filename: agent.filename,
76
- skillId: getSkillId(agent.id),
77
- }));
86
+ const configuredSkillIds = getConfiguredSkillIds(config);
87
+ let expected;
88
+
89
+ if (configuredSkillIds) {
90
+ const canonicalMap = config.canonicalSkillMap || {};
91
+ expected = configuredSkillIds.map((skillId) => ({
92
+ skillId,
93
+ agentId: canonicalMap[skillId]?.agentId || null,
94
+ greetingAgentId: canonicalMap[skillId]?.greetingAgentId || canonicalMap[skillId]?.agentId || null,
95
+ filename: canonicalMap[skillId]?.filename || null,
96
+ }));
97
+ } else {
98
+ const agents = parseAllAgents(resolved.sourceDir).filter(isParsableAgent);
99
+ expected = agents.map(agent => ({
100
+ agentId: agent.id,
101
+ greetingAgentId: agent.id,
102
+ filename: agent.filename,
103
+ skillId: getSkillId(agent.id),
104
+ }));
105
+ }
78
106
 
79
107
  const missing = [];
80
108
  for (const item of expected) {
@@ -92,7 +120,7 @@ function validateCodexSkills(options = {}) {
92
120
  errors.push(`${item.skillId}: unable to read skill file (${error.message})`);
93
121
  continue;
94
122
  }
95
- const issues = validateSkillContent(content, item);
123
+ const issues = validateSkillContent(content, item, config);
96
124
  for (const issue of issues) {
97
125
  errors.push(`${item.skillId}: ${issue}`);
98
126
  }
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const { syncSkills } = require('./codex-skills-sync/index');
8
+ const { loadCodexCatalogConfig } = require('./codex-parity/catalog');
9
+
10
+ function parseArgs(argv = process.argv.slice(2)) {
11
+ const args = new Set(argv);
12
+ return {
13
+ dryRun: args.has('--dry-run'),
14
+ quiet: args.has('--quiet') || args.has('-q'),
15
+ json: args.has('--json'),
16
+ };
17
+ }
18
+
19
+ function countMarkdownFiles(dirPath) {
20
+ if (!fs.existsSync(dirPath)) return 0;
21
+ return fs.readdirSync(dirPath).filter((entry) => entry.endsWith('.md')).length;
22
+ }
23
+
24
+ function countSkillFiles(skillsDir) {
25
+ if (!fs.existsSync(skillsDir)) return 0;
26
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
27
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('sinapse-'))
28
+ .filter((entry) => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
29
+ .length;
30
+ }
31
+
32
+ function runLegacyCodexSync(projectRoot, options = {}) {
33
+ const args = [
34
+ path.join('.sinapse-ai', 'infrastructure', 'scripts', 'ide-sync', 'index.js'),
35
+ 'sync',
36
+ '--ide',
37
+ 'codex',
38
+ ];
39
+
40
+ if (options.dryRun) args.push('--dry-run');
41
+ if (options.quiet) args.push('--quiet');
42
+
43
+ const result = spawnSync(process.execPath, args, {
44
+ cwd: projectRoot,
45
+ encoding: 'utf8',
46
+ });
47
+
48
+ return {
49
+ ok: result.status === 0,
50
+ mode: 'canonical',
51
+ delegated: true,
52
+ stdout: result.stdout || '',
53
+ stderr: result.stderr || '',
54
+ status: result.status || 0,
55
+ };
56
+ }
57
+
58
+ function syncCodexLocalFirst(options = {}, deps = {}) {
59
+ const projectRoot = options.projectRoot || process.cwd();
60
+ const config = loadCodexCatalogConfig(projectRoot);
61
+ const runLegacySync = deps.runLegacyCodexSync || runLegacyCodexSync;
62
+ const runSyncSkills = deps.syncSkills || syncSkills;
63
+ const errors = [];
64
+
65
+ if (config.catalogMode !== 'expanded') {
66
+ return runLegacySync(projectRoot, options);
67
+ }
68
+
69
+ const agentsDir = path.join(projectRoot, config.codexAgentsDir || '.codex/agents');
70
+ const skillsDir = path.join(projectRoot, config.skillsDir || '.codex/skills');
71
+ const sourceDir = path.join(projectRoot, config.canonicalAgentsDir || '.sinapse-ai/development/agents');
72
+
73
+ if (!fs.existsSync(agentsDir)) {
74
+ errors.push(`Missing Codex agents dir: ${path.relative(projectRoot, agentsDir)}`);
75
+ }
76
+
77
+ if (errors.length > 0) {
78
+ return {
79
+ ok: false,
80
+ mode: 'expanded',
81
+ delegated: false,
82
+ errors,
83
+ metrics: {
84
+ codexAgents: countMarkdownFiles(agentsDir),
85
+ codexSkills: 0,
86
+ generatedSkills: 0,
87
+ },
88
+ };
89
+ }
90
+
91
+ const syncResult = runSyncSkills({
92
+ projectRoot,
93
+ sourceDir,
94
+ localSkillsDir: skillsDir,
95
+ dryRun: Boolean(options.dryRun),
96
+ quiet: true,
97
+ config,
98
+ });
99
+
100
+ return {
101
+ ok: true,
102
+ mode: 'expanded',
103
+ delegated: false,
104
+ errors: [],
105
+ metrics: {
106
+ codexAgents: countMarkdownFiles(agentsDir),
107
+ codexSkills: countSkillFiles(skillsDir),
108
+ generatedSkills: syncResult.generated,
109
+ },
110
+ };
111
+ }
112
+
113
+ function formatHumanReport(result) {
114
+ if (result.delegated) {
115
+ return [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
116
+ }
117
+
118
+ if (!result.ok) {
119
+ return [
120
+ `X Codex local-first sync failed (${result.errors.length} issue(s))`,
121
+ ...result.errors.map((error) => `- ${error}`),
122
+ ].join('\n');
123
+ }
124
+
125
+ return `OK Codex local-first sync complete (agents preserved: ${result.metrics.codexAgents}, total skills: ${result.metrics.codexSkills}, skills regenerated: ${result.metrics.generatedSkills})`;
126
+ }
127
+
128
+ function main() {
129
+ const args = parseArgs();
130
+ const result = syncCodexLocalFirst(args);
131
+
132
+ if (!args.quiet) {
133
+ if (args.json) {
134
+ console.log(JSON.stringify(result, null, 2));
135
+ } else {
136
+ console.log(formatHumanReport(result));
137
+ }
138
+ }
139
+
140
+ if (!result.ok) {
141
+ process.exitCode = 1;
142
+ }
143
+ }
144
+
145
+ if (require.main === module) {
146
+ main();
147
+ }
148
+
149
+ module.exports = {
150
+ parseArgs,
151
+ syncCodexLocalFirst,
152
+ formatHumanReport,
153
+ runLegacyCodexSync,
154
+ countMarkdownFiles,
155
+ countSkillFiles,
156
+ };