switchman-dev 0.1.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.
Files changed (50) hide show
  1. package/CLAUDE.md +98 -0
  2. package/README.md +243 -0
  3. package/examples/README.md +117 -0
  4. package/examples/setup.sh +102 -0
  5. package/examples/taskapi/.switchman/switchman.db +0 -0
  6. package/examples/taskapi/package-lock.json +4736 -0
  7. package/examples/taskapi/package.json +18 -0
  8. package/examples/taskapi/src/db.js +179 -0
  9. package/examples/taskapi/src/middleware/auth.js +96 -0
  10. package/examples/taskapi/src/middleware/validate.js +133 -0
  11. package/examples/taskapi/src/routes/tasks.js +65 -0
  12. package/examples/taskapi/src/routes/users.js +38 -0
  13. package/examples/taskapi/src/server.js +7 -0
  14. package/examples/taskapi/tests/api.test.js +112 -0
  15. package/examples/teardown.sh +37 -0
  16. package/examples/walkthrough.sh +172 -0
  17. package/examples/worktrees/agent-rate-limiting/package-lock.json +4736 -0
  18. package/examples/worktrees/agent-rate-limiting/package.json +18 -0
  19. package/examples/worktrees/agent-rate-limiting/src/db.js +179 -0
  20. package/examples/worktrees/agent-rate-limiting/src/middleware/auth.js +96 -0
  21. package/examples/worktrees/agent-rate-limiting/src/middleware/validate.js +133 -0
  22. package/examples/worktrees/agent-rate-limiting/src/routes/tasks.js +65 -0
  23. package/examples/worktrees/agent-rate-limiting/src/routes/users.js +38 -0
  24. package/examples/worktrees/agent-rate-limiting/src/server.js +7 -0
  25. package/examples/worktrees/agent-rate-limiting/tests/api.test.js +112 -0
  26. package/examples/worktrees/agent-tests/package-lock.json +4736 -0
  27. package/examples/worktrees/agent-tests/package.json +18 -0
  28. package/examples/worktrees/agent-tests/src/db.js +179 -0
  29. package/examples/worktrees/agent-tests/src/middleware/auth.js +96 -0
  30. package/examples/worktrees/agent-tests/src/middleware/validate.js +133 -0
  31. package/examples/worktrees/agent-tests/src/routes/tasks.js +65 -0
  32. package/examples/worktrees/agent-tests/src/routes/users.js +38 -0
  33. package/examples/worktrees/agent-tests/src/server.js +7 -0
  34. package/examples/worktrees/agent-tests/tests/api.test.js +112 -0
  35. package/examples/worktrees/agent-validation/package-lock.json +4736 -0
  36. package/examples/worktrees/agent-validation/package.json +18 -0
  37. package/examples/worktrees/agent-validation/src/db.js +179 -0
  38. package/examples/worktrees/agent-validation/src/middleware/auth.js +96 -0
  39. package/examples/worktrees/agent-validation/src/middleware/validate.js +133 -0
  40. package/examples/worktrees/agent-validation/src/routes/tasks.js +65 -0
  41. package/examples/worktrees/agent-validation/src/routes/users.js +38 -0
  42. package/examples/worktrees/agent-validation/src/server.js +7 -0
  43. package/examples/worktrees/agent-validation/tests/api.test.js +112 -0
  44. package/package.json +29 -0
  45. package/src/cli/index.js +602 -0
  46. package/src/core/db.js +240 -0
  47. package/src/core/detector.js +172 -0
  48. package/src/core/git.js +265 -0
  49. package/src/mcp/server.js +555 -0
  50. package/tests/test.js +259 -0
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * switchman CLI
4
+ * Conflict-aware task coordinator for parallel AI coding agents
5
+ *
6
+ * Commands:
7
+ * switchman init - Initialize in current repo
8
+ * switchman task add - Add a task to the queue
9
+ * switchman task list - List all tasks
10
+ * switchman task assign - Assign task to a worktree
11
+ * switchman task done - Mark task complete
12
+ * switchman worktree add - Register a worktree
13
+ * switchman worktree list - List registered worktrees
14
+ * switchman scan - Scan for conflicts across worktrees
15
+ * switchman claim - Claim files for a task
16
+ * switchman status - Show full system status
17
+ */
18
+
19
+ import { program } from 'commander';
20
+ import chalk from 'chalk';
21
+ import ora from 'ora';
22
+ import { readFileSync } from 'fs';
23
+ import { join, dirname } from 'path';
24
+ import { fileURLToPath } from 'url';
25
+ import { execSync } from 'child_process';
26
+
27
+ import { findRepoRoot, listGitWorktrees, createGitWorktree } from '../core/git.js';
28
+ import {
29
+ initDb, openDb,
30
+ createTask, assignTask, completeTask, failTask, listTasks, getTask, getNextPendingTask,
31
+ registerWorktree, listWorktrees,
32
+ claimFiles, releaseFileClaims, getActiveFileClaims, checkFileConflicts,
33
+ logConflict,
34
+ } from '../core/db.js';
35
+ import { scanAllWorktrees } from '../core/detector.js';
36
+
37
+ const __dirname = dirname(fileURLToPath(import.meta.url));
38
+
39
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
40
+
41
+ function getRepo() {
42
+ try {
43
+ return findRepoRoot();
44
+ } catch (err) {
45
+ console.error(chalk.red(err.message));
46
+ process.exit(1);
47
+ }
48
+ }
49
+
50
+ function getDb(repoRoot) {
51
+ try {
52
+ return openDb(repoRoot);
53
+ } catch (err) {
54
+ console.error(chalk.red(err.message));
55
+ process.exit(1);
56
+ }
57
+ }
58
+
59
+ function statusBadge(status) {
60
+ const colors = {
61
+ pending: chalk.yellow,
62
+ in_progress: chalk.blue,
63
+ done: chalk.green,
64
+ failed: chalk.red,
65
+ idle: chalk.gray,
66
+ busy: chalk.blue,
67
+ };
68
+ return (colors[status] || chalk.white)(status.toUpperCase().padEnd(11));
69
+ }
70
+
71
+ function printTable(rows, columns) {
72
+ if (!rows.length) return;
73
+ const widths = columns.map(col =>
74
+ Math.max(col.label.length, ...rows.map(r => String(r[col.key] || '').length))
75
+ );
76
+ const header = columns.map((col, i) => col.label.padEnd(widths[i])).join(' ');
77
+ console.log(chalk.dim(header));
78
+ console.log(chalk.dim('─'.repeat(header.length)));
79
+ for (const row of rows) {
80
+ console.log(columns.map((col, i) => {
81
+ const val = String(row[col.key] || '');
82
+ return col.format ? col.format(val) : val.padEnd(widths[i]);
83
+ }).join(' '));
84
+ }
85
+ }
86
+
87
+ // ─── Program ──────────────────────────────────────────────────────────────────
88
+
89
+ program
90
+ .name('switchman')
91
+ .description('Conflict-aware task coordinator for parallel AI coding agents')
92
+ .version('0.1.0');
93
+
94
+ // ── init ──────────────────────────────────────────────────────────────────────
95
+
96
+ program
97
+ .command('init')
98
+ .description('Initialize switchman in the current git repository')
99
+ .action(() => {
100
+ const repoRoot = getRepo();
101
+ const spinner = ora('Initializing switchman...').start();
102
+ try {
103
+ const db = initDb(repoRoot);
104
+
105
+ // Auto-register existing git worktrees
106
+ const gitWorktrees = listGitWorktrees(repoRoot);
107
+ for (const wt of gitWorktrees) {
108
+ registerWorktree(db, { name: wt.name, path: wt.path, branch: wt.branch || 'unknown' });
109
+ }
110
+
111
+ db.close();
112
+ spinner.succeed(`Initialized in ${chalk.cyan(repoRoot)}`);
113
+ console.log(chalk.dim(` Found and registered ${gitWorktrees.length} git worktree(s)`));
114
+ console.log(chalk.dim(` Database: .switchman/switchman.db`));
115
+ console.log('');
116
+ console.log(`Next steps:`);
117
+ console.log(` ${chalk.cyan('switchman task add "Fix the login bug"')} — add a task`);
118
+ console.log(` ${chalk.cyan('switchman scan')} — check for conflicts`);
119
+ console.log(` ${chalk.cyan('switchman status')} — view full status`);
120
+ } catch (err) {
121
+ spinner.fail(err.message);
122
+ process.exit(1);
123
+ }
124
+ });
125
+
126
+
127
+ // ── setup ─────────────────────────────────────────────────────────────────────
128
+
129
+ program
130
+ .command('setup')
131
+ .description('One-command setup: create agent worktrees and initialise switchman')
132
+ .option('-a, --agents <n>', 'Number of agent worktrees to create (default: 3)', '3')
133
+ .option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
134
+ .action((opts) => {
135
+ const agentCount = parseInt(opts.agents);
136
+
137
+ if (isNaN(agentCount) || agentCount < 1 || agentCount > 10) {
138
+ console.error(chalk.red('--agents must be a number between 1 and 10'));
139
+ process.exit(1);
140
+ }
141
+
142
+ const repoRoot = getRepo();
143
+ const spinner = ora('Setting up Switchman...').start();
144
+
145
+ try {
146
+ // git worktree add requires at least one commit
147
+ try {
148
+ execSync('git rev-parse HEAD', {
149
+ cwd: repoRoot,
150
+ stdio: ['pipe', 'pipe', 'pipe'],
151
+ });
152
+ } catch {
153
+ spinner.fail('Your repo needs at least one commit before worktrees can be created.');
154
+ console.log(chalk.dim(' Run: git commit --allow-empty -m "init" then try again'));
155
+ process.exit(1);
156
+ }
157
+
158
+ // Init the switchman database
159
+ const db = initDb(repoRoot);
160
+
161
+ // Create one worktree per agent
162
+ const created = [];
163
+ for (let i = 1; i <= agentCount; i++) {
164
+ const name = `agent${i}`;
165
+ const branch = `${opts.prefix}/agent${i}`;
166
+ spinner.text = `Creating worktree ${i}/${agentCount}...`;
167
+ try {
168
+ const wtPath = createGitWorktree(repoRoot, name, branch);
169
+ registerWorktree(db, { name, path: wtPath, branch });
170
+ created.push({ name, path: wtPath, branch });
171
+ } catch {
172
+ // Worktree already exists — register it without failing
173
+ const repoName = repoRoot.split('/').pop();
174
+ const wtPath = join(repoRoot, '..', `${repoName}-${name}`);
175
+ registerWorktree(db, { name, path: wtPath, branch });
176
+ created.push({ name, path: wtPath, branch, existed: true });
177
+ }
178
+ }
179
+
180
+ // Register the main worktree too
181
+ const gitWorktrees = listGitWorktrees(repoRoot);
182
+ for (const wt of gitWorktrees) {
183
+ registerWorktree(db, { name: wt.name, path: wt.path, branch: wt.branch || 'unknown' });
184
+ }
185
+
186
+ db.close();
187
+
188
+ const label = agentCount === 1 ? 'workspace' : 'workspaces';
189
+ spinner.succeed(`Switchman ready — ${agentCount} agent ${label} created`);
190
+ console.log('');
191
+
192
+ for (const wt of created) {
193
+ const note = wt.existed ? chalk.dim(' (already existed, re-registered)') : '';
194
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(wt.path)}${note}`);
195
+ console.log(` ${chalk.dim('branch:')} ${wt.branch}`);
196
+ }
197
+
198
+ console.log('');
199
+ console.log(chalk.bold('Next steps:'));
200
+ console.log(` 1. Add your tasks:`);
201
+ console.log(` ${chalk.cyan('switchman task add "Your first task" --priority 8')}`);
202
+ console.log(` 2. Open Claude Code in each folder above — agents will coordinate automatically`);
203
+ console.log(` 3. Check status at any time:`);
204
+ console.log(` ${chalk.cyan('switchman status')}`);
205
+ console.log('');
206
+
207
+ } catch (err) {
208
+ spinner.fail(err.message);
209
+ process.exit(1);
210
+ }
211
+ });
212
+
213
+
214
+ // ── task ──────────────────────────────────────────────────────────────────────
215
+
216
+ const taskCmd = program.command('task').description('Manage the task queue');
217
+
218
+ taskCmd
219
+ .command('add <title>')
220
+ .description('Add a new task to the queue')
221
+ .option('-d, --description <desc>', 'Task description')
222
+ .option('-p, --priority <n>', 'Priority 1-10 (default 5)', '5')
223
+ .option('--id <id>', 'Custom task ID')
224
+ .action((title, opts) => {
225
+ const repoRoot = getRepo();
226
+ const db = getDb(repoRoot);
227
+ const taskId = createTask(db, {
228
+ id: opts.id,
229
+ title,
230
+ description: opts.description,
231
+ priority: parseInt(opts.priority),
232
+ });
233
+ db.close();
234
+ console.log(`${chalk.green('✓')} Task created: ${chalk.cyan(taskId)}`);
235
+ console.log(` ${chalk.dim(title)}`);
236
+ });
237
+
238
+ taskCmd
239
+ .command('list')
240
+ .description('List all tasks')
241
+ .option('-s, --status <status>', 'Filter by status (pending|in_progress|done|failed)')
242
+ .action((opts) => {
243
+ const repoRoot = getRepo();
244
+ const db = getDb(repoRoot);
245
+ const tasks = listTasks(db, opts.status);
246
+ db.close();
247
+
248
+ if (!tasks.length) {
249
+ console.log(chalk.dim('No tasks found.'));
250
+ return;
251
+ }
252
+
253
+ console.log('');
254
+ for (const t of tasks) {
255
+ const badge = statusBadge(t.status);
256
+ const worktree = t.worktree ? chalk.cyan(t.worktree) : chalk.dim('unassigned');
257
+ console.log(`${badge} ${chalk.bold(t.title)}`);
258
+ console.log(` ${chalk.dim('id:')} ${t.id} ${chalk.dim('worktree:')} ${worktree} ${chalk.dim('priority:')} ${t.priority}`);
259
+ if (t.description) console.log(` ${chalk.dim(t.description)}`);
260
+ console.log('');
261
+ }
262
+ });
263
+
264
+ taskCmd
265
+ .command('assign <taskId> <worktree>')
266
+ .description('Assign a task to a worktree')
267
+ .option('--agent <name>', 'Agent name (e.g. claude-code)')
268
+ .action((taskId, worktree, opts) => {
269
+ const repoRoot = getRepo();
270
+ const db = getDb(repoRoot);
271
+ const ok = assignTask(db, taskId, worktree, opts.agent);
272
+ db.close();
273
+ if (ok) {
274
+ console.log(`${chalk.green('✓')} Assigned ${chalk.cyan(taskId)} → ${chalk.cyan(worktree)}`);
275
+ } else {
276
+ console.log(chalk.red(`Could not assign task. It may not exist or is not in 'pending' status.`));
277
+ }
278
+ });
279
+
280
+ taskCmd
281
+ .command('done <taskId>')
282
+ .description('Mark a task as complete and release all file claims')
283
+ .action((taskId) => {
284
+ const repoRoot = getRepo();
285
+ const db = getDb(repoRoot);
286
+ completeTask(db, taskId);
287
+ releaseFileClaims(db, taskId);
288
+ db.close();
289
+ console.log(`${chalk.green('✓')} Task ${chalk.cyan(taskId)} marked done — file claims released`);
290
+ });
291
+
292
+ taskCmd
293
+ .command('fail <taskId> [reason]')
294
+ .description('Mark a task as failed')
295
+ .action((taskId, reason) => {
296
+ const repoRoot = getRepo();
297
+ const db = getDb(repoRoot);
298
+ failTask(db, taskId, reason);
299
+ releaseFileClaims(db, taskId);
300
+ db.close();
301
+ console.log(`${chalk.red('✗')} Task ${chalk.cyan(taskId)} marked failed`);
302
+ });
303
+
304
+ taskCmd
305
+ .command('next')
306
+ .description('Get and assign the next pending task (for agent automation)')
307
+ .option('--json', 'Output as JSON')
308
+ .option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
309
+ .option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
310
+ .action((opts) => {
311
+ const repoRoot = getRepo();
312
+ const db = getDb(repoRoot);
313
+ const task = getNextPendingTask(db);
314
+
315
+ if (!task) {
316
+ db.close();
317
+ if (opts.json) console.log(JSON.stringify({ task: null }));
318
+ else console.log(chalk.dim('No pending tasks.'));
319
+ return;
320
+ }
321
+
322
+ // Determine worktree name: explicit flag, or derive from cwd
323
+ const worktreeName = opts.worktree || process.cwd().split('/').pop();
324
+ const assigned = assignTask(db, task.id, worktreeName, opts.agent || null);
325
+ db.close();
326
+
327
+ if (!assigned) {
328
+ // Race condition: another agent grabbed it between get and assign
329
+ if (opts.json) console.log(JSON.stringify({ task: null, message: 'Task claimed by another agent — try again' }));
330
+ else console.log(chalk.yellow('Task was just claimed by another agent. Run again to get the next one.'));
331
+ return;
332
+ }
333
+
334
+ if (opts.json) {
335
+ console.log(JSON.stringify({ task: { ...task, worktree: worktreeName, status: 'in_progress' } }, null, 2));
336
+ } else {
337
+ console.log(`${chalk.green('✓')} Assigned: ${chalk.bold(task.title)}`);
338
+ console.log(` ${chalk.dim('id:')} ${task.id} ${chalk.dim('worktree:')} ${chalk.cyan(worktreeName)} ${chalk.dim('priority:')} ${task.priority}`);
339
+ }
340
+ });
341
+
342
+ // ── worktree ───────────────────────────────────────────────────────────────────
343
+
344
+ const wtCmd = program.command('worktree').description('Manage worktrees');
345
+
346
+ wtCmd
347
+ .command('add <name> <path> <branch>')
348
+ .description('Register a worktree with switchman')
349
+ .option('--agent <name>', 'Agent assigned to this worktree')
350
+ .action((name, path, branch, opts) => {
351
+ const repoRoot = getRepo();
352
+ const db = getDb(repoRoot);
353
+ registerWorktree(db, { name, path, branch, agent: opts.agent });
354
+ db.close();
355
+ console.log(`${chalk.green('✓')} Registered worktree: ${chalk.cyan(name)}`);
356
+ });
357
+
358
+ wtCmd
359
+ .command('list')
360
+ .description('List all registered worktrees')
361
+ .action(() => {
362
+ const repoRoot = getRepo();
363
+ const db = getDb(repoRoot);
364
+ const worktrees = listWorktrees(db);
365
+ const gitWorktrees = listGitWorktrees(repoRoot);
366
+ db.close();
367
+
368
+ if (!worktrees.length && !gitWorktrees.length) {
369
+ console.log(chalk.dim('No worktrees found.'));
370
+ return;
371
+ }
372
+
373
+ // Show git worktrees (source of truth) annotated with db info
374
+ console.log('');
375
+ console.log(chalk.bold('Git Worktrees:'));
376
+ for (const wt of gitWorktrees) {
377
+ const dbInfo = worktrees.find(d => d.path === wt.path);
378
+ const agent = dbInfo?.agent ? chalk.cyan(dbInfo.agent) : chalk.dim('no agent');
379
+ const status = dbInfo?.status ? statusBadge(dbInfo.status) : chalk.dim('unregistered');
380
+ console.log(` ${chalk.bold(wt.name.padEnd(20))} ${status} branch: ${chalk.cyan(wt.branch || 'unknown')} agent: ${agent}`);
381
+ console.log(` ${chalk.dim(wt.path)}`);
382
+ }
383
+ console.log('');
384
+ });
385
+
386
+ wtCmd
387
+ .command('sync')
388
+ .description('Sync git worktrees into the switchman database')
389
+ .action(() => {
390
+ const repoRoot = getRepo();
391
+ const db = getDb(repoRoot);
392
+ const gitWorktrees = listGitWorktrees(repoRoot);
393
+ for (const wt of gitWorktrees) {
394
+ registerWorktree(db, { name: wt.name, path: wt.path, branch: wt.branch || 'unknown' });
395
+ }
396
+ db.close();
397
+ console.log(`${chalk.green('✓')} Synced ${gitWorktrees.length} worktree(s) from git`);
398
+ });
399
+
400
+ // ── claim ──────────────────────────────────────────────────────────────────────
401
+
402
+ program
403
+ .command('claim <taskId> <worktree> [files...]')
404
+ .description('Claim files for a task (warns if conflicts exist)')
405
+ .option('--agent <name>', 'Agent name')
406
+ .option('--force', 'Claim even if conflicts exist')
407
+ .action((taskId, worktree, files, opts) => {
408
+ if (!files.length) {
409
+ console.log(chalk.yellow('No files specified. Use: switchman claim <taskId> <worktree> file1 file2 ...'));
410
+ return;
411
+ }
412
+ const repoRoot = getRepo();
413
+ const db = getDb(repoRoot);
414
+
415
+ // Check for existing claims
416
+ const conflicts = checkFileConflicts(db, files, worktree);
417
+
418
+ if (conflicts.length > 0 && !opts.force) {
419
+ console.log(chalk.red(`\n⚠ Claim conflicts detected:`));
420
+ for (const c of conflicts) {
421
+ console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
422
+ }
423
+ console.log(chalk.dim('\nUse --force to claim anyway, or resolve conflicts first.'));
424
+ db.close();
425
+ return;
426
+ }
427
+
428
+ claimFiles(db, taskId, worktree, files, opts.agent);
429
+ db.close();
430
+ console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)}`);
431
+ files.forEach(f => console.log(` ${chalk.dim(f)}`));
432
+ });
433
+
434
+ program
435
+ .command('release <taskId>')
436
+ .description('Release all file claims for a task')
437
+ .action((taskId) => {
438
+ const repoRoot = getRepo();
439
+ const db = getDb(repoRoot);
440
+ releaseFileClaims(db, taskId);
441
+ db.close();
442
+ console.log(`${chalk.green('✓')} Released all claims for task ${chalk.cyan(taskId)}`);
443
+ });
444
+
445
+ // ── scan ───────────────────────────────────────────────────────────────────────
446
+
447
+ program
448
+ .command('scan')
449
+ .description('Scan all worktrees for conflicts')
450
+ .option('--json', 'Output raw JSON')
451
+ .option('--quiet', 'Only show conflicts')
452
+ .action(async (opts) => {
453
+ const repoRoot = getRepo();
454
+ const db = getDb(repoRoot);
455
+ const spinner = ora('Scanning worktrees for conflicts...').start();
456
+
457
+ try {
458
+ const report = await scanAllWorktrees(db, repoRoot);
459
+ db.close();
460
+ spinner.stop();
461
+
462
+ if (opts.json) {
463
+ console.log(JSON.stringify(report, null, 2));
464
+ return;
465
+ }
466
+
467
+ console.log('');
468
+ console.log(chalk.bold(`Conflict Scan Report`));
469
+ console.log(chalk.dim(`${report.scannedAt}`));
470
+ console.log('');
471
+
472
+ // Worktrees summary
473
+ if (!opts.quiet) {
474
+ console.log(chalk.bold('Worktrees:'));
475
+ for (const wt of report.worktrees) {
476
+ const files = report.fileMap?.[wt.name] || [];
477
+ console.log(` ${chalk.cyan(wt.name.padEnd(20))} branch: ${(wt.branch || 'unknown').padEnd(30)} ${chalk.dim(files.length + ' changed file(s)')}`);
478
+ }
479
+ console.log('');
480
+ }
481
+
482
+ // File-level overlaps (uncommitted)
483
+ if (report.fileConflicts.length > 0) {
484
+ console.log(chalk.yellow(`⚠ Files being edited in multiple worktrees (uncommitted):`));
485
+ for (const fc of report.fileConflicts) {
486
+ console.log(` ${chalk.yellow(fc.file)}`);
487
+ console.log(` ${chalk.dim('edited in:')} ${fc.worktrees.join(', ')}`);
488
+ }
489
+ console.log('');
490
+ }
491
+
492
+ // Branch-level conflicts
493
+ if (report.conflicts.length > 0) {
494
+ console.log(chalk.red(`✗ Branch conflicts detected:`));
495
+ for (const c of report.conflicts) {
496
+ const icon = c.type === 'merge_conflict' ? chalk.red('MERGE CONFLICT') : chalk.yellow('FILE OVERLAP');
497
+ console.log(` ${icon}`);
498
+ console.log(` ${chalk.cyan(c.worktreeA)} (${c.branchA}) ↔ ${chalk.cyan(c.worktreeB)} (${c.branchB})`);
499
+ if (c.conflictingFiles.length) {
500
+ console.log(` Conflicting files:`);
501
+ c.conflictingFiles.forEach(f => console.log(` ${chalk.yellow(f)}`));
502
+ }
503
+ }
504
+ console.log('');
505
+ }
506
+
507
+ // All clear
508
+ if (report.conflicts.length === 0 && report.fileConflicts.length === 0) {
509
+ console.log(chalk.green(`✓ No conflicts detected across ${report.worktrees.length} worktree(s)`));
510
+ }
511
+
512
+ } catch (err) {
513
+ spinner.fail(err.message);
514
+ db.close();
515
+ process.exit(1);
516
+ }
517
+ });
518
+
519
+ // ── status ─────────────────────────────────────────────────────────────────────
520
+
521
+ program
522
+ .command('status')
523
+ .description('Show full system status: tasks, worktrees, claims, and conflicts')
524
+ .action(async () => {
525
+ const repoRoot = getRepo();
526
+ const db = getDb(repoRoot);
527
+
528
+ console.log('');
529
+ console.log(chalk.bold.cyan('━━━ switchman status ━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
530
+ console.log(chalk.dim(`Repo: ${repoRoot}`));
531
+ console.log('');
532
+
533
+ // Tasks
534
+ const tasks = listTasks(db);
535
+ const pending = tasks.filter(t => t.status === 'pending');
536
+ const inProgress = tasks.filter(t => t.status === 'in_progress');
537
+ const done = tasks.filter(t => t.status === 'done');
538
+ const failed = tasks.filter(t => t.status === 'failed');
539
+
540
+ console.log(chalk.bold('Tasks:'));
541
+ console.log(` ${chalk.yellow('Pending')} ${pending.length}`);
542
+ console.log(` ${chalk.blue('In Progress')} ${inProgress.length}`);
543
+ console.log(` ${chalk.green('Done')} ${done.length}`);
544
+ console.log(` ${chalk.red('Failed')} ${failed.length}`);
545
+
546
+ if (inProgress.length > 0) {
547
+ console.log('');
548
+ console.log(chalk.bold('Active Tasks:'));
549
+ for (const t of inProgress) {
550
+ console.log(` ${chalk.cyan(t.worktree || 'unassigned')} → ${t.title}`);
551
+ }
552
+ }
553
+
554
+ if (pending.length > 0) {
555
+ console.log('');
556
+ console.log(chalk.bold('Next Up:'));
557
+ const next = pending.slice(0, 3);
558
+ for (const t of next) {
559
+ console.log(` [p${t.priority}] ${t.title} ${chalk.dim(t.id)}`);
560
+ }
561
+ }
562
+
563
+ // File Claims
564
+ const claims = getActiveFileClaims(db);
565
+ if (claims.length > 0) {
566
+ console.log('');
567
+ console.log(chalk.bold(`Active File Claims (${claims.length}):`));
568
+ const byWorktree = {};
569
+ for (const c of claims) {
570
+ if (!byWorktree[c.worktree]) byWorktree[c.worktree] = [];
571
+ byWorktree[c.worktree].push(c.file_path);
572
+ }
573
+ for (const [wt, files] of Object.entries(byWorktree)) {
574
+ console.log(` ${chalk.cyan(wt)}: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` +${files.length - 5} more` : ''}`);
575
+ }
576
+ }
577
+
578
+ // Quick conflict scan
579
+ console.log('');
580
+ const spinner = ora('Running conflict scan...').start();
581
+ try {
582
+ const report = await scanAllWorktrees(db, repoRoot);
583
+ spinner.stop();
584
+
585
+ const totalConflicts = report.conflicts.length + report.fileConflicts.length;
586
+ if (totalConflicts === 0) {
587
+ console.log(chalk.green(`✓ No conflicts across ${report.worktrees.length} worktree(s)`));
588
+ } else {
589
+ console.log(chalk.red(`⚠ ${totalConflicts} conflict(s) detected — run 'switchman scan' for details`));
590
+ }
591
+ } catch {
592
+ spinner.stop();
593
+ console.log(chalk.dim('Could not run conflict scan'));
594
+ }
595
+
596
+ db.close();
597
+ console.log('');
598
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
599
+ console.log('');
600
+ });
601
+
602
+ program.parse();