@paths.design/caws-cli 8.2.3 → 9.0.0

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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @fileoverview CAWS Parallel CLI Command
3
+ * Orchestrates parallel multi-agent workspaces
4
+ * @author @darianrosebrook
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const path = require('path');
9
+ const {
10
+ loadPlan,
11
+ setupParallel,
12
+ getParallelStatus,
13
+ mergeParallel,
14
+ teardownParallel,
15
+ } = require('../parallel/parallel-manager');
16
+
17
+ /**
18
+ * Handle parallel subcommands
19
+ * @param {string} subcommand - Subcommand name
20
+ * @param {Object} options - Command options
21
+ */
22
+ async function parallelCommand(subcommand, options = {}) {
23
+ try {
24
+ switch (subcommand) {
25
+ case 'setup':
26
+ return handleSetup(options);
27
+ case 'status':
28
+ return handleStatus();
29
+ case 'merge':
30
+ return handleMerge(options);
31
+ case 'teardown':
32
+ return handleTeardown(options);
33
+ default:
34
+ console.error(chalk.red(`Unknown parallel subcommand: ${subcommand}`));
35
+ console.log(chalk.blue('Available: setup, status, merge, teardown'));
36
+ process.exit(1);
37
+ }
38
+ } catch (error) {
39
+ console.error(chalk.red(`${error.message}`));
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ function handleSetup(options) {
45
+ const { planFile, baseBranch } = options;
46
+
47
+ if (!planFile) {
48
+ console.error(chalk.red('Plan file is required'));
49
+ console.log(chalk.blue('Usage: caws parallel setup <plan-file> [--base-branch <branch>]'));
50
+ process.exit(1);
51
+ }
52
+
53
+ const planPath = path.resolve(planFile);
54
+ console.log(chalk.cyan(`Loading plan: ${planFile}`));
55
+
56
+ const plan = loadPlan(planPath);
57
+
58
+ // Allow CLI --base-branch to override plan file
59
+ if (baseBranch) {
60
+ plan.baseBranch = baseBranch;
61
+ }
62
+
63
+ console.log(chalk.cyan(`Setting up ${plan.agents.length} parallel worktree(s)...`));
64
+ const results = setupParallel(plan);
65
+
66
+ console.log(chalk.green(`\nParallel workspace created`));
67
+ console.log(chalk.gray(` Base branch: ${results[0] ? results[0].baseBranch : plan.baseBranch || 'main'}`));
68
+ console.log(chalk.gray(` Strategy: ${plan.mergeStrategy}`));
69
+ console.log('');
70
+
71
+ // Print table
72
+ console.log(
73
+ chalk.bold(
74
+ 'Agent'.padEnd(20) +
75
+ 'Branch'.padEnd(25) +
76
+ 'Scope'
77
+ )
78
+ );
79
+ console.log(chalk.gray('-'.repeat(70)));
80
+
81
+ for (const entry of results) {
82
+ console.log(
83
+ entry.name.padEnd(20) +
84
+ chalk.cyan(entry.branch.padEnd(25)) +
85
+ chalk.gray(entry.scope || '(all)')
86
+ );
87
+ }
88
+
89
+ console.log('');
90
+ console.log(chalk.blue('Direct each agent to its worktree:'));
91
+ for (const entry of results) {
92
+ console.log(chalk.gray(` ${entry.name}: cd ${entry.path}`));
93
+ }
94
+ console.log('');
95
+ console.log(chalk.blue('Monitor progress: caws parallel status'));
96
+ }
97
+
98
+ function handleStatus() {
99
+ const status = getParallelStatus();
100
+
101
+ if (!status) {
102
+ console.log(chalk.gray('No active parallel run.'));
103
+ console.log(chalk.blue('Start one with: caws parallel setup <plan-file>'));
104
+ return;
105
+ }
106
+
107
+ console.log(chalk.bold.cyan('CAWS Parallel Status'));
108
+ console.log(chalk.cyan('='.repeat(70)));
109
+ console.log(chalk.gray(` Base branch: ${status.baseBranch}`));
110
+ console.log(chalk.gray(` Strategy: ${status.mergeStrategy}`));
111
+ console.log(chalk.gray(` Created: ${status.createdAt}`));
112
+ console.log('');
113
+
114
+ // Agent table
115
+ console.log(
116
+ chalk.bold(
117
+ 'Agent'.padEnd(18) +
118
+ 'Status'.padEnd(10) +
119
+ 'Branch'.padEnd(22) +
120
+ 'Commits'.padEnd(9) +
121
+ 'Dirty'.padEnd(7) +
122
+ 'Scope'
123
+ )
124
+ );
125
+ console.log(chalk.gray('-'.repeat(80)));
126
+
127
+ for (const agent of status.agents) {
128
+ const statusColor =
129
+ agent.status === 'active'
130
+ ? chalk.green
131
+ : agent.status === 'missing'
132
+ ? chalk.red
133
+ : chalk.yellow;
134
+
135
+ console.log(
136
+ agent.name.padEnd(18) +
137
+ statusColor(agent.status.padEnd(10)) +
138
+ agent.branch.padEnd(22) +
139
+ String(agent.commitCount).padEnd(9) +
140
+ (agent.dirty ? chalk.yellow('yes') : chalk.gray('no')).padEnd(7 + 10) + // +10 for chalk color codes
141
+ chalk.gray(agent.scope || '(all)')
142
+ );
143
+ }
144
+
145
+ // Show conflicts
146
+ if (status.conflicts.length > 0) {
147
+ console.log('');
148
+ console.log(chalk.yellow(`WARNING: ${status.conflicts.length} file-level conflict(s) detected:`));
149
+ for (const conflict of status.conflicts) {
150
+ console.log(chalk.yellow(` ${conflict.file} -- modified by: ${conflict.agents.join(', ')}`));
151
+ }
152
+ console.log(chalk.blue(' These files were modified by multiple agents and may cause merge conflicts.'));
153
+ }
154
+
155
+ console.log('');
156
+ }
157
+
158
+ function handleMerge(options) {
159
+ const { strategy, dryRun, force } = options;
160
+
161
+ if (dryRun) {
162
+ console.log(chalk.cyan('Dry run: previewing merge...'));
163
+ } else {
164
+ console.log(chalk.cyan('Merging parallel branches back to base...'));
165
+ }
166
+
167
+ const result = mergeParallel({ strategy, dryRun, force });
168
+
169
+ // Show conflicts
170
+ if (result.conflicts.length > 0) {
171
+ console.log(chalk.yellow(`\n${result.conflicts.length} file-level conflict(s) detected:`));
172
+ for (const conflict of result.conflicts) {
173
+ console.log(chalk.yellow(` ${conflict.file} -- ${conflict.agents.join(', ')}`));
174
+ }
175
+
176
+ if (!force && !dryRun) {
177
+ console.log('');
178
+ console.log(chalk.red('Merge aborted due to conflicts.'));
179
+ console.log(chalk.blue(' Review conflicts, then: caws parallel merge --force'));
180
+ return;
181
+ }
182
+ }
183
+
184
+ if (dryRun) {
185
+ console.log(chalk.green(`\nWould merge ${result.merged.length} branch(es):`));
186
+ for (const name of result.merged) {
187
+ console.log(chalk.gray(` - ${name}`));
188
+ }
189
+ return;
190
+ }
191
+
192
+ if (result.merged.length > 0) {
193
+ console.log(chalk.green(`\nMerged ${result.merged.length} branch(es):`));
194
+ for (const name of result.merged) {
195
+ console.log(chalk.gray(` - ${name}`));
196
+ }
197
+ }
198
+
199
+ if (result.failed.length > 0) {
200
+ console.log(chalk.red(`\nFailed to merge ${result.failed.length} branch(es):`));
201
+ for (const fail of result.failed) {
202
+ console.log(chalk.red(` - ${fail.name}: ${fail.error}`));
203
+ }
204
+ }
205
+
206
+ if (result.merged.length > 0 && result.failed.length === 0) {
207
+ console.log('');
208
+ console.log(chalk.blue('Clean up with: caws parallel teardown --delete-branches'));
209
+ }
210
+ }
211
+
212
+ function handleTeardown(options) {
213
+ const { deleteBranches, force } = options;
214
+
215
+ console.log(chalk.cyan('Tearing down parallel worktrees...'));
216
+ const result = teardownParallel({ deleteBranches, force });
217
+
218
+ if (result.destroyed.length > 0) {
219
+ console.log(chalk.green(`Destroyed ${result.destroyed.length} worktree(s):`));
220
+ for (const name of result.destroyed) {
221
+ console.log(chalk.gray(` - ${name}`));
222
+ }
223
+ }
224
+
225
+ if (result.failed.length > 0) {
226
+ console.log(chalk.red(`Failed to destroy ${result.failed.length} worktree(s):`));
227
+ for (const fail of result.failed) {
228
+ console.log(chalk.red(` - ${fail.name}: ${fail.error}`));
229
+ }
230
+ console.log(chalk.blue(' Use --force to override'));
231
+ }
232
+
233
+ if (deleteBranches) {
234
+ console.log(chalk.gray(' Branches also deleted'));
235
+ }
236
+ }
237
+
238
+ module.exports = { parallelCommand };
@@ -363,6 +363,16 @@ async function updateSpec(id, updates = {}) {
363
363
  return false;
364
364
  }
365
365
 
366
+ // Validate status if being updated
367
+ if (updates.status) {
368
+ const { SPEC_STATUSES } = require('../constants/spec-types');
369
+ if (!SPEC_STATUSES[updates.status]) {
370
+ throw new Error(
371
+ `Invalid status '${updates.status}'. Valid values: ${Object.keys(SPEC_STATUSES).join(', ')}`
372
+ );
373
+ }
374
+ }
375
+
366
376
  // Apply updates
367
377
  const updatedSpec = {
368
378
  ...spec,
@@ -536,6 +546,30 @@ async function deleteSpec(id) {
536
546
  return true;
537
547
  }
538
548
 
549
+ /**
550
+ * Close a spec (sets status to 'closed', removing scope enforcement).
551
+ * @param {string} id - Spec identifier
552
+ * @returns {Promise<boolean>} Success status
553
+ */
554
+ async function closeSpec(id) {
555
+ const spec = await loadSpec(id);
556
+ if (!spec) {
557
+ return false;
558
+ }
559
+
560
+ const currentStatus = spec.status || 'draft';
561
+ if (currentStatus === 'closed') {
562
+ console.log(chalk.yellow(`Spec '${id}' is already closed.`));
563
+ return true;
564
+ }
565
+ if (currentStatus === 'archived') {
566
+ console.log(chalk.yellow(`Spec '${id}' is archived and cannot be closed.`));
567
+ return false;
568
+ }
569
+
570
+ return await updateSpec(id, { status: 'closed' });
571
+ }
572
+
539
573
  /**
540
574
  * Display specs in a formatted table
541
575
  * @param {Array} specs - Array of spec objects
@@ -554,7 +588,7 @@ function displaySpecsTable(specs) {
554
588
  console.log(chalk.gray('-'.repeat(80)));
555
589
 
556
590
  // Sort specs by type and status priority
557
- const statusPriority = { active: 0, draft: 1, completed: 2, archived: 3 };
591
+ const statusPriority = { active: 0, draft: 1, completed: 2, closed: 3, archived: 4 };
558
592
  const sortedSpecs = specs.sort((a, b) => {
559
593
  const typeDiff = a.type.localeCompare(b.type);
560
594
  if (typeDiff !== 0) return typeDiff;
@@ -1001,6 +1035,24 @@ async function specsCommand(action, options = {}) {
1001
1035
  });
1002
1036
  }
1003
1037
 
1038
+ case 'close': {
1039
+ if (!options.id) {
1040
+ throw new Error('Spec ID is required. Usage: caws specs close <id>');
1041
+ }
1042
+
1043
+ const closed = await closeSpec(options.id);
1044
+ if (!closed) {
1045
+ throw new Error(`Could not close spec '${options.id}'`);
1046
+ }
1047
+
1048
+ console.log(chalk.green(`Closed spec: ${options.id} -- scope restrictions removed`));
1049
+
1050
+ return outputResult({
1051
+ command: 'specs close',
1052
+ spec: options.id,
1053
+ });
1054
+ }
1055
+
1004
1056
  case 'types': {
1005
1057
  console.log(chalk.bold.cyan('\nAvailable Spec Types'));
1006
1058
  console.log(chalk.cyan('==============================================\n'));
@@ -1019,7 +1071,7 @@ async function specsCommand(action, options = {}) {
1019
1071
 
1020
1072
  default:
1021
1073
  throw new Error(
1022
- `Unknown specs action: ${action}. Use: list, create, show, update, delete, conflicts, migrate, types`
1074
+ `Unknown specs action: ${action}. Use: list, create, show, update, delete, close, conflicts, migrate, types`
1023
1075
  );
1024
1076
  }
1025
1077
  },
@@ -1037,6 +1089,7 @@ module.exports = {
1037
1089
  loadSpec,
1038
1090
  updateSpec,
1039
1091
  deleteSpec,
1092
+ closeSpec,
1040
1093
  displaySpecsTable,
1041
1094
  displaySpecDetails,
1042
1095
  askConflictResolution,
@@ -510,9 +510,10 @@ async function displayVisualStatus(data, currentMode) {
510
510
  console.log(chalk.green(`Specs System (${specs.length} specs)`));
511
511
 
512
512
  // Show active specs first
513
- const activeSpecs = specs.filter((s) => s.status === 'active');
513
+ const activeSpecs = specs.filter((s) => s.status === 'active' || s.status === 'in_progress');
514
514
  const draftSpecs = specs.filter((s) => s.status === 'draft');
515
515
  const completedSpecs = specs.filter((s) => s.status === 'completed');
516
+ const closedSpecs = specs.filter((s) => s.status === 'closed' || s.status === 'archived');
516
517
 
517
518
  if (activeSpecs.length > 0) {
518
519
  console.log(
@@ -543,10 +544,19 @@ async function displayVisualStatus(data, currentMode) {
543
544
  });
544
545
  }
545
546
  }
547
+ if (closedSpecs.length > 0) {
548
+ console.log(
549
+ chalk.gray(
550
+ ` Closed: ${closedSpecs.length} spec${closedSpecs.length > 1 ? 's' : ''}`
551
+ )
552
+ );
553
+ }
546
554
 
547
- // Overall specs progress
555
+ // Overall specs progress (completed + closed both count as done)
548
556
  const totalSpecs = specs.length;
549
- const completedSpecsCount = specs.filter((s) => s.status === 'completed').length;
557
+ const completedSpecsCount = specs.filter((s) =>
558
+ s.status === 'completed' || s.status === 'closed' || s.status === 'archived'
559
+ ).length;
550
560
  const activeSpecsCount = specs.filter((s) => s.status === 'active').length;
551
561
  const progressPercentage =
552
562
  totalSpecs > 0 ? Math.round((completedSpecsCount / totalSpecs) * 100) : 0;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @fileoverview Spec Types Constants
3
- * Defines spec types and their metadata for consistent display
2
+ * @fileoverview Spec Types and Status Constants
3
+ * Defines spec types, statuses, and their metadata for consistent display
4
4
  * @author @darianrosebrook
5
5
  */
6
6
 
@@ -37,6 +37,29 @@ const SPEC_TYPES = {
37
37
  },
38
38
  };
39
39
 
40
+ /**
41
+ * Spec statuses and lifecycle metadata.
42
+ * Terminal statuses mean the spec is done — its scope restrictions
43
+ * should NOT be enforced by the scope guard.
44
+ */
45
+ const SPEC_STATUSES = {
46
+ draft: { label: 'Draft', color: chalk.yellow, terminal: false },
47
+ active: { label: 'Active', color: chalk.green, terminal: false },
48
+ in_progress: { label: 'In Progress', color: chalk.green, terminal: false },
49
+ completed: { label: 'Completed', color: chalk.blue, terminal: true },
50
+ closed: { label: 'Closed', color: chalk.gray, terminal: true },
51
+ archived: { label: 'Archived', color: chalk.gray, terminal: true },
52
+ };
53
+
54
+ /**
55
+ * Status keys that indicate a spec is done (scope no longer enforced).
56
+ */
57
+ const TERMINAL_STATUSES = Object.entries(SPEC_STATUSES)
58
+ .filter(([, v]) => v.terminal)
59
+ .map(([k]) => k);
60
+
40
61
  module.exports = {
41
62
  SPEC_TYPES,
63
+ SPEC_STATUSES,
64
+ TERMINAL_STATUSES,
42
65
  };
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ const { tutorialCommand } = require('./commands/tutorial');
51
51
  const { planCommand } = require('./commands/plan');
52
52
  const { worktreeCommand } = require('./commands/worktree');
53
53
  const { sessionCommand } = require('./commands/session');
54
+ const { parallelCommand } = require('./commands/parallel');
54
55
 
55
56
  // Import scaffold functionality
56
57
  const { scaffoldProject, setScaffoldDependencies } = require('./scaffold');
@@ -263,7 +264,7 @@ specsCmd
263
264
  specsCmd
264
265
  .command('update <id>')
265
266
  .description('Update spec properties')
266
- .option('-s, --status <status>', 'Spec status (draft, active, completed)')
267
+ .option('-s, --status <status>', 'Spec status (draft, active, in_progress, completed, closed, archived)')
267
268
  .option('--title <title>', 'Spec title')
268
269
  .option('--description <desc>', 'Spec description')
269
270
  .action((id, options) => specsCommand('update', { id, ...options }));
@@ -273,6 +274,11 @@ specsCmd
273
274
  .description('Delete a spec')
274
275
  .action((id) => specsCommand('delete', { id }));
275
276
 
277
+ specsCmd
278
+ .command('close <id>')
279
+ .description('Close a completed spec (removes scope enforcement)')
280
+ .action((id) => specsCommand('close', { id }));
281
+
276
282
  specsCmd
277
283
  .command('conflicts')
278
284
  .description('Check for scope conflicts between specs')
@@ -429,6 +435,37 @@ sessionCmd
429
435
  .description('Show session briefing for hooks/startup')
430
436
  .action(() => sessionCommand('briefing'));
431
437
 
438
+ // Parallel command group
439
+ const parallelCmd = program
440
+ .command('parallel')
441
+ .description('Orchestrate parallel multi-agent workspaces');
442
+
443
+ parallelCmd
444
+ .command('setup <plan-file>')
445
+ .description('Create worktrees and sessions from a plan file')
446
+ .option('--base-branch <branch>', 'Base branch for all worktrees')
447
+ .action((planFile, options) => parallelCommand('setup', { planFile, ...options }));
448
+
449
+ parallelCmd
450
+ .command('status')
451
+ .description('Show all active parallel worktrees and sessions')
452
+ .action(() => parallelCommand('status'));
453
+
454
+ parallelCmd
455
+ .command('merge')
456
+ .description('Merge all parallel branches back to base')
457
+ .option('--strategy <strategy>', 'Merge strategy: rebase, merge, or squash', 'merge')
458
+ .option('--dry-run', 'Preview merge without executing', false)
459
+ .option('--force', 'Force merge even with detected conflicts', false)
460
+ .action((options) => parallelCommand('merge', options));
461
+
462
+ parallelCmd
463
+ .command('teardown')
464
+ .description('Destroy all parallel worktrees')
465
+ .option('--delete-branches', 'Also delete associated branches', false)
466
+ .option('--force', 'Force removal even if worktrees are dirty', false)
467
+ .action((options) => parallelCommand('teardown', options));
468
+
432
469
  // Templates command
433
470
  program
434
471
  .command('templates [subcommand]')
@@ -699,6 +736,7 @@ program.exitOverride((err) => {
699
736
  'tool',
700
737
  'worktree',
701
738
  'session',
739
+ 'parallel',
702
740
  ];
703
741
  const similar = findSimilarCommand(commandName, validCommands);
704
742
 
@@ -792,6 +830,9 @@ if (require.main === module) {
792
830
  'hooks',
793
831
  'burnup',
794
832
  'tool',
833
+ 'worktree',
834
+ 'session',
835
+ 'parallel',
795
836
  ];
796
837
  const similar = findSimilarCommand(commandName, validCommands);
797
838
 
@@ -802,7 +843,7 @@ if (require.main === module) {
802
843
  }
803
844
 
804
845
  console.error(
805
- chalk.yellow('Available commands: init, validate, scaffold, provenance, hooks')
846
+ chalk.yellow('Available commands: init, validate, scaffold, provenance, hooks, parallel')
806
847
  );
807
848
  console.error(chalk.yellow('Try: caws --help for full command list'));
808
849
  console.error(