5-phase-workflow 1.9.0 → 1.9.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.
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
@@ -38,13 +38,17 @@ function compareVersions(v1, v2) {
38
38
  }
39
39
 
40
40
  // Get installed version from .5/version.json
41
+ // Reads per-runtime version when available (runtimes[activeRuntime].packageVersion),
42
+ // falling back to top-level packageVersion for backward compatibility.
41
43
  function getInstalledVersion(isGlobal) {
42
44
  const versionFile = path.join(getDataPath(isGlobal), 'version.json');
43
45
  if (!fs.existsSync(versionFile)) return null;
44
46
 
45
47
  try {
46
48
  const data = JSON.parse(fs.readFileSync(versionFile, 'utf8'));
47
- return data.packageVersion;
49
+ return (data.runtimes && data.runtimes[activeRuntime] && data.runtimes[activeRuntime].packageVersion)
50
+ || data.packageVersion
51
+ || null;
48
52
  } catch (e) {
49
53
  return null; // Corrupted file, treat as missing
50
54
  }
@@ -624,7 +628,10 @@ function cleanupOrphanedFiles(targetPath, dataDir) {
624
628
  if (fs.existsSync(versionFile)) {
625
629
  try {
626
630
  const data = JSON.parse(fs.readFileSync(versionFile, 'utf8'));
627
- oldManifest = data.manifest || null;
631
+ // Prefer per-runtime manifest to avoid cross-runtime orphan cleanup
632
+ oldManifest = (data.runtimes && data.runtimes[activeRuntime] && data.runtimes[activeRuntime].manifest)
633
+ || data.manifest
634
+ || null;
628
635
  } catch (e) {
629
636
  // Corrupted file, treat as no manifest
630
637
  }
@@ -677,27 +684,73 @@ function ensureDotFiveGitignore(dataDir) {
677
684
  }
678
685
  }
679
686
 
680
- // Initialize version.json after successful install
681
- function initializeVersionJson(isGlobal) {
682
- const dataDir = getDataPath(isGlobal);
687
+ // Write (or update) version.json, preserving per-runtime state for other runtimes.
688
+ // - Reads existing file to preserve the other runtime's runtimes[] entry.
689
+ // - Writes runtimes[activeRuntime] with current version + manifest.
690
+ // - Top-level packageVersion = minimum across all runtime versions (so hooks
691
+ // detect an update if ANY runtime is behind).
692
+ function writeVersionJson(dataDir, isGlobal, version) {
683
693
  const versionFile = path.join(dataDir, 'version.json');
694
+ const now = new Date().toISOString();
684
695
 
685
- if (!fs.existsSync(dataDir)) {
686
- fs.mkdirSync(dataDir, { recursive: true });
696
+ let existing = {};
697
+ if (fs.existsSync(versionFile)) {
698
+ try {
699
+ existing = JSON.parse(fs.readFileSync(versionFile, 'utf8'));
700
+ } catch (e) {
701
+ existing = {};
702
+ }
687
703
  }
688
704
 
689
- const version = getPackageVersion();
690
- const now = new Date().toISOString();
691
-
692
- const versionData = {
705
+ const runtimes = existing.runtimes || {};
706
+ runtimes[activeRuntime] = {
693
707
  packageVersion: version,
694
- installedAt: now,
695
708
  lastUpdated: now,
696
- installationType: isGlobal ? 'global' : 'local',
697
709
  manifest: getFileManifest()
698
710
  };
699
711
 
712
+ // Top-level packageVersion = min across all runtime versions
713
+ const runtimeVersions = Object.values(runtimes).map(r => r.packageVersion).filter(Boolean);
714
+ const minVersion = runtimeVersions.reduce((min, v) => compareVersions(v, min) < 0 ? v : min, version);
715
+
716
+ const versionData = {
717
+ packageVersion: minVersion,
718
+ installedAt: existing.installedAt || now,
719
+ lastUpdated: now,
720
+ installationType: existing.installationType || (isGlobal ? 'global' : 'local'),
721
+ manifest: runtimes[activeRuntime].manifest,
722
+ runtimes
723
+ };
724
+
725
+ if (!fs.existsSync(dataDir)) {
726
+ fs.mkdirSync(dataDir, { recursive: true });
727
+ }
700
728
  fs.writeFileSync(versionFile, JSON.stringify(versionData, null, 2));
729
+ }
730
+
731
+ // Detect which runtimes are actually installed by probing marker files.
732
+ function getInstalledRuntimes(isGlobal) {
733
+ const installed = [];
734
+ const saved = activeRuntime;
735
+ for (const rt of ['claude', 'codex']) {
736
+ activeRuntime = rt;
737
+ const tp = getTargetPath(isGlobal);
738
+ if (checkExistingInstallation(tp)) installed.push(rt);
739
+ }
740
+ activeRuntime = saved;
741
+ return installed;
742
+ }
743
+
744
+ // Initialize version.json after successful install
745
+ function initializeVersionJson(isGlobal) {
746
+ const dataDir = getDataPath(isGlobal);
747
+
748
+ if (!fs.existsSync(dataDir)) {
749
+ fs.mkdirSync(dataDir, { recursive: true });
750
+ }
751
+
752
+ const version = getPackageVersion();
753
+ writeVersionJson(dataDir, isGlobal, version);
701
754
  ensureDotFiveGitignore(dataDir);
702
755
  log.success('Initialized version tracking');
703
756
  }
@@ -794,6 +847,7 @@ function showCommandsHelp(isGlobal) {
794
847
  log.info(' $5-reconfigure - Refresh docs/skills (no Q&A)');
795
848
  log.info(' $5-eject - Eject from update mechanism');
796
849
  log.info(' $5-unlock - Remove planning guard lock');
850
+ log.info(' $5-synchronize-agents - Sync user content between runtimes');
797
851
  } else {
798
852
  log.info('Available commands:');
799
853
  log.info(' /5:plan-feature - Start feature planning (Phase 1)');
@@ -806,6 +860,7 @@ function showCommandsHelp(isGlobal) {
806
860
  log.info(' /5:reconfigure - Refresh docs/skills (no Q&A)');
807
861
  log.info(' /5:eject - Eject from update mechanism');
808
862
  log.info(' /5:unlock - Remove planning guard lock');
863
+ log.info(' /5:synchronize-agents - Sync user content between runtimes');
809
864
  }
810
865
  log.info('');
811
866
  log.info(`Config file: ${path.join(getDataPath(isGlobal), 'config.json')}`);
@@ -868,25 +923,8 @@ function performUpdate(targetPath, sourcePath, isGlobal, versionInfo) {
868
923
  // Merge settings (deep merge preserves user customizations)
869
924
  mergeSettings(targetPath, sourcePath);
870
925
 
871
- // Update version.json
872
- const versionFile = path.join(dataDir, 'version.json');
873
- const now = new Date().toISOString();
874
-
875
- const existing = fs.existsSync(versionFile)
876
- ? JSON.parse(fs.readFileSync(versionFile, 'utf8'))
877
- : {};
878
- const versionData = {
879
- packageVersion: versionInfo.available,
880
- installedAt: existing.installedAt || now,
881
- lastUpdated: now,
882
- installationType: existing.installationType || (isGlobal ? 'global' : 'local'),
883
- manifest: getFileManifest()
884
- };
885
-
886
- if (!fs.existsSync(dataDir)) {
887
- fs.mkdirSync(dataDir, { recursive: true });
888
- }
889
- fs.writeFileSync(versionFile, JSON.stringify(versionData, null, 2));
926
+ // Update version.json (per-runtime, preserving other runtime's state)
927
+ writeVersionJson(dataDir, isGlobal, versionInfo.available);
890
928
  ensureDotFiveGitignore(dataDir);
891
929
 
892
930
  // Create features directory if it doesn't exist
@@ -1092,24 +1130,8 @@ function performCodexUpdate(targetPath, sourcePath, isGlobal, versionInfo) {
1092
1130
  const dataDir = getDataPath(isGlobal);
1093
1131
  cleanupOrphanedFiles(targetPath, dataDir);
1094
1132
 
1095
- // Update version.json
1096
- const versionFile = path.join(dataDir, 'version.json');
1097
- const now = new Date().toISOString();
1098
- const existing = fs.existsSync(versionFile)
1099
- ? JSON.parse(fs.readFileSync(versionFile, 'utf8'))
1100
- : {};
1101
- const versionData = {
1102
- packageVersion: versionInfo.available,
1103
- installedAt: existing.installedAt || now,
1104
- lastUpdated: now,
1105
- installationType: existing.installationType || (isGlobal ? 'global' : 'local'),
1106
- manifest: getFileManifest()
1107
- };
1108
-
1109
- if (!fs.existsSync(dataDir)) {
1110
- fs.mkdirSync(dataDir, { recursive: true });
1111
- }
1112
- fs.writeFileSync(versionFile, JSON.stringify(versionData, null, 2));
1133
+ // Update version.json (per-runtime, preserving other runtime's state)
1134
+ writeVersionJson(dataDir, isGlobal, versionInfo.available);
1113
1135
  ensureDotFiveGitignore(dataDir);
1114
1136
 
1115
1137
  const featuresDir = path.join(dataDir, 'features');
@@ -1216,6 +1238,7 @@ function install(isGlobal, forceUpgrade = false) {
1216
1238
  log.warn('Detected legacy installation (no version tracking)');
1217
1239
  log.info(`Upgrading from legacy install to ${versionInfo.available}`);
1218
1240
  update(targetPath, sourcePath, isGlobal, versionInfo);
1241
+ updateOtherRuntime(isGlobal);
1219
1242
  return;
1220
1243
  } else if (versionInfo.needsUpdate) {
1221
1244
  log.info(`Installed: ${versionInfo.installed}`);
@@ -1235,11 +1258,13 @@ function install(isGlobal, forceUpgrade = false) {
1235
1258
  return;
1236
1259
  }
1237
1260
  update(targetPath, sourcePath, isGlobal, versionInfo);
1261
+ updateOtherRuntime(isGlobal);
1238
1262
  });
1239
1263
  return; // Wait for user input
1240
1264
  }
1241
1265
  // Force upgrade, no prompt
1242
1266
  update(targetPath, sourcePath, isGlobal, versionInfo);
1267
+ updateOtherRuntime(isGlobal);
1243
1268
  return;
1244
1269
  } else {
1245
1270
  // Same version
@@ -1252,6 +1277,31 @@ function install(isGlobal, forceUpgrade = false) {
1252
1277
  freshInstall(targetPath, sourcePath, isGlobal);
1253
1278
  }
1254
1279
 
1280
+ // After updating the active runtime, check if the other runtime is also installed
1281
+ // and needs an update. Updates it silently if so.
1282
+ function updateOtherRuntime(isGlobal) {
1283
+ const primaryRuntime = activeRuntime;
1284
+ const installed = getInstalledRuntimes(isGlobal);
1285
+
1286
+ for (const rt of installed) {
1287
+ if (rt === primaryRuntime) continue;
1288
+
1289
+ activeRuntime = rt;
1290
+ const tp = getTargetPath(isGlobal);
1291
+ const sp = getSourcePath();
1292
+ const vi = getVersionInfo(tp, isGlobal);
1293
+
1294
+ if (vi.exists && (vi.needsUpdate || vi.legacy)) {
1295
+ const updateFn = rt === 'codex' ? performCodexUpdate : performUpdate;
1296
+ updateFn(tp, sp, isGlobal, vi);
1297
+ } else if (vi.exists) {
1298
+ log.success(`${rt}: already at version ${vi.installed}`);
1299
+ }
1300
+ }
1301
+
1302
+ activeRuntime = primaryRuntime;
1303
+ }
1304
+
1255
1305
  // Perform uninstallation
1256
1306
  function uninstall() {
1257
1307
  if (activeRuntime === 'codex') {
@@ -1369,25 +1419,48 @@ function main() {
1369
1419
  }
1370
1420
 
1371
1421
  if (options.check) {
1372
- const targetPath = getTargetPath(options.global);
1373
1422
  migrateDataDir(options.global);
1374
- const versionInfo = getVersionInfo(targetPath, options.global);
1375
1423
 
1376
- if (!versionInfo.exists) {
1377
- log.info(`Not installed (${activeRuntime})`);
1424
+ // When --codex is explicitly passed, check only Codex. Otherwise check all installed runtimes.
1425
+ const runtimesToCheck = options.runtime === 'codex'
1426
+ ? ['codex']
1427
+ : getInstalledRuntimes(options.global);
1428
+
1429
+ if (runtimesToCheck.length === 0) {
1430
+ log.info('Not installed');
1378
1431
  return;
1379
1432
  }
1380
1433
 
1381
- log.info(`Runtime: ${activeRuntime}`);
1382
- log.info(`Installed: ${versionInfo.installed || 'legacy (no version)'}`);
1383
- log.info(`Available: ${versionInfo.available}`);
1434
+ const primaryRuntime = activeRuntime;
1435
+ let anyUpdateAvailable = false;
1384
1436
 
1385
- if (versionInfo.needsUpdate) {
1386
- log.warn('Update available');
1387
- log.info(`Run: npx 5-phase-workflow${activeRuntime === 'codex' ? ' --codex' : ''} --upgrade`);
1388
- } else {
1389
- log.success('Up to date');
1437
+ for (const rt of runtimesToCheck) {
1438
+ activeRuntime = rt;
1439
+ const tp = getTargetPath(options.global);
1440
+ const vi = getVersionInfo(tp, options.global);
1441
+
1442
+ if (!vi.exists) {
1443
+ log.info(`${rt}: not installed`);
1444
+ continue;
1445
+ }
1446
+
1447
+ log.info(`Runtime: ${rt}`);
1448
+ log.info(`Installed: ${vi.installed || 'legacy (no version)'}`);
1449
+ log.info(`Available: ${vi.available}`);
1450
+
1451
+ if (vi.needsUpdate) {
1452
+ log.warn(`${rt}: update available`);
1453
+ anyUpdateAvailable = true;
1454
+ } else {
1455
+ log.success(`${rt}: up to date`);
1456
+ }
1390
1457
  }
1458
+
1459
+ if (anyUpdateAvailable) {
1460
+ log.info('Run: npx 5-phase-workflow --upgrade');
1461
+ }
1462
+
1463
+ activeRuntime = primaryRuntime;
1391
1464
  return;
1392
1465
  }
1393
1466