5-phase-workflow 1.9.1 → 1.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,6 +141,7 @@ Claude Code exposes the workflow under the `/5:` namespace. Codex exposes the sa
141
141
  | `/5:quick-implement` or `$5-quick-implement` | Fast | Streamlined workflow for small tasks |
142
142
  | `/5:eject` or `$5-eject` | Utility | Permanently remove update infrastructure |
143
143
  | `/5:unlock` or `$5-unlock` | Utility | Remove planning guard lock |
144
+ | `/5:synchronize-agents` or `$5-synchronize-agents` | Utility | Sync user content between Claude Code and Codex runtimes |
144
145
 
145
146
  ## Configuration
146
147
 
@@ -278,7 +279,8 @@ After installation, your `.claude/` directory will contain:
278
279
  │ ├── quick-implement.md
279
280
  │ ├── configure.md
280
281
  │ ├── eject.md
281
- └── unlock.md
282
+ ├── unlock.md
283
+ │ └── synchronize-agents.md
282
284
  ├── skills/ # Atomic operations
283
285
  │ ├── build-project/
284
286
  │ ├── run-tests/
@@ -398,6 +400,26 @@ This permanently removes the update infrastructure:
398
400
 
399
401
  All other workflow files remain untouched. **This is irreversible.** To restore update functionality, reinstall with `npx 5-phase-workflow`.
400
402
 
403
+ ### Synchronizing Runtimes
404
+
405
+ If you have both Claude Code and Codex installed, user-generated content (project-specific skills, custom commands, rules) only exists in the runtime where it was created. To sync this content bidirectionally:
406
+
407
+ ```bash
408
+ # Claude Code
409
+ /5:synchronize-agents
410
+
411
+ # Codex
412
+ $5-synchronize-agents
413
+ ```
414
+
415
+ This will:
416
+ - Sync project-specific skills (e.g., `create-controller`, `run-tests`) between `.claude/skills/` and `.codex/skills/` with appropriate format conversion
417
+ - Convert custom Claude commands to Codex skills
418
+ - Append `.claude/rules/` content to `.codex/instructions.md`
419
+ - Sync Codex-only skills back to Claude
420
+
421
+ Workflow-managed files and shared data (`.5/`) are not affected — those are handled by the installer.
422
+
401
423
  ## Development
402
424
 
403
425
  ### Running Tests
package/bin/install.js CHANGED
@@ -847,6 +847,7 @@ function showCommandsHelp(isGlobal) {
847
847
  log.info(' $5-reconfigure - Refresh docs/skills (no Q&A)');
848
848
  log.info(' $5-eject - Eject from update mechanism');
849
849
  log.info(' $5-unlock - Remove planning guard lock');
850
+ log.info(' $5-synchronize-agents - Sync user content between runtimes');
850
851
  } else {
851
852
  log.info('Available commands:');
852
853
  log.info(' /5:plan-feature - Start feature planning (Phase 1)');
@@ -859,6 +860,7 @@ function showCommandsHelp(isGlobal) {
859
860
  log.info(' /5:reconfigure - Refresh docs/skills (no Q&A)');
860
861
  log.info(' /5:eject - Eject from update mechanism');
861
862
  log.info(' /5:unlock - Remove planning guard lock');
863
+ log.info(' /5:synchronize-agents - Sync user content between runtimes');
862
864
  }
863
865
  log.info('');
864
866
  log.info(`Config file: ${path.join(getDataPath(isGlobal), 'config.json')}`);
@@ -0,0 +1,639 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // ANSI colors for terminal output
7
+ const colors = {
8
+ reset: '\x1b[0m',
9
+ bright: '\x1b[1m',
10
+ green: '\x1b[32m',
11
+ yellow: '\x1b[33m',
12
+ blue: '\x1b[34m',
13
+ red: '\x1b[31m',
14
+ dim: '\x1b[2m'
15
+ };
16
+
17
+ const log = {
18
+ info: (msg) => console.log(`${colors.blue}i${colors.reset} ${msg}`),
19
+ success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
20
+ warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
21
+ error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
22
+ header: (msg) => console.log(`\n${colors.bright}${msg}${colors.reset}\n`),
23
+ dim: (msg) => console.log(` ${colors.dim}${msg}${colors.reset}`)
24
+ };
25
+
26
+ // ── Workflow-managed file lists (must match install.js) ────────────────────
27
+
28
+ const WORKFLOW_MANAGED_SKILLS = new Set([
29
+ 'configure-docs-index',
30
+ 'configure-project',
31
+ 'configure-skills',
32
+ 'generate-readme'
33
+ ]);
34
+
35
+ const WORKFLOW_MANAGED_AGENTS = new Set([
36
+ 'component-executor.md'
37
+ ]);
38
+
39
+ const RULES_SYNC_START = '<!-- 5-sync:rules-start -->';
40
+ const RULES_SYNC_END = '<!-- 5-sync:rules-end -->';
41
+ const AGENTS_SYNC_START = '<!-- 5-sync:agents-start -->';
42
+ const AGENTS_SYNC_END = '<!-- 5-sync:agents-end -->';
43
+
44
+ // ── Conversion functions (mirrors install.js logic) ────────────────────────
45
+
46
+ function extractFrontmatterAndBody(content) {
47
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
48
+ if (!match) return { frontmatter: null, body: content };
49
+ return { frontmatter: match[1], body: match[2] };
50
+ }
51
+
52
+ function extractFrontmatterField(frontmatter, field) {
53
+ if (!frontmatter) return null;
54
+ const match = frontmatter.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
55
+ return match ? match[1].trim() : null;
56
+ }
57
+
58
+ function claudeToCodexContent(content) {
59
+ return content
60
+ .replace(/\/5:([a-z0-9-]+)/g, (_, name) => `$5-${name}`)
61
+ .replace(/\.claude\//g, '.codex/');
62
+ }
63
+
64
+ function codexToClaudeContent(content) {
65
+ return content
66
+ .replace(/\$5-([a-z0-9-]+)/g, (_, name) => `/5:${name}`)
67
+ .replace(/\.codex\//g, '.claude/')
68
+ .replace(/<codex_skill_adapter>[\s\S]*?<\/codex_skill_adapter>\n*/g, '');
69
+ }
70
+
71
+ function getCodexSkillAdapterHeader(skillName) {
72
+ const invocation = `$${skillName}`;
73
+ return `<codex_skill_adapter>
74
+ ## Skill Invocation
75
+ - This skill is invoked by mentioning \`${invocation}\`.
76
+ - Treat all user text after \`${invocation}\` as the skill argument.
77
+
78
+ ## Tool Mapping (Claude Code → Codex)
79
+ This skill was authored for Claude Code. Map these tool references:
80
+
81
+ | Claude Code | Codex Equivalent |
82
+ |-------------|------------------|
83
+ | \`AskUserQuestion\` | Ask the user directly in conversation |
84
+ | \`Task(subagent_type="Explore")\` | Research the codebase yourself using available tools |
85
+ | \`Task(prompt="...")\` | \`spawn_agent(message="...")\` |
86
+ | \`Read\` | \`read_file\` |
87
+ | \`Write\` | \`write_file\` |
88
+ | \`Edit\` | \`patch\` |
89
+ | \`Bash\` | \`shell\` |
90
+ | \`Glob\` | \`glob\` / \`list_directory\` |
91
+ | \`Grep\` | \`grep\` / \`search\` |
92
+ | \`TaskCreate/TaskUpdate\` | Track progress internally |
93
+ | \`EnterPlanMode\` | Not available — use structured output instead |
94
+ </codex_skill_adapter>`;
95
+ }
96
+
97
+ function convertClaudeCommandToCodexSkill(content, skillName) {
98
+ const converted = claudeToCodexContent(content);
99
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
100
+
101
+ let description = `Custom command: ${skillName}`;
102
+ if (frontmatter) {
103
+ const maybeDesc = extractFrontmatterField(frontmatter, 'description');
104
+ if (maybeDesc) description = maybeDesc;
105
+ }
106
+
107
+ const shortDesc = description.length > 180 ? `${description.slice(0, 177)}...` : description;
108
+ const adapter = getCodexSkillAdapterHeader(skillName);
109
+
110
+ return `---
111
+ name: ${skillName}
112
+ description: ${description}
113
+ metadata:
114
+ short-description: ${shortDesc}
115
+ ---
116
+
117
+ ${adapter}
118
+
119
+ ${body.trimStart()}`;
120
+ }
121
+
122
+ function convertClaudeSkillToCodex(content) {
123
+ return claudeToCodexContent(content);
124
+ }
125
+
126
+ function convertCodexSkillToClaude(content) {
127
+ return codexToClaudeContent(content);
128
+ }
129
+
130
+ // ── File helpers ────────────────────────────────────────────────────────────
131
+
132
+ function copyDirContents(src, dest, transformMd) {
133
+ if (!fs.existsSync(dest)) {
134
+ fs.mkdirSync(dest, { recursive: true });
135
+ }
136
+ const entries = fs.readdirSync(src, { withFileTypes: true });
137
+ for (const entry of entries) {
138
+ const srcPath = path.join(src, entry.name);
139
+ const destPath = path.join(dest, entry.name);
140
+ if (entry.isDirectory()) {
141
+ copyDirContents(srcPath, destPath, transformMd);
142
+ } else if (transformMd && entry.name.endsWith('.md')) {
143
+ const content = fs.readFileSync(srcPath, 'utf8');
144
+ fs.writeFileSync(destPath, transformMd(content));
145
+ } else {
146
+ fs.copyFileSync(srcPath, destPath);
147
+ }
148
+ }
149
+ }
150
+
151
+ function dirContentsEqual(dirA, dirB, transformFn) {
152
+ if (!fs.existsSync(dirA) || !fs.existsSync(dirB)) return false;
153
+
154
+ const filesA = fs.readdirSync(dirA).sort();
155
+ const filesB = fs.readdirSync(dirB).sort();
156
+ if (filesA.length !== filesB.length) return false;
157
+
158
+ for (const file of filesA) {
159
+ if (!filesB.includes(file)) return false;
160
+ const pathA = path.join(dirA, file);
161
+ const pathB = path.join(dirB, file);
162
+ const statA = fs.statSync(pathA);
163
+ const statB = fs.statSync(pathB);
164
+ if (statA.isDirectory() !== statB.isDirectory()) return false;
165
+ if (statA.isDirectory()) {
166
+ if (!dirContentsEqual(pathA, pathB, transformFn)) return false;
167
+ } else {
168
+ let contentA = fs.readFileSync(pathA, 'utf8');
169
+ const contentB = fs.readFileSync(pathB, 'utf8');
170
+ if (file.endsWith('.md') && transformFn) {
171
+ contentA = transformFn(contentA);
172
+ }
173
+ if (contentA !== contentB) return false;
174
+ }
175
+ }
176
+ return true;
177
+ }
178
+
179
+ // ── Inventory ──────────────────────────────────────────────────────────────
180
+
181
+ function findProjectRoot() {
182
+ // Walk up from cwd to find .5/ or .claude/ or .codex/
183
+ let dir = process.cwd();
184
+ while (dir !== path.dirname(dir)) {
185
+ if (fs.existsSync(path.join(dir, '.5')) ||
186
+ fs.existsSync(path.join(dir, '.claude')) ||
187
+ fs.existsSync(path.join(dir, '.codex'))) {
188
+ return dir;
189
+ }
190
+ dir = path.dirname(dir);
191
+ }
192
+ return process.cwd();
193
+ }
194
+
195
+ function isClaudeInstalled(root) {
196
+ return fs.existsSync(path.join(root, '.claude', 'commands', '5', 'plan-feature.md'));
197
+ }
198
+
199
+ function isCodexInstalled(root) {
200
+ return fs.existsSync(path.join(root, '.codex', 'skills', '5-plan-feature', 'SKILL.md'));
201
+ }
202
+
203
+ function getClaudeUserSkills(root) {
204
+ const skillsDir = path.join(root, '.claude', 'skills');
205
+ if (!fs.existsSync(skillsDir)) return [];
206
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
207
+ .filter(d => d.isDirectory())
208
+ .map(d => d.name)
209
+ .filter(name => !WORKFLOW_MANAGED_SKILLS.has(name));
210
+ }
211
+
212
+ function getCodexUserSkills(root) {
213
+ const skillsDir = path.join(root, '.codex', 'skills');
214
+ if (!fs.existsSync(skillsDir)) return [];
215
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
216
+ .filter(d => d.isDirectory())
217
+ .map(d => d.name)
218
+ .filter(name => !name.startsWith('5-') && !WORKFLOW_MANAGED_SKILLS.has(name));
219
+ }
220
+
221
+ function getClaudeCustomCommands(root) {
222
+ const commandsDir = path.join(root, '.claude', 'commands');
223
+ if (!fs.existsSync(commandsDir)) return [];
224
+ const namespaces = fs.readdirSync(commandsDir, { withFileTypes: true })
225
+ .filter(d => d.isDirectory() && d.name !== '5')
226
+ .map(d => d.name);
227
+
228
+ const commands = [];
229
+ for (const ns of namespaces) {
230
+ const nsDir = path.join(commandsDir, ns);
231
+ const files = fs.readdirSync(nsDir).filter(f => f.endsWith('.md'));
232
+ for (const file of files) {
233
+ commands.push({ namespace: ns, name: file.replace('.md', ''), file });
234
+ }
235
+ }
236
+ return commands;
237
+ }
238
+
239
+ function getClaudeCustomAgents(root) {
240
+ const agentsDir = path.join(root, '.claude', 'agents');
241
+ if (!fs.existsSync(agentsDir)) return [];
242
+ return fs.readdirSync(agentsDir)
243
+ .filter(f => f.endsWith('.md') && !WORKFLOW_MANAGED_AGENTS.has(f));
244
+ }
245
+
246
+ function getClaudeRules(root) {
247
+ const rulesDir = path.join(root, '.claude', 'rules');
248
+ if (!fs.existsSync(rulesDir)) return [];
249
+ return fs.readdirSync(rulesDir).filter(f => f.endsWith('.md'));
250
+ }
251
+
252
+ // ── Sync action classification ─────────────────────────────────────────────
253
+
254
+ function classifySkillActions(root) {
255
+ const claudeSkills = getClaudeUserSkills(root);
256
+ const codexSkills = getCodexUserSkills(root);
257
+ const claudeSet = new Set(claudeSkills);
258
+ const codexSet = new Set(codexSkills);
259
+
260
+ const actions = [];
261
+
262
+ // Claude → Codex
263
+ for (const skill of claudeSkills) {
264
+ const claudeDir = path.join(root, '.claude', 'skills', skill);
265
+ const codexDir = path.join(root, '.codex', 'skills', skill);
266
+ if (!codexSet.has(skill)) {
267
+ actions.push({ type: 'new', direction: 'claude-to-codex', category: 'skill', name: skill });
268
+ } else if (!dirContentsEqual(claudeDir, codexDir, convertClaudeSkillToCodex)) {
269
+ actions.push({ type: 'updated', direction: 'claude-to-codex', category: 'skill', name: skill });
270
+ } else {
271
+ actions.push({ type: 'in-sync', direction: 'both', category: 'skill', name: skill });
272
+ }
273
+ }
274
+
275
+ // Codex → Claude
276
+ for (const skill of codexSkills) {
277
+ if (claudeSet.has(skill)) continue; // Already handled above
278
+ const claudeDir = path.join(root, '.claude', 'skills', skill);
279
+ if (!fs.existsSync(claudeDir)) {
280
+ actions.push({ type: 'new', direction: 'codex-to-claude', category: 'skill', name: skill });
281
+ }
282
+ }
283
+
284
+ return actions;
285
+ }
286
+
287
+ function classifyCommandActions(root) {
288
+ const commands = getClaudeCustomCommands(root);
289
+ const actions = [];
290
+
291
+ for (const cmd of commands) {
292
+ const skillName = `${cmd.namespace}-${cmd.name}`;
293
+ const codexSkillDir = path.join(root, '.codex', 'skills', skillName);
294
+ if (!fs.existsSync(codexSkillDir)) {
295
+ actions.push({ type: 'new', direction: 'claude-to-codex', category: 'command', name: `${cmd.namespace}/${cmd.name}`, skillName });
296
+ } else {
297
+ // Check if content changed
298
+ const claudeContent = fs.readFileSync(path.join(root, '.claude', 'commands', cmd.namespace, cmd.file), 'utf8');
299
+ const converted = convertClaudeCommandToCodexSkill(claudeContent, skillName);
300
+ const existing = fs.readFileSync(path.join(codexSkillDir, 'SKILL.md'), 'utf8');
301
+ if (converted !== existing) {
302
+ actions.push({ type: 'updated', direction: 'claude-to-codex', category: 'command', name: `${cmd.namespace}/${cmd.name}`, skillName });
303
+ } else {
304
+ actions.push({ type: 'in-sync', direction: 'both', category: 'command', name: `${cmd.namespace}/${cmd.name}` });
305
+ }
306
+ }
307
+ }
308
+
309
+ return actions;
310
+ }
311
+
312
+ function classifyRulesActions(root) {
313
+ const rules = getClaudeRules(root);
314
+ if (rules.length === 0) return [];
315
+
316
+ const instructionsPath = path.join(root, '.codex', 'instructions.md');
317
+ if (!fs.existsSync(instructionsPath)) {
318
+ return [{ type: 'new', direction: 'claude-to-codex', category: 'rules', name: `${rules.length} rule file(s)` }];
319
+ }
320
+
321
+ const instructions = fs.readFileSync(instructionsPath, 'utf8');
322
+ const newSection = buildRulesSection(root, rules);
323
+
324
+ // Extract existing section
325
+ const startIdx = instructions.indexOf(RULES_SYNC_START);
326
+ const endIdx = instructions.indexOf(RULES_SYNC_END);
327
+ if (startIdx === -1 || endIdx === -1) {
328
+ return [{ type: 'new', direction: 'claude-to-codex', category: 'rules', name: `${rules.length} rule file(s)` }];
329
+ }
330
+
331
+ const existing = instructions.substring(startIdx, endIdx + RULES_SYNC_END.length);
332
+ if (existing === newSection) {
333
+ return [{ type: 'in-sync', direction: 'both', category: 'rules', name: `${rules.length} rule file(s)` }];
334
+ }
335
+ return [{ type: 'updated', direction: 'claude-to-codex', category: 'rules', name: `${rules.length} rule file(s)` }];
336
+ }
337
+
338
+ function classifyAgentActions(root) {
339
+ const agents = getClaudeCustomAgents(root);
340
+ if (agents.length === 0) return [];
341
+
342
+ const instructionsPath = path.join(root, '.codex', 'instructions.md');
343
+ if (!fs.existsSync(instructionsPath)) {
344
+ return [{ type: 'new', direction: 'claude-to-codex', category: 'agents', name: `${agents.length} agent(s)` }];
345
+ }
346
+
347
+ const instructions = fs.readFileSync(instructionsPath, 'utf8');
348
+ const newSection = buildAgentsSection(root, agents);
349
+
350
+ const startIdx = instructions.indexOf(AGENTS_SYNC_START);
351
+ const endIdx = instructions.indexOf(AGENTS_SYNC_END);
352
+ if (startIdx === -1 || endIdx === -1) {
353
+ return [{ type: 'new', direction: 'claude-to-codex', category: 'agents', name: `${agents.length} agent(s)` }];
354
+ }
355
+
356
+ const existing = instructions.substring(startIdx, endIdx + AGENTS_SYNC_END.length);
357
+ if (existing === newSection) {
358
+ return [{ type: 'in-sync', direction: 'both', category: 'agents', name: `${agents.length} agent(s)` }];
359
+ }
360
+ return [{ type: 'updated', direction: 'claude-to-codex', category: 'agents', name: `${agents.length} agent(s)` }];
361
+ }
362
+
363
+ // ── Build sync content ─────────────────────────────────────────────────────
364
+
365
+ function buildRulesSection(root, ruleFiles) {
366
+ const rulesDir = path.join(root, '.claude', 'rules');
367
+ let section = `${RULES_SYNC_START}\n## Project Rules\n\n`;
368
+ section += '> Synced from .claude/rules/ — do not edit this section manually.\n';
369
+ section += '> Re-run /5:synchronize-agents (or $5-synchronize-agents) to update.\n\n';
370
+
371
+ for (const file of ruleFiles) {
372
+ const content = fs.readFileSync(path.join(rulesDir, file), 'utf8');
373
+ const { body } = extractFrontmatterAndBody(content);
374
+ const name = file.replace('.md', '');
375
+ section += `### ${name}\n\n${body.trim()}\n\n`;
376
+ }
377
+
378
+ section += RULES_SYNC_END;
379
+ return section;
380
+ }
381
+
382
+ function buildAgentsSection(root, agentFiles) {
383
+ const agentsDir = path.join(root, '.claude', 'agents');
384
+ let section = `${AGENTS_SYNC_START}\n## Custom Agent References\n\n`;
385
+ section += '> Synced from .claude/agents/ — do not edit this section manually.\n';
386
+ section += '> In Codex, use `spawn_agent()` with equivalent instructions.\n\n';
387
+
388
+ for (const file of agentFiles) {
389
+ const content = fs.readFileSync(path.join(agentsDir, file), 'utf8');
390
+ const converted = claudeToCodexContent(content);
391
+ const name = file.replace('.md', '');
392
+ section += `### ${name}\n\n${converted.trim()}\n\n`;
393
+ }
394
+
395
+ section += AGENTS_SYNC_END;
396
+ return section;
397
+ }
398
+
399
+ function updateInstructionsSection(instructionsContent, startMarker, endMarker, newSection) {
400
+ const startIdx = instructionsContent.indexOf(startMarker);
401
+ const endIdx = instructionsContent.indexOf(endMarker);
402
+
403
+ if (startIdx !== -1 && endIdx !== -1) {
404
+ // Replace existing section
405
+ return instructionsContent.substring(0, startIdx) +
406
+ newSection +
407
+ instructionsContent.substring(endIdx + endMarker.length);
408
+ }
409
+ // Append
410
+ return instructionsContent.trimEnd() + '\n\n' + newSection + '\n';
411
+ }
412
+
413
+ // ── Execute sync ───────────────────────────────────────────────────────────
414
+
415
+ function executeSync(root, actions) {
416
+ const counts = { 'claude-to-codex': { new: 0, updated: 0 }, 'codex-to-claude': { new: 0, updated: 0 }, synced: 0 };
417
+
418
+ for (const action of actions) {
419
+ if (action.type === 'in-sync') {
420
+ counts.synced++;
421
+ continue;
422
+ }
423
+
424
+ if (action.category === 'skill') {
425
+ if (action.direction === 'claude-to-codex') {
426
+ syncSkillClaudeToCodex(root, action.name);
427
+ } else {
428
+ syncSkillCodexToClaude(root, action.name);
429
+ }
430
+ counts[action.direction][action.type]++;
431
+ } else if (action.category === 'command') {
432
+ syncCommandClaudeToCodex(root, action);
433
+ counts['claude-to-codex'][action.type]++;
434
+ }
435
+ // rules and agents are handled separately in batch
436
+ }
437
+
438
+ // Sync rules to instructions.md
439
+ const ruleActions = actions.filter(a => a.category === 'rules' && a.type !== 'in-sync');
440
+ if (ruleActions.length > 0) {
441
+ syncRulesToInstructions(root);
442
+ for (const a of ruleActions) counts['claude-to-codex'][a.type]++;
443
+ }
444
+
445
+ // Sync agents to instructions.md
446
+ const agentActions = actions.filter(a => a.category === 'agents' && a.type !== 'in-sync');
447
+ if (agentActions.length > 0) {
448
+ syncAgentsToInstructions(root);
449
+ for (const a of agentActions) counts['claude-to-codex'][a.type]++;
450
+ }
451
+
452
+ return counts;
453
+ }
454
+
455
+ function syncSkillClaudeToCodex(root, skillName) {
456
+ const src = path.join(root, '.claude', 'skills', skillName);
457
+ const dest = path.join(root, '.codex', 'skills', skillName);
458
+ copyDirContents(src, dest, convertClaudeSkillToCodex);
459
+ log.dim(`skill: ${skillName} → .codex/skills/${skillName}/`);
460
+ }
461
+
462
+ function syncSkillCodexToClaude(root, skillName) {
463
+ const src = path.join(root, '.codex', 'skills', skillName);
464
+ const dest = path.join(root, '.claude', 'skills', skillName);
465
+ copyDirContents(src, dest, convertCodexSkillToClaude);
466
+ log.dim(`skill: ${skillName} → .claude/skills/${skillName}/`);
467
+ }
468
+
469
+ function syncCommandClaudeToCodex(root, action) {
470
+ const [ns, name] = action.name.split('/');
471
+ const srcFile = path.join(root, '.claude', 'commands', ns, `${name}.md`);
472
+ const content = fs.readFileSync(srcFile, 'utf8');
473
+ const converted = convertClaudeCommandToCodexSkill(content, action.skillName);
474
+
475
+ const destDir = path.join(root, '.codex', 'skills', action.skillName);
476
+ if (!fs.existsSync(destDir)) {
477
+ fs.mkdirSync(destDir, { recursive: true });
478
+ }
479
+ fs.writeFileSync(path.join(destDir, 'SKILL.md'), converted);
480
+ log.dim(`command: ${action.name} → .codex/skills/${action.skillName}/`);
481
+ }
482
+
483
+ function syncRulesToInstructions(root) {
484
+ const rules = getClaudeRules(root);
485
+ const section = buildRulesSection(root, rules);
486
+ const instructionsPath = path.join(root, '.codex', 'instructions.md');
487
+ let content = fs.existsSync(instructionsPath) ? fs.readFileSync(instructionsPath, 'utf8') : '';
488
+ content = updateInstructionsSection(content, RULES_SYNC_START, RULES_SYNC_END, section);
489
+ fs.writeFileSync(instructionsPath, content);
490
+ log.dim(`rules: ${rules.length} file(s) → .codex/instructions.md`);
491
+ }
492
+
493
+ function syncAgentsToInstructions(root) {
494
+ const agents = getClaudeCustomAgents(root);
495
+ const section = buildAgentsSection(root, agents);
496
+ const instructionsPath = path.join(root, '.codex', 'instructions.md');
497
+ let content = fs.existsSync(instructionsPath) ? fs.readFileSync(instructionsPath, 'utf8') : '';
498
+ content = updateInstructionsSection(content, AGENTS_SYNC_START, AGENTS_SYNC_END, section);
499
+ fs.writeFileSync(instructionsPath, content);
500
+ log.dim(`agents: ${agents.length} file(s) → .codex/instructions.md`);
501
+ }
502
+
503
+ // ── Display ────────────────────────────────────────────────────────────────
504
+
505
+ function formatActionLabel(type) {
506
+ if (type === 'new') return `${colors.green}NEW${colors.reset}`;
507
+ if (type === 'updated') return `${colors.yellow}UPD${colors.reset}`;
508
+ return `${colors.dim}OK${colors.reset}`;
509
+ }
510
+
511
+ function printSummary(actions) {
512
+ const claudeToCodex = actions.filter(a => a.direction === 'claude-to-codex');
513
+ const codexToClaude = actions.filter(a => a.direction === 'codex-to-claude');
514
+ const inSync = actions.filter(a => a.type === 'in-sync');
515
+
516
+ if (claudeToCodex.length > 0) {
517
+ console.log(' Claude → Codex:');
518
+ for (const a of claudeToCodex) {
519
+ console.log(` [${formatActionLabel(a.type)}] ${a.category}: ${a.name}`);
520
+ }
521
+ }
522
+
523
+ if (codexToClaude.length > 0) {
524
+ console.log(' Codex → Claude:');
525
+ for (const a of codexToClaude) {
526
+ console.log(` [${formatActionLabel(a.type)}] ${a.category}: ${a.name}`);
527
+ }
528
+ }
529
+
530
+ if (inSync.length > 0) {
531
+ console.log(` ${colors.dim}Already in sync: ${inSync.length} item(s)${colors.reset}`);
532
+ }
533
+ }
534
+
535
+ function printResults(counts) {
536
+ const c2c = counts['claude-to-codex'];
537
+ const c2cl = counts['codex-to-claude'];
538
+ const total = c2c.new + c2c.updated + c2cl.new + c2cl.updated;
539
+
540
+ if (total === 0 && counts.synced > 0) {
541
+ log.success(`Everything is already in sync (${counts.synced} item(s))`);
542
+ return;
543
+ }
544
+
545
+ console.log('');
546
+ if (c2c.new + c2c.updated > 0) {
547
+ console.log(` Claude → Codex: ${c2c.new} new, ${c2c.updated} updated`);
548
+ }
549
+ if (c2cl.new + c2cl.updated > 0) {
550
+ console.log(` Codex → Claude: ${c2cl.new} new, ${c2cl.updated} updated`);
551
+ }
552
+ if (counts.synced > 0) {
553
+ console.log(` ${colors.dim}Skipped: ${counts.synced} (already in sync)${colors.reset}`);
554
+ }
555
+ }
556
+
557
+ // ── Main ───────────────────────────────────────────────────────────────────
558
+
559
+ function main() {
560
+ const args = process.argv.slice(2);
561
+ const dryRun = args.includes('--dry-run');
562
+ const quiet = args.includes('--quiet');
563
+
564
+ const root = findProjectRoot();
565
+
566
+ if (!quiet) {
567
+ log.header('Synchronize Agent Runtimes');
568
+ }
569
+
570
+ // Step 1: Detect runtimes
571
+ const hasClaude = isClaudeInstalled(root);
572
+ const hasCodex = isCodexInstalled(root);
573
+
574
+ if (!hasClaude && !hasCodex) {
575
+ log.error('No runtime installations found.');
576
+ log.info('Install Claude Code: npx 5-phase-workflow');
577
+ log.info('Install Codex: npx 5-phase-workflow --codex');
578
+ process.exit(1);
579
+ }
580
+
581
+ if (!hasClaude) {
582
+ log.error('Claude Code runtime not installed.');
583
+ log.info('Install with: npx 5-phase-workflow');
584
+ process.exit(1);
585
+ }
586
+
587
+ if (!hasCodex) {
588
+ log.error('Codex runtime not installed.');
589
+ log.info('Install with: npx 5-phase-workflow --codex');
590
+ process.exit(1);
591
+ }
592
+
593
+ if (!quiet) {
594
+ log.success('Both runtimes detected');
595
+ }
596
+
597
+ // Step 2-3: Inventory and classify
598
+ const actions = [
599
+ ...classifySkillActions(root),
600
+ ...classifyCommandActions(root),
601
+ ...classifyRulesActions(root),
602
+ ...classifyAgentActions(root)
603
+ ];
604
+
605
+ if (actions.length === 0) {
606
+ log.info('No user-generated content found to synchronize.');
607
+ process.exit(0);
608
+ }
609
+
610
+ const actionable = actions.filter(a => a.type !== 'in-sync');
611
+
612
+ if (actionable.length === 0) {
613
+ log.success(`Everything is already in sync (${actions.length} item(s))`);
614
+ process.exit(0);
615
+ }
616
+
617
+ // Step 4: Show summary
618
+ if (!quiet) {
619
+ console.log('');
620
+ printSummary(actions);
621
+ console.log('');
622
+ }
623
+
624
+ if (dryRun) {
625
+ log.info('Dry run — no changes made.');
626
+ process.exit(0);
627
+ }
628
+
629
+ // Step 5: Execute
630
+ const counts = executeSync(root, actions);
631
+
632
+ // Step 6: Report
633
+ if (!quiet) {
634
+ log.header('Synchronization Complete');
635
+ printResults(counts);
636
+ }
637
+ }
638
+
639
+ main();