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 +23 -1
- package/bin/install.js +135 -62
- package/bin/sync-agents.js +639 -0
- package/package.json +1 -1
- package/src/commands/5/analyze-feature.md +159 -0
- package/src/commands/5/discuss-feature.md +2 -0
- package/src/commands/5/implement-feature.md +3 -0
- package/src/commands/5/plan-feature.md +36 -45
- package/src/commands/5/plan-implementation.md +10 -9
- package/src/commands/5/quick-implement.md +2 -1
- package/src/commands/5/synchronize-agents.md +60 -0
- package/src/templates/workflow/FEATURE-SPEC.md +60 -95
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
|
-
│
|
|
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
|
-
|
|
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
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
686
|
-
|
|
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
|
|
690
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
log.info(`Available: ${versionInfo.available}`);
|
|
1434
|
+
const primaryRuntime = activeRuntime;
|
|
1435
|
+
let anyUpdateAvailable = false;
|
|
1384
1436
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
|