murmur8 3.5.0 → 4.1.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.
- package/.blueprint/agents/TEAM_MANIFESTO.md +1 -1
- package/README.md +93 -49
- package/SKILL.md +17 -1
- package/bin/cli.js +69 -48
- package/package.json +3 -2
- package/src/history.js +1 -11
- package/src/index.js +3 -0
- package/src/init.js +41 -5
- package/src/{parallel.js → murm.js} +150 -114
- package/src/theme.js +119 -0
|
@@ -4,15 +4,49 @@ const path = require('path');
|
|
|
4
4
|
const { execSync, spawn } = require('child_process');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const readline = require('readline');
|
|
7
|
+
const theme = require('./theme');
|
|
7
8
|
|
|
8
|
-
const CONFIG_FILE = '.claude/
|
|
9
|
-
const LOCK_FILE = '.claude/
|
|
9
|
+
const CONFIG_FILE = '.claude/murm-config.json';
|
|
10
|
+
const LOCK_FILE = '.claude/murm.lock';
|
|
11
|
+
const QUEUE_FILE = '.claude/murm-queue.json';
|
|
12
|
+
|
|
13
|
+
// Legacy paths for migration
|
|
14
|
+
const LEGACY_CONFIG_FILE = '.claude/parallel-config.json';
|
|
15
|
+
const LEGACY_LOCK_FILE = '.claude/parallel.lock';
|
|
16
|
+
const LEGACY_QUEUE_FILE = '.claude/parallel-queue.json';
|
|
10
17
|
|
|
11
18
|
// Track running processes for abort handling
|
|
12
19
|
let runningProcesses = new Map();
|
|
13
20
|
let isAborting = false;
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Migrate a legacy file path to the new path.
|
|
24
|
+
* If the old file exists and the new one doesn't, rename it.
|
|
25
|
+
*/
|
|
26
|
+
function migrateFile(oldPath, newPath) {
|
|
27
|
+
if (fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
|
|
28
|
+
const dir = path.dirname(newPath);
|
|
29
|
+
if (!fs.existsSync(dir)) {
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
fs.renameSync(oldPath, newPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run all legacy path migrations.
|
|
38
|
+
* Called once per process on first config/queue/lock access.
|
|
39
|
+
*/
|
|
40
|
+
let migrationDone = false;
|
|
41
|
+
function ensureMigrated() {
|
|
42
|
+
if (migrationDone) return;
|
|
43
|
+
migrationDone = true;
|
|
44
|
+
migrateFile(LEGACY_CONFIG_FILE, CONFIG_FILE);
|
|
45
|
+
migrateFile(LEGACY_LOCK_FILE, LOCK_FILE);
|
|
46
|
+
migrateFile(LEGACY_QUEUE_FILE, QUEUE_FILE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getDefaultMurmConfig() {
|
|
16
50
|
return {
|
|
17
51
|
maxConcurrency: 3,
|
|
18
52
|
maxFeatures: 10,
|
|
@@ -22,23 +56,29 @@ function getDefaultParallelConfig() {
|
|
|
22
56
|
skill: '/implement-feature',
|
|
23
57
|
skillFlags: '--no-commit',
|
|
24
58
|
worktreeDir: '.claude/worktrees',
|
|
25
|
-
queueFile:
|
|
59
|
+
queueFile: QUEUE_FILE
|
|
26
60
|
};
|
|
27
61
|
}
|
|
28
62
|
|
|
29
|
-
function
|
|
63
|
+
function readMurmConfig() {
|
|
64
|
+
ensureMigrated();
|
|
30
65
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
31
|
-
return
|
|
66
|
+
return getDefaultMurmConfig();
|
|
32
67
|
}
|
|
33
68
|
try {
|
|
34
69
|
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
35
|
-
|
|
70
|
+
const parsed = JSON.parse(content);
|
|
71
|
+
// Migrate legacy queueFile value in config
|
|
72
|
+
if (parsed.queueFile === LEGACY_QUEUE_FILE) {
|
|
73
|
+
parsed.queueFile = QUEUE_FILE;
|
|
74
|
+
}
|
|
75
|
+
return { ...getDefaultMurmConfig(), ...parsed };
|
|
36
76
|
} catch {
|
|
37
|
-
return
|
|
77
|
+
return getDefaultMurmConfig();
|
|
38
78
|
}
|
|
39
79
|
}
|
|
40
80
|
|
|
41
|
-
function
|
|
81
|
+
function writeMurmConfig(config) {
|
|
42
82
|
const dir = path.dirname(CONFIG_FILE);
|
|
43
83
|
if (!fs.existsSync(dir)) {
|
|
44
84
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -47,13 +87,11 @@ function writeParallelConfig(config) {
|
|
|
47
87
|
}
|
|
48
88
|
|
|
49
89
|
function getQueueFile() {
|
|
50
|
-
return
|
|
90
|
+
return readMurmConfig().queueFile;
|
|
51
91
|
}
|
|
52
92
|
|
|
53
|
-
const QUEUE_FILE = '.claude/parallel-queue.json'; // Legacy reference
|
|
54
|
-
|
|
55
93
|
function buildWorktreePath(slug, config = null) {
|
|
56
|
-
const cfg = config ||
|
|
94
|
+
const cfg = config || readMurmConfig();
|
|
57
95
|
return `${cfg.worktreeDir}/feat-${slug}`;
|
|
58
96
|
}
|
|
59
97
|
|
|
@@ -62,7 +100,7 @@ function buildBranchName(slug) {
|
|
|
62
100
|
}
|
|
63
101
|
|
|
64
102
|
function getDefaultConfig() {
|
|
65
|
-
const cfg =
|
|
103
|
+
const cfg = readMurmConfig();
|
|
66
104
|
return { maxConcurrency: cfg.maxConcurrency };
|
|
67
105
|
}
|
|
68
106
|
|
|
@@ -71,7 +109,7 @@ function getQueuePath(worktreePath) {
|
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
function shouldCleanupWorktree(state) {
|
|
74
|
-
return state.status === '
|
|
112
|
+
return state.status === 'murm_complete' || state.status === 'aborted';
|
|
75
113
|
}
|
|
76
114
|
|
|
77
115
|
function validatePreFlight({ isGitRepo, isDirty, gitVersion }) {
|
|
@@ -131,7 +169,7 @@ function promoteFromQueue(state) {
|
|
|
131
169
|
}
|
|
132
170
|
|
|
133
171
|
function buildPipelineCommand(slug, worktreePath, config = null) {
|
|
134
|
-
const cfg = config ||
|
|
172
|
+
const cfg = config || readMurmConfig();
|
|
135
173
|
const flags = cfg.skillFlags ? ` ${cfg.skillFlags}` : '';
|
|
136
174
|
return `${cfg.cli} --cwd ${worktreePath} ${cfg.skill} "${slug}"${flags}`;
|
|
137
175
|
}
|
|
@@ -161,12 +199,12 @@ function orderByCompletion(features) {
|
|
|
161
199
|
}
|
|
162
200
|
|
|
163
201
|
const VALID_TRANSITIONS = {
|
|
164
|
-
|
|
165
|
-
worktree_created: ['
|
|
166
|
-
|
|
167
|
-
merge_pending: ['
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
murm_queued: ['worktree_created', 'aborted'],
|
|
203
|
+
worktree_created: ['murm_running', 'murm_failed', 'aborted'],
|
|
204
|
+
murm_running: ['merge_pending', 'murm_failed', 'aborted'],
|
|
205
|
+
merge_pending: ['murm_complete', 'merge_conflict', 'aborted'],
|
|
206
|
+
murm_failed: [],
|
|
207
|
+
murm_complete: [],
|
|
170
208
|
merge_conflict: [],
|
|
171
209
|
aborted: []
|
|
172
210
|
};
|
|
@@ -183,23 +221,23 @@ function formatStatus(states) {
|
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
function formatFeatureStatus(state) {
|
|
186
|
-
const statusDisplay = state.status.replace('
|
|
224
|
+
const statusDisplay = state.status.replace('murm_', '');
|
|
187
225
|
const stage = state.stage ? ` (${state.stage})` : '';
|
|
188
226
|
return `${state.slug}: ${statusDisplay}${stage}`;
|
|
189
227
|
}
|
|
190
228
|
|
|
191
229
|
function summarizeFinal(results) {
|
|
192
230
|
return {
|
|
193
|
-
completed: results.filter(r => r.status === '
|
|
194
|
-
failed: results.filter(r => r.status === '
|
|
231
|
+
completed: results.filter(r => r.status === 'murm_complete').length,
|
|
232
|
+
failed: results.filter(r => r.status === 'murm_failed').length,
|
|
195
233
|
conflicts: results.filter(r => r.status === 'merge_conflict').length
|
|
196
234
|
};
|
|
197
235
|
}
|
|
198
236
|
|
|
199
237
|
function aggregateResults(results) {
|
|
200
238
|
return {
|
|
201
|
-
completed: results.filter(r => r.status === '
|
|
202
|
-
failed: results.filter(r => r.status === '
|
|
239
|
+
completed: results.filter(r => r.status === 'murm_complete').length,
|
|
240
|
+
failed: results.filter(r => r.status === 'murm_failed').length,
|
|
203
241
|
total: results.length
|
|
204
242
|
};
|
|
205
243
|
}
|
|
@@ -296,12 +334,12 @@ function promptConfirm(message) {
|
|
|
296
334
|
}
|
|
297
335
|
|
|
298
336
|
function buildConfirmMessage(slugs, config) {
|
|
299
|
-
const
|
|
337
|
+
const murmCfg = readMurmConfig();
|
|
300
338
|
const { active, queued } = splitByLimit(slugs, config.maxConcurrency);
|
|
301
339
|
|
|
302
340
|
let msg = '\nThis will:\n';
|
|
303
|
-
msg += ` • Create ${slugs.length} git worktree(s) in ${
|
|
304
|
-
msg += ` • Start ${active.length}
|
|
341
|
+
msg += ` • Create ${slugs.length} git worktree(s) in ${murmCfg.worktreeDir}/\n`;
|
|
342
|
+
msg += ` • Start ${active.length} murmuration pipeline(s) (max concurrent: ${config.maxConcurrency})\n`;
|
|
305
343
|
if (queued.length > 0) {
|
|
306
344
|
msg += ` • Queue ${queued.length} additional feature(s)\n`;
|
|
307
345
|
}
|
|
@@ -313,6 +351,7 @@ function buildConfirmMessage(slugs, config) {
|
|
|
313
351
|
// --- Lock File ---
|
|
314
352
|
|
|
315
353
|
function acquireLock(slugs) {
|
|
354
|
+
ensureMigrated();
|
|
316
355
|
if (fs.existsSync(LOCK_FILE)) {
|
|
317
356
|
const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8'));
|
|
318
357
|
|
|
@@ -357,6 +396,7 @@ function releaseLock() {
|
|
|
357
396
|
}
|
|
358
397
|
|
|
359
398
|
function getLockInfo() {
|
|
399
|
+
ensureMigrated();
|
|
360
400
|
if (!fs.existsSync(LOCK_FILE)) {
|
|
361
401
|
return null;
|
|
362
402
|
}
|
|
@@ -370,7 +410,7 @@ function getLockInfo() {
|
|
|
370
410
|
// --- Feature Limit ---
|
|
371
411
|
|
|
372
412
|
function validateFeatureLimit(slugs) {
|
|
373
|
-
const config =
|
|
413
|
+
const config = readMurmConfig();
|
|
374
414
|
if (slugs.length > config.maxFeatures) {
|
|
375
415
|
return {
|
|
376
416
|
valid: false,
|
|
@@ -385,7 +425,7 @@ function validateFeatureLimit(slugs) {
|
|
|
385
425
|
// --- Disk Space Check ---
|
|
386
426
|
|
|
387
427
|
function checkDiskSpace() {
|
|
388
|
-
const config =
|
|
428
|
+
const config = readMurmConfig();
|
|
389
429
|
try {
|
|
390
430
|
// Get available space on current filesystem
|
|
391
431
|
const output = execSync('df -m . | tail -1', { encoding: 'utf8' });
|
|
@@ -419,8 +459,8 @@ function validateDiskSpace() {
|
|
|
419
459
|
// --- Logging ---
|
|
420
460
|
|
|
421
461
|
function createLogStream(slug, config) {
|
|
422
|
-
const
|
|
423
|
-
const worktreePath = buildWorktreePath(slug,
|
|
462
|
+
const murmCfg = config || readMurmConfig();
|
|
463
|
+
const worktreePath = buildWorktreePath(slug, murmCfg);
|
|
424
464
|
const logPath = path.join(worktreePath, 'pipeline.log');
|
|
425
465
|
|
|
426
466
|
// Ensure directory exists
|
|
@@ -610,7 +650,7 @@ function estimateScope(featureValidations) {
|
|
|
610
650
|
});
|
|
611
651
|
}
|
|
612
652
|
|
|
613
|
-
function
|
|
653
|
+
function validateMurmBatch(slugs) {
|
|
614
654
|
const featureValidations = slugs.map(validateFeatureSpec);
|
|
615
655
|
const fileOverlaps = detectFileOverlap(featureValidations);
|
|
616
656
|
const dependencies = detectDependencies(featureValidations);
|
|
@@ -744,7 +784,7 @@ function withTimeout(promise, timeoutMs, slug) {
|
|
|
744
784
|
}
|
|
745
785
|
|
|
746
786
|
function getTimeoutMs() {
|
|
747
|
-
const config =
|
|
787
|
+
const config = readMurmConfig();
|
|
748
788
|
return config.timeout * 60 * 1000; // Convert minutes to ms
|
|
749
789
|
}
|
|
750
790
|
|
|
@@ -807,29 +847,20 @@ function getDetailedStatus() {
|
|
|
807
847
|
});
|
|
808
848
|
|
|
809
849
|
return {
|
|
810
|
-
active: features.some(f => f.status === '
|
|
850
|
+
active: features.some(f => f.status === 'murm_running'),
|
|
811
851
|
features
|
|
812
852
|
};
|
|
813
853
|
}
|
|
814
854
|
|
|
815
855
|
function formatDetailedStatus(details) {
|
|
816
856
|
if (!details.active && details.features.length === 0) {
|
|
817
|
-
return 'No
|
|
857
|
+
return 'No murmuration pipelines active.';
|
|
818
858
|
}
|
|
819
859
|
|
|
820
|
-
let output = '
|
|
860
|
+
let output = 'Murmuration Status\n\n';
|
|
821
861
|
|
|
822
862
|
for (const f of details.features) {
|
|
823
|
-
const statusIcon =
|
|
824
|
-
'parallel_queued': '⏳',
|
|
825
|
-
'worktree_created': '📁',
|
|
826
|
-
'parallel_running': '🔄',
|
|
827
|
-
'merge_pending': '🔀',
|
|
828
|
-
'parallel_complete': '✅',
|
|
829
|
-
'parallel_failed': '❌',
|
|
830
|
-
'merge_conflict': '⚠️',
|
|
831
|
-
'aborted': '🛑'
|
|
832
|
-
}[f.status] || '❓';
|
|
863
|
+
const statusIcon = theme.STATUS_ICONS[f.status] || '?';
|
|
833
864
|
|
|
834
865
|
const elapsed = f.elapsedSeconds > 0
|
|
835
866
|
? ` (${Math.floor(f.elapsedSeconds / 60)}m ${f.elapsedSeconds % 60}s)`
|
|
@@ -837,8 +868,8 @@ function formatDetailedStatus(details) {
|
|
|
837
868
|
|
|
838
869
|
output += `${statusIcon} ${f.slug}${elapsed}\n`;
|
|
839
870
|
|
|
840
|
-
if (f.status === '
|
|
841
|
-
const bar = progressBar(f.percent);
|
|
871
|
+
if (f.status === 'murm_running') {
|
|
872
|
+
const bar = theme.progressBar(f.percent);
|
|
842
873
|
output += ` ${bar} ${f.percent}% - ${f.stage}\n`;
|
|
843
874
|
}
|
|
844
875
|
}
|
|
@@ -847,9 +878,7 @@ function formatDetailedStatus(details) {
|
|
|
847
878
|
}
|
|
848
879
|
|
|
849
880
|
function progressBar(percent, width = 20) {
|
|
850
|
-
|
|
851
|
-
const empty = width - filled;
|
|
852
|
-
return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']';
|
|
881
|
+
return theme.progressBar(percent, width);
|
|
853
882
|
}
|
|
854
883
|
|
|
855
884
|
// --- Abort Handling ---
|
|
@@ -859,7 +888,7 @@ function setupAbortHandler(queue) {
|
|
|
859
888
|
if (isAborting) return;
|
|
860
889
|
isAborting = true;
|
|
861
890
|
|
|
862
|
-
console.log(
|
|
891
|
+
console.log(`\n\n${theme.MESSAGES.flockScattering}\n`);
|
|
863
892
|
|
|
864
893
|
// Kill all running processes
|
|
865
894
|
for (const [slug, procInfo] of runningProcesses) {
|
|
@@ -874,7 +903,7 @@ function setupAbortHandler(queue) {
|
|
|
874
903
|
// Update queue state
|
|
875
904
|
if (queue && queue.features) {
|
|
876
905
|
queue.features.forEach(f => {
|
|
877
|
-
if (f.status === '
|
|
906
|
+
if (f.status === 'murm_running' || f.status === 'worktree_created') {
|
|
878
907
|
f.status = 'aborted';
|
|
879
908
|
}
|
|
880
909
|
});
|
|
@@ -884,7 +913,7 @@ function setupAbortHandler(queue) {
|
|
|
884
913
|
releaseLock();
|
|
885
914
|
|
|
886
915
|
console.log('\nAborted. Worktrees preserved for debugging.');
|
|
887
|
-
console.log("Run 'murmur8
|
|
916
|
+
console.log("Run 'murmur8 murm cleanup' to remove.\n");
|
|
888
917
|
|
|
889
918
|
process.exit(130); // Standard exit code for Ctrl+C
|
|
890
919
|
};
|
|
@@ -895,16 +924,16 @@ function setupAbortHandler(queue) {
|
|
|
895
924
|
return handler;
|
|
896
925
|
}
|
|
897
926
|
|
|
898
|
-
async function
|
|
927
|
+
async function abortMurm(options = {}) {
|
|
899
928
|
const lock = getLockInfo();
|
|
900
929
|
const queue = loadQueue();
|
|
901
930
|
|
|
902
931
|
if (!lock && (!queue.features || queue.features.length === 0)) {
|
|
903
|
-
console.log('No
|
|
932
|
+
console.log('No murmuration pipelines are currently running.');
|
|
904
933
|
return { success: true };
|
|
905
934
|
}
|
|
906
935
|
|
|
907
|
-
console.log('Stopping
|
|
936
|
+
console.log('Stopping murmuration pipelines...\n');
|
|
908
937
|
|
|
909
938
|
// Try to kill the main process if we have lock info
|
|
910
939
|
if (lock && lock.pid !== process.pid) {
|
|
@@ -920,7 +949,7 @@ async function abortParallel(options = {}) {
|
|
|
920
949
|
let abortedCount = 0;
|
|
921
950
|
if (queue.features) {
|
|
922
951
|
queue.features.forEach(f => {
|
|
923
|
-
if (f.status === '
|
|
952
|
+
if (f.status === 'murm_running' || f.status === 'worktree_created' || f.status === 'murm_queued') {
|
|
924
953
|
f.status = 'aborted';
|
|
925
954
|
abortedCount++;
|
|
926
955
|
console.log(`${f.slug}: Marked as aborted`);
|
|
@@ -945,7 +974,7 @@ async function abortParallel(options = {}) {
|
|
|
945
974
|
worktrees.forEach(w => console.log(` • ${w}`));
|
|
946
975
|
}
|
|
947
976
|
}
|
|
948
|
-
console.log("\nTo clean up: murmur8
|
|
977
|
+
console.log("\nTo clean up: murmur8 murm cleanup");
|
|
949
978
|
}
|
|
950
979
|
|
|
951
980
|
return { success: true, abortedCount };
|
|
@@ -970,7 +999,7 @@ function saveQueue(queue) {
|
|
|
970
999
|
// --- Pipeline Execution ---
|
|
971
1000
|
|
|
972
1001
|
function runPipelineInWorktree(slug, worktreePath, config = null, options = {}) {
|
|
973
|
-
const cfg = config ||
|
|
1002
|
+
const cfg = config || readMurmConfig();
|
|
974
1003
|
const cliParts = cfg.cli.split(' ');
|
|
975
1004
|
const skillParts = cfg.skill.split(' ');
|
|
976
1005
|
const flagParts = cfg.skillFlags ? cfg.skillFlags.split(' ') : [];
|
|
@@ -1027,7 +1056,7 @@ function runPipelineInWorktree(slug, worktreePath, config = null, options = {})
|
|
|
1027
1056
|
// --- Main Orchestration ---
|
|
1028
1057
|
|
|
1029
1058
|
function dryRun(slugs, config, baseBranch, gitStatus, validation, batchValidation = null) {
|
|
1030
|
-
const
|
|
1059
|
+
const murmCfg = readMurmConfig();
|
|
1031
1060
|
const { active, queued } = splitByLimit(slugs, config.maxConcurrency);
|
|
1032
1061
|
|
|
1033
1062
|
console.log('\n=== DRY RUN MODE ===\n');
|
|
@@ -1042,28 +1071,28 @@ function dryRun(slugs, config, baseBranch, gitStatus, validation, batchValidatio
|
|
|
1042
1071
|
validation.errors.forEach(e => console.log(` - ${e}`));
|
|
1043
1072
|
}
|
|
1044
1073
|
|
|
1045
|
-
// Show batch validation results (already printed in
|
|
1074
|
+
// Show batch validation results (already printed in runMurm if issues found)
|
|
1046
1075
|
if (batchValidation && !batchValidation.valid) {
|
|
1047
1076
|
console.log(`\n⚠️ WARNING: Feature validation failed. Real execution would abort.`);
|
|
1048
1077
|
}
|
|
1049
1078
|
|
|
1050
1079
|
console.log(`\nConfiguration:`);
|
|
1051
1080
|
console.log(` Max concurrency: ${config.maxConcurrency}`);
|
|
1052
|
-
console.log(` Max features: ${
|
|
1053
|
-
console.log(` Timeout: ${
|
|
1054
|
-
console.log(` Min disk space: ${
|
|
1055
|
-
console.log(` CLI: ${
|
|
1056
|
-
console.log(` Skill: ${
|
|
1057
|
-
console.log(` Flags: ${
|
|
1058
|
-
console.log(` Worktree dir: ${
|
|
1081
|
+
console.log(` Max features: ${murmCfg.maxFeatures}`);
|
|
1082
|
+
console.log(` Timeout: ${murmCfg.timeout} min per pipeline`);
|
|
1083
|
+
console.log(` Min disk space: ${murmCfg.minDiskSpaceMB} MB`);
|
|
1084
|
+
console.log(` CLI: ${murmCfg.cli}`);
|
|
1085
|
+
console.log(` Skill: ${murmCfg.skill}`);
|
|
1086
|
+
console.log(` Flags: ${murmCfg.skillFlags || '(none)'}`);
|
|
1087
|
+
console.log(` Worktree dir: ${murmCfg.worktreeDir}`);
|
|
1059
1088
|
console.log(` Total features: ${slugs.length}`);
|
|
1060
1089
|
|
|
1061
1090
|
console.log(`\nInitial batch (${active.length} features):`);
|
|
1062
1091
|
active.forEach(slug => {
|
|
1063
1092
|
console.log(` → ${slug}`);
|
|
1064
|
-
console.log(` Worktree: ${buildWorktreePath(slug,
|
|
1093
|
+
console.log(` Worktree: ${buildWorktreePath(slug, murmCfg)}`);
|
|
1065
1094
|
console.log(` Branch: ${buildBranchName(slug)}`);
|
|
1066
|
-
console.log(` Command: ${buildPipelineCommand(slug, buildWorktreePath(slug,
|
|
1095
|
+
console.log(` Command: ${buildPipelineCommand(slug, buildWorktreePath(slug, murmCfg), murmCfg)}`);
|
|
1067
1096
|
});
|
|
1068
1097
|
|
|
1069
1098
|
if (queued.length > 0) {
|
|
@@ -1075,7 +1104,7 @@ function dryRun(slugs, config, baseBranch, gitStatus, validation, batchValidatio
|
|
|
1075
1104
|
|
|
1076
1105
|
console.log(`\nExecution plan:`);
|
|
1077
1106
|
console.log(` 1. Create ${active.length} git worktrees`);
|
|
1078
|
-
console.log(` 2. Spawn ${active.length}
|
|
1107
|
+
console.log(` 2. Spawn ${active.length} murmuration pipeline processes`);
|
|
1079
1108
|
console.log(` 3. As each completes: merge to ${baseBranch}, cleanup worktree`);
|
|
1080
1109
|
if (queued.length > 0) {
|
|
1081
1110
|
console.log(` 4. Promote queued features as slots free`);
|
|
@@ -1088,7 +1117,7 @@ function dryRun(slugs, config, baseBranch, gitStatus, validation, batchValidatio
|
|
|
1088
1117
|
return { success: true, dryRun: true };
|
|
1089
1118
|
}
|
|
1090
1119
|
|
|
1091
|
-
async function
|
|
1120
|
+
async function runMurm(slugs, options = {}) {
|
|
1092
1121
|
const config = { ...getDefaultConfig(), ...options };
|
|
1093
1122
|
const baseBranch = getCurrentBranch();
|
|
1094
1123
|
|
|
@@ -1099,7 +1128,7 @@ async function runParallel(slugs, options = {}) {
|
|
|
1099
1128
|
// Batch validation (unless skipped)
|
|
1100
1129
|
let batchValidation = null;
|
|
1101
1130
|
if (!options.skipPreflight) {
|
|
1102
|
-
batchValidation =
|
|
1131
|
+
batchValidation = validateMurmBatch(slugs);
|
|
1103
1132
|
|
|
1104
1133
|
// Show pre-flight results in dry-run or if there are issues
|
|
1105
1134
|
if (options.dryRun || !batchValidation.valid || batchValidation.fileOverlaps.length > 0 || batchValidation.dependencies.length > 0) {
|
|
@@ -1141,7 +1170,7 @@ async function runParallel(slugs, options = {}) {
|
|
|
1141
1170
|
const limitCheck = validateFeatureLimit(slugs);
|
|
1142
1171
|
if (!limitCheck.valid) {
|
|
1143
1172
|
console.error(`\nError: ${limitCheck.error}`);
|
|
1144
|
-
console.error(`\nTo increase limit: murmur8
|
|
1173
|
+
console.error(`\nTo increase limit: murmur8 murm-config set maxFeatures <N>\n`);
|
|
1145
1174
|
return { success: false, error: 'feature-limit-exceeded' };
|
|
1146
1175
|
}
|
|
1147
1176
|
|
|
@@ -1161,14 +1190,14 @@ async function runParallel(slugs, options = {}) {
|
|
|
1161
1190
|
const lockResult = acquireLock(slugs);
|
|
1162
1191
|
if (!lockResult.acquired) {
|
|
1163
1192
|
const lock = lockResult.existingLock;
|
|
1164
|
-
console.error('\nError: Another
|
|
1193
|
+
console.error('\nError: Another murmuration execution is in progress');
|
|
1165
1194
|
console.error(` PID: ${lock.pid}`);
|
|
1166
1195
|
console.error(` Started: ${lock.startedAt}`);
|
|
1167
1196
|
console.error(` Features: ${lock.features.join(', ')}`);
|
|
1168
1197
|
console.error('\nOptions:');
|
|
1169
1198
|
console.error(' • Wait for it to complete');
|
|
1170
|
-
console.error(' • Run: murmur8
|
|
1171
|
-
console.error(' • Force override: murmur8
|
|
1199
|
+
console.error(' • Run: murmur8 murm status');
|
|
1200
|
+
console.error(' • Force override: murmur8 murm ... --force\n');
|
|
1172
1201
|
return { success: false, error: 'locked' };
|
|
1173
1202
|
}
|
|
1174
1203
|
} else {
|
|
@@ -1191,7 +1220,9 @@ async function runParallel(slugs, options = {}) {
|
|
|
1191
1220
|
}
|
|
1192
1221
|
}
|
|
1193
1222
|
|
|
1194
|
-
|
|
1223
|
+
const useColor = process.stdout.isTTY || false;
|
|
1224
|
+
console.log(theme.banner(useColor));
|
|
1225
|
+
console.log(theme.MESSAGES.startingFlock(slugs.length));
|
|
1195
1226
|
console.log(`Base branch: ${baseBranch}`);
|
|
1196
1227
|
console.log(`Max concurrency: ${config.maxConcurrency}\n`);
|
|
1197
1228
|
|
|
@@ -1199,7 +1230,7 @@ async function runParallel(slugs, options = {}) {
|
|
|
1199
1230
|
const queue = {
|
|
1200
1231
|
features: slugs.map(slug => ({
|
|
1201
1232
|
slug,
|
|
1202
|
-
status: '
|
|
1233
|
+
status: 'murm_queued',
|
|
1203
1234
|
worktreePath: null,
|
|
1204
1235
|
branchName: null,
|
|
1205
1236
|
startedAt: null,
|
|
@@ -1246,30 +1277,30 @@ async function runParallel(slugs, options = {}) {
|
|
|
1246
1277
|
|
|
1247
1278
|
if (result.success) {
|
|
1248
1279
|
feature.status = 'merge_pending';
|
|
1249
|
-
console.log(`[${timestamp}] ${result.slug}:
|
|
1280
|
+
console.log(`[${timestamp}] ${result.slug}: ${theme.MESSAGES.landed} \u2713`);
|
|
1250
1281
|
|
|
1251
1282
|
// Attempt merge
|
|
1252
1283
|
const mergeResult = mergeBranch(result.slug);
|
|
1253
1284
|
if (mergeResult.success) {
|
|
1254
|
-
feature.status = '
|
|
1255
|
-
console.log(`[${timestamp}] ${result.slug}:
|
|
1285
|
+
feature.status = 'murm_complete';
|
|
1286
|
+
console.log(`[${timestamp}] ${result.slug}: ${theme.MESSAGES.mergedAndLanded} \u2713`);
|
|
1256
1287
|
removeWorktree(result.slug);
|
|
1257
1288
|
} else if (mergeResult.conflict) {
|
|
1258
1289
|
feature.status = 'merge_conflict';
|
|
1259
1290
|
feature.conflictDetails = mergeResult.output;
|
|
1260
|
-
console.log(`[${timestamp}] ${result.slug}:
|
|
1291
|
+
console.log(`[${timestamp}] ${result.slug}: ${theme.MESSAGES.turbulence} \u26a0 (branch preserved)`);
|
|
1261
1292
|
execSync('git merge --abort', { stdio: 'pipe' });
|
|
1262
1293
|
} else {
|
|
1263
|
-
feature.status = '
|
|
1264
|
-
console.log(`[${timestamp}] ${result.slug}:
|
|
1294
|
+
feature.status = 'murm_failed';
|
|
1295
|
+
console.log(`[${timestamp}] ${result.slug}: ${theme.MESSAGES.lostFormation} \u2717`);
|
|
1265
1296
|
}
|
|
1266
1297
|
} else {
|
|
1267
|
-
feature.status = '
|
|
1298
|
+
feature.status = 'murm_failed';
|
|
1268
1299
|
if (result.timedOut) {
|
|
1269
|
-
console.log(`[${timestamp}] ${result.slug}:
|
|
1300
|
+
console.log(`[${timestamp}] ${result.slug}: ${theme.MESSAGES.timedOut} \u23f1 (see log: ${feature.logPath})`);
|
|
1270
1301
|
feature.timedOut = true;
|
|
1271
1302
|
} else {
|
|
1272
|
-
console.log(`[${timestamp}] ${result.slug}:
|
|
1303
|
+
console.log(`[${timestamp}] ${result.slug}: ${theme.MESSAGES.lostFormation} \u2717 (see log: ${feature.logPath})`);
|
|
1273
1304
|
}
|
|
1274
1305
|
// Preserve worktree for debugging
|
|
1275
1306
|
}
|
|
@@ -1287,22 +1318,22 @@ async function runParallel(slugs, options = {}) {
|
|
|
1287
1318
|
|
|
1288
1319
|
// Final summary
|
|
1289
1320
|
const summary = summarizeFinal(queue.features);
|
|
1290
|
-
console.log(
|
|
1321
|
+
console.log(`\n${theme.MESSAGES.murmurationComplete}`);
|
|
1291
1322
|
console.log(`Completed: ${summary.completed}`);
|
|
1292
1323
|
console.log(`Failed: ${summary.failed}`);
|
|
1293
1324
|
console.log(`Conflicts: ${summary.conflicts}`);
|
|
1294
1325
|
|
|
1295
1326
|
if (summary.conflicts > 0) {
|
|
1296
|
-
console.log(
|
|
1327
|
+
console.log(`\n${theme.MESSAGES.conflictsHeader}`);
|
|
1297
1328
|
queue.features
|
|
1298
1329
|
.filter(f => f.status === 'merge_conflict')
|
|
1299
1330
|
.forEach(f => console.log(` - ${f.branchName}`));
|
|
1300
1331
|
}
|
|
1301
1332
|
|
|
1302
1333
|
if (summary.failed > 0) {
|
|
1303
|
-
console.log(
|
|
1334
|
+
console.log(`\n${theme.MESSAGES.failuresHeader}`);
|
|
1304
1335
|
queue.features
|
|
1305
|
-
.filter(f => f.status === '
|
|
1336
|
+
.filter(f => f.status === 'murm_failed')
|
|
1306
1337
|
.forEach(f => {
|
|
1307
1338
|
console.log(` - ${f.worktreePath}`);
|
|
1308
1339
|
if (f.logPath) {
|
|
@@ -1320,9 +1351,9 @@ async function runParallel(slugs, options = {}) {
|
|
|
1320
1351
|
|
|
1321
1352
|
async function startFeature(slug, queue, running, options = {}) {
|
|
1322
1353
|
const feature = queue.features.find(f => f.slug === slug);
|
|
1323
|
-
const
|
|
1354
|
+
const murmCfg = readMurmConfig();
|
|
1324
1355
|
|
|
1325
|
-
console.log(`[${new Date().toISOString().slice(11, 19)}] ${slug}:
|
|
1356
|
+
console.log(`[${new Date().toISOString().slice(11, 19)}] ${slug}: ${theme.MESSAGES.takingFlight}`);
|
|
1326
1357
|
const { worktreePath, branchName } = createWorktree(slug);
|
|
1327
1358
|
|
|
1328
1359
|
feature.worktreePath = worktreePath;
|
|
@@ -1338,27 +1369,27 @@ async function startFeature(slug, queue, running, options = {}) {
|
|
|
1338
1369
|
const timeoutMs = getTimeoutMs();
|
|
1339
1370
|
const timeoutMin = timeoutMs / 60000;
|
|
1340
1371
|
console.log(`[${new Date().toISOString().slice(11, 19)}] ${slug}: Started (log: ${feature.logPath}, timeout: ${timeoutMin}min)`);
|
|
1341
|
-
feature.status = '
|
|
1372
|
+
feature.status = 'murm_running';
|
|
1342
1373
|
saveQueue(queue);
|
|
1343
1374
|
|
|
1344
|
-
const pipelinePromise = runPipelineInWorktree(slug, worktreePath,
|
|
1375
|
+
const pipelinePromise = runPipelineInWorktree(slug, worktreePath, murmCfg, options);
|
|
1345
1376
|
const promise = withTimeout(pipelinePromise, timeoutMs, slug);
|
|
1346
1377
|
running.set(slug, promise);
|
|
1347
1378
|
}
|
|
1348
1379
|
|
|
1349
1380
|
// --- Rollback ---
|
|
1350
1381
|
|
|
1351
|
-
async function
|
|
1382
|
+
async function rollbackMurm(options = {}) {
|
|
1352
1383
|
const queue = loadQueue();
|
|
1353
1384
|
|
|
1354
1385
|
if (!queue.features || queue.features.length === 0) {
|
|
1355
|
-
console.log('No
|
|
1386
|
+
console.log('No murmuration run to rollback.');
|
|
1356
1387
|
return { success: true, rolledBack: 0 };
|
|
1357
1388
|
}
|
|
1358
1389
|
|
|
1359
|
-
const completedFeatures = queue.features.filter(f => f.status === '
|
|
1390
|
+
const completedFeatures = queue.features.filter(f => f.status === 'murm_complete');
|
|
1360
1391
|
const failedFeatures = queue.features.filter(f =>
|
|
1361
|
-
f.status === '
|
|
1392
|
+
f.status === 'murm_failed' || f.status === 'merge_conflict'
|
|
1362
1393
|
);
|
|
1363
1394
|
|
|
1364
1395
|
if (completedFeatures.length === 0 && failedFeatures.length === 0) {
|
|
@@ -1366,7 +1397,7 @@ async function rollbackParallel(options = {}) {
|
|
|
1366
1397
|
return { success: true, rolledBack: 0 };
|
|
1367
1398
|
}
|
|
1368
1399
|
|
|
1369
|
-
console.log('\
|
|
1400
|
+
console.log('\nMurmuration Rollback\n');
|
|
1370
1401
|
|
|
1371
1402
|
if (options.dryRun) {
|
|
1372
1403
|
console.log('DRY RUN - No changes will be made\n');
|
|
@@ -1389,7 +1420,7 @@ async function rollbackParallel(options = {}) {
|
|
|
1389
1420
|
if (logOutput) {
|
|
1390
1421
|
const commitHash = logOutput.split(' ')[0];
|
|
1391
1422
|
execSync(`git revert --no-commit ${commitHash}`, { stdio: 'pipe' });
|
|
1392
|
-
execSync(`git commit -m "Revert: ${f.slug} (
|
|
1423
|
+
execSync(`git commit -m "Revert: ${f.slug} (murmuration rollback)"`, { stdio: 'pipe' });
|
|
1393
1424
|
console.log(` ✓ Reverted commit ${commitHash}`);
|
|
1394
1425
|
rolledBack++;
|
|
1395
1426
|
} else {
|
|
@@ -1464,9 +1495,15 @@ module.exports = {
|
|
|
1464
1495
|
// Configuration
|
|
1465
1496
|
CONFIG_FILE,
|
|
1466
1497
|
LOCK_FILE,
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1498
|
+
QUEUE_FILE,
|
|
1499
|
+
LEGACY_CONFIG_FILE,
|
|
1500
|
+
LEGACY_LOCK_FILE,
|
|
1501
|
+
LEGACY_QUEUE_FILE,
|
|
1502
|
+
migrateFile,
|
|
1503
|
+
ensureMigrated,
|
|
1504
|
+
getDefaultMurmConfig,
|
|
1505
|
+
readMurmConfig,
|
|
1506
|
+
writeMurmConfig,
|
|
1470
1507
|
getQueueFile,
|
|
1471
1508
|
// Utility functions
|
|
1472
1509
|
buildWorktreePath,
|
|
@@ -1510,7 +1547,7 @@ module.exports = {
|
|
|
1510
1547
|
detectFileOverlap,
|
|
1511
1548
|
detectDependencies,
|
|
1512
1549
|
estimateScope,
|
|
1513
|
-
|
|
1550
|
+
validateMurmBatch,
|
|
1514
1551
|
formatPreflightResults,
|
|
1515
1552
|
// Timeout
|
|
1516
1553
|
withTimeout,
|
|
@@ -1521,10 +1558,10 @@ module.exports = {
|
|
|
1521
1558
|
formatDetailedStatus,
|
|
1522
1559
|
progressBar,
|
|
1523
1560
|
// Abort handling
|
|
1524
|
-
|
|
1561
|
+
abortMurm,
|
|
1525
1562
|
setupAbortHandler,
|
|
1526
1563
|
// Rollback
|
|
1527
|
-
|
|
1564
|
+
rollbackMurm,
|
|
1528
1565
|
// Git operations
|
|
1529
1566
|
checkGitStatus,
|
|
1530
1567
|
createWorktree,
|
|
@@ -1534,11 +1571,10 @@ module.exports = {
|
|
|
1534
1571
|
// Queue management
|
|
1535
1572
|
loadQueue,
|
|
1536
1573
|
saveQueue,
|
|
1537
|
-
QUEUE_FILE,
|
|
1538
1574
|
// Execution
|
|
1539
1575
|
dryRun,
|
|
1540
1576
|
runPipelineInWorktree,
|
|
1541
|
-
|
|
1577
|
+
runMurm,
|
|
1542
1578
|
startFeature,
|
|
1543
1579
|
cleanupWorktrees
|
|
1544
1580
|
};
|