switchman-dev 0.1.4 → 0.1.5

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/src/cli/index.js CHANGED
@@ -19,6 +19,7 @@
19
19
  import { program } from 'commander';
20
20
  import chalk from 'chalk';
21
21
  import ora from 'ora';
22
+ import { existsSync } from 'fs';
22
23
  import { join } from 'path';
23
24
  import { execSync, spawn } from 'child_process';
24
25
 
@@ -170,6 +171,40 @@ function renderMiniBar(items) {
170
171
  return items.map(({ label, value, color = chalk.white }) => `${color('■')} ${label}:${value}`).join(chalk.dim(' '));
171
172
  }
172
173
 
174
+ function renderChip(label, value, color = chalk.white) {
175
+ return color(`[${label}:${value}]`);
176
+ }
177
+
178
+ function renderSignalStrip(signals) {
179
+ return signals.join(chalk.dim(' '));
180
+ }
181
+
182
+ function formatClockTime(isoString) {
183
+ if (!isoString) return null;
184
+ const date = new Date(isoString);
185
+ if (Number.isNaN(date.getTime())) return null;
186
+ return date.toLocaleTimeString('en-GB', {
187
+ hour: '2-digit',
188
+ minute: '2-digit',
189
+ second: '2-digit',
190
+ hour12: false,
191
+ });
192
+ }
193
+
194
+ function buildWatchSignature(report) {
195
+ return JSON.stringify({
196
+ health: report.health,
197
+ summary: report.summary,
198
+ counts: report.counts,
199
+ active_work: report.active_work,
200
+ attention: report.attention,
201
+ queue_summary: report.queue?.summary || null,
202
+ next_up: report.next_up || null,
203
+ next_steps: report.next_steps,
204
+ suggested_commands: report.suggested_commands,
205
+ });
206
+ }
207
+
173
208
  function formatRelativePolicy(policy) {
174
209
  return `stale ${policy.stale_after_minutes}m • heartbeat ${policy.heartbeat_interval_seconds}s • auto-reap ${policy.reap_on_status_check ? 'on' : 'off'}`;
175
210
  }
@@ -178,6 +213,155 @@ function sleepSync(ms) {
178
213
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
179
214
  }
180
215
 
216
+ function boolBadge(ok) {
217
+ return ok ? chalk.green('OK ') : chalk.yellow('CHECK');
218
+ }
219
+
220
+ function printErrorWithNext(message, nextCommand = null) {
221
+ console.error(chalk.red(message));
222
+ if (nextCommand) {
223
+ console.error(`${chalk.yellow('next:')} ${nextCommand}`);
224
+ }
225
+ }
226
+
227
+ function collectSetupVerification(repoRoot, { homeDir = null } = {}) {
228
+ const dbPath = join(repoRoot, '.switchman', 'switchman.db');
229
+ const rootMcpPath = join(repoRoot, '.mcp.json');
230
+ const cursorMcpPath = join(repoRoot, '.cursor', 'mcp.json');
231
+ const claudeGuidePath = join(repoRoot, 'CLAUDE.md');
232
+ const checks = [];
233
+ const nextSteps = [];
234
+ let workspaces = [];
235
+ let db = null;
236
+
237
+ const dbExists = existsSync(dbPath);
238
+ checks.push({
239
+ key: 'database',
240
+ ok: dbExists,
241
+ label: 'Project database',
242
+ detail: dbExists ? '.switchman/switchman.db is ready' : 'Switchman database is missing',
243
+ });
244
+ if (!dbExists) {
245
+ nextSteps.push('Run `switchman init` or `switchman setup --agents 3` in this repo.');
246
+ }
247
+
248
+ if (dbExists) {
249
+ try {
250
+ db = getDb(repoRoot);
251
+ workspaces = listWorktrees(db);
252
+ } catch {
253
+ checks.push({
254
+ key: 'database_open',
255
+ ok: false,
256
+ label: 'Database access',
257
+ detail: 'Switchman could not open the project database',
258
+ });
259
+ nextSteps.push('Re-run `switchman init` if the project database looks corrupted.');
260
+ } finally {
261
+ try { db?.close(); } catch { /* no-op */ }
262
+ }
263
+ }
264
+
265
+ const agentWorkspaces = workspaces.filter((entry) => entry.name !== 'main');
266
+ const workspaceReady = agentWorkspaces.length > 0;
267
+ checks.push({
268
+ key: 'workspaces',
269
+ ok: workspaceReady,
270
+ label: 'Agent workspaces',
271
+ detail: workspaceReady
272
+ ? `${agentWorkspaces.length} agent workspace(s) registered`
273
+ : 'No agent workspaces are registered yet',
274
+ });
275
+ if (!workspaceReady) {
276
+ nextSteps.push('Run `switchman setup --agents 3` to create agent workspaces.');
277
+ }
278
+
279
+ const rootMcpExists = existsSync(rootMcpPath);
280
+ checks.push({
281
+ key: 'claude_mcp',
282
+ ok: rootMcpExists,
283
+ label: 'Claude Code MCP',
284
+ detail: rootMcpExists ? '.mcp.json is present in the repo root' : '.mcp.json is missing from the repo root',
285
+ });
286
+ if (!rootMcpExists) {
287
+ nextSteps.push('Re-run `switchman setup --agents 3` to restore the repo-local MCP config.');
288
+ }
289
+
290
+ const cursorMcpExists = existsSync(cursorMcpPath);
291
+ checks.push({
292
+ key: 'cursor_mcp',
293
+ ok: cursorMcpExists,
294
+ label: 'Cursor MCP',
295
+ detail: cursorMcpExists ? '.cursor/mcp.json is present in the repo root' : '.cursor/mcp.json is missing from the repo root',
296
+ });
297
+ if (!cursorMcpExists) {
298
+ nextSteps.push('Re-run `switchman setup --agents 3` if you want Cursor to attach automatically.');
299
+ }
300
+
301
+ const claudeGuideExists = existsSync(claudeGuidePath);
302
+ checks.push({
303
+ key: 'claude_md',
304
+ ok: claudeGuideExists,
305
+ label: 'Claude guide',
306
+ detail: claudeGuideExists ? 'CLAUDE.md is present' : 'CLAUDE.md is optional but recommended for Claude Code',
307
+ });
308
+ if (!claudeGuideExists) {
309
+ nextSteps.push('If you use Claude Code, add `CLAUDE.md` from the repo root setup guide.');
310
+ }
311
+
312
+ const windsurfConfigExists = existsSync(getWindsurfMcpConfigPath(homeDir || undefined));
313
+ checks.push({
314
+ key: 'windsurf_mcp',
315
+ ok: windsurfConfigExists,
316
+ label: 'Windsurf MCP',
317
+ detail: windsurfConfigExists
318
+ ? 'Windsurf shared MCP config is installed'
319
+ : 'Windsurf shared MCP config is optional and not installed',
320
+ });
321
+ if (!windsurfConfigExists) {
322
+ nextSteps.push('If you use Windsurf, run `switchman mcp install --windsurf` once.');
323
+ }
324
+
325
+ const ok = checks.every((item) => item.ok || ['claude_md', 'windsurf_mcp'].includes(item.key));
326
+ return {
327
+ ok,
328
+ repo_root: repoRoot,
329
+ checks,
330
+ workspaces: workspaces.map((entry) => ({
331
+ name: entry.name,
332
+ path: entry.path,
333
+ branch: entry.branch,
334
+ })),
335
+ suggested_commands: [
336
+ 'switchman status --watch',
337
+ 'switchman task add "Your first task" --priority 8',
338
+ 'switchman gate ci',
339
+ ...nextSteps.some((step) => step.includes('Windsurf')) ? ['switchman mcp install --windsurf'] : [],
340
+ ],
341
+ next_steps: [...new Set(nextSteps)].slice(0, 6),
342
+ };
343
+ }
344
+
345
+ function renderSetupVerification(report, { compact = false } = {}) {
346
+ console.log(chalk.bold(compact ? 'First-run check:' : 'Setup verification:'));
347
+ for (const check of report.checks) {
348
+ const badge = boolBadge(check.ok);
349
+ console.log(` ${badge} ${check.label} ${chalk.dim(`— ${check.detail}`)}`);
350
+ }
351
+ if (report.next_steps.length > 0) {
352
+ console.log('');
353
+ console.log(chalk.bold('Fix next:'));
354
+ for (const step of report.next_steps) {
355
+ console.log(` - ${step}`);
356
+ }
357
+ }
358
+ console.log('');
359
+ console.log(chalk.bold('Try next:'));
360
+ for (const command of report.suggested_commands.slice(0, 4)) {
361
+ console.log(` ${chalk.cyan(command)}`);
362
+ }
363
+ }
364
+
181
365
  function summarizeLeaseScope(db, lease) {
182
366
  const reservations = listScopeReservations(db, { leaseId: lease.id });
183
367
  const pathScopes = reservations
@@ -665,13 +849,34 @@ function renderUnifiedStatusReport(report) {
665
849
  const badge = healthColor(healthLabel(report.health));
666
850
  const mergeColor = report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red;
667
851
  const queueCounts = report.counts.queue;
852
+ const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
853
+ const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
854
+ const focusItem = blockedCount > 0
855
+ ? report.attention.find((item) => item.severity === 'block')
856
+ : warningCount > 0
857
+ ? report.attention.find((item) => item.severity !== 'block')
858
+ : report.next_up[0];
859
+ const focusLine = focusItem
860
+ ? ('title' in focusItem
861
+ ? `${focusItem.title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
862
+ : `${focusItem.title} ${chalk.dim(focusItem.id)}`)
863
+ : 'Nothing urgent. Safe to keep parallel work moving.';
864
+ const queueLoad = queueCounts.queued + queueCounts.retrying + queueCounts.merging + queueCounts.blocked;
865
+ const landingLabel = report.merge_readiness.ci_gate_ok ? 'ready' : 'hold';
668
866
 
669
867
  console.log('');
670
- console.log(healthColor('='.repeat(64)));
671
- console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• user-centred repo overview')}`);
868
+ console.log(healthColor('='.repeat(72)));
869
+ console.log(`${badge} ${chalk.bold('switchman status')} ${chalk.dim('• mission control for parallel agents')}`);
672
870
  console.log(`${chalk.dim(report.repo_root)}`);
673
871
  console.log(`${chalk.dim(report.summary)}`);
674
- console.log(healthColor('='.repeat(64)));
872
+ console.log(healthColor('='.repeat(72)));
873
+ console.log(renderSignalStrip([
874
+ renderChip('health', healthLabel(report.health), healthColor),
875
+ renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
876
+ renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
877
+ renderChip('landing', landingLabel, mergeColor),
878
+ renderChip('queue', queueLoad, queueLoad > 0 ? chalk.blue : chalk.green),
879
+ ]));
675
880
  console.log(renderMetricRow([
676
881
  { label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
677
882
  { label: 'leases', value: `${report.counts.active_leases} active`, color: chalk.blue },
@@ -685,13 +890,18 @@ function renderUnifiedStatusReport(report) {
685
890
  { label: 'merging', value: queueCounts.merging, color: chalk.blue },
686
891
  { label: 'merged', value: queueCounts.merged, color: chalk.green },
687
892
  ]));
893
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
688
894
  console.log(chalk.dim(`policy: ${formatRelativePolicy(report.lease_policy)} • requeue on reap ${report.lease_policy.requeue_task_on_reap ? 'on' : 'off'}`));
689
895
 
690
896
  const runningLines = report.active_work.length > 0
691
897
  ? report.active_work.slice(0, 5).map((item) => {
692
- const boundary = item.boundary_validation ? ` validation:${item.boundary_validation.status}` : '';
693
- const stale = (item.dependency_invalidations?.length || 0) > 0 ? ` stale:${item.dependency_invalidations.length}` : '';
694
- return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${chalk.dim(boundary)}${chalk.dim(stale)}`;
898
+ const boundary = item.boundary_validation
899
+ ? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
900
+ : '';
901
+ const stale = (item.dependency_invalidations?.length || 0) > 0
902
+ ? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
903
+ : '';
904
+ return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`;
695
905
  })
696
906
  : [chalk.dim('Nothing active right now.')];
697
907
 
@@ -700,7 +910,7 @@ function renderUnifiedStatusReport(report) {
700
910
 
701
911
  const blockedLines = blockedItems.length > 0
702
912
  ? blockedItems.slice(0, 4).flatMap((item) => {
703
- const lines = [`${chalk.red('BLOCK')} ${item.title}`];
913
+ const lines = [`${renderChip('BLOCKED', item.kind || 'item', chalk.red)} ${item.title}`];
704
914
  if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
705
915
  lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
706
916
  if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
@@ -710,8 +920,7 @@ function renderUnifiedStatusReport(report) {
710
920
 
711
921
  const warningLines = warningItems.length > 0
712
922
  ? warningItems.slice(0, 4).flatMap((item) => {
713
- const tone = chalk.yellow('WARN ');
714
- const lines = [`${tone} ${item.title}`];
923
+ const lines = [`${renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
715
924
  if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
716
925
  lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
717
926
  if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
@@ -728,7 +937,7 @@ function renderUnifiedStatusReport(report) {
728
937
  .filter((entry) => ['blocked', 'retrying', 'merging'].includes(entry.status))
729
938
  .slice(0, 4)
730
939
  .flatMap((item) => {
731
- const lines = [`${statusBadge(item.status)} ${item.id} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
940
+ const lines = [`${renderChip(item.status.toUpperCase(), item.id, item.status === 'blocked' ? chalk.red : item.status === 'retrying' ? chalk.yellow : chalk.blue)} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
732
941
  if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
733
942
  if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
734
943
  return lines;
@@ -738,7 +947,7 @@ function renderUnifiedStatusReport(report) {
738
947
 
739
948
  const nextActionLines = [
740
949
  ...(report.next_up.length > 0
741
- ? report.next_up.map((task) => `[p${task.priority}] ${task.title} ${chalk.dim(task.id)}`)
950
+ ? report.next_up.map((task) => `${renderChip('NEXT', `p${task.priority}`, chalk.green)} ${task.title} ${chalk.dim(task.id)}`)
742
951
  : [chalk.dim('No pending tasks waiting right now.')]),
743
952
  '',
744
953
  ...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
@@ -859,6 +1068,23 @@ program
859
1068
  .description('Conflict-aware task coordinator for parallel AI coding agents')
860
1069
  .version('0.1.0');
861
1070
 
1071
+ program.showHelpAfterError('(run with --help for usage examples)');
1072
+ program.addHelpText('after', `
1073
+ Start here:
1074
+ switchman setup --agents 5
1075
+ switchman status --watch
1076
+ switchman gate ci
1077
+
1078
+ Most useful commands:
1079
+ switchman task add "Implement auth helper" --priority 9
1080
+ switchman lease next --json
1081
+ switchman queue run --watch
1082
+
1083
+ Docs:
1084
+ README.md
1085
+ docs/setup-cursor.md
1086
+ `);
1087
+
862
1088
  // ── init ──────────────────────────────────────────────────────────────────────
863
1089
 
864
1090
  program
@@ -902,6 +1128,11 @@ program
902
1128
  .description('One-command setup: create agent workspaces and initialise Switchman')
903
1129
  .option('-a, --agents <n>', 'Number of agent workspaces to create (default: 3)', '3')
904
1130
  .option('--prefix <prefix>', 'Branch prefix (default: switchman)', 'switchman')
1131
+ .addHelpText('after', `
1132
+ Examples:
1133
+ switchman setup --agents 5
1134
+ switchman setup --agents 3 --prefix team
1135
+ `)
905
1136
  .action((opts) => {
906
1137
  const agentCount = parseInt(opts.agents);
907
1138
 
@@ -984,12 +1215,41 @@ program
984
1215
  console.log(` ${chalk.cyan('switchman status')}`);
985
1216
  console.log('');
986
1217
 
1218
+ const verification = collectSetupVerification(repoRoot);
1219
+ renderSetupVerification(verification, { compact: true });
1220
+
987
1221
  } catch (err) {
988
1222
  spinner.fail(err.message);
989
1223
  process.exit(1);
990
1224
  }
991
1225
  });
992
1226
 
1227
+ program
1228
+ .command('verify-setup')
1229
+ .description('Check whether this repo is ready for a smooth first Switchman run')
1230
+ .option('--json', 'Output raw JSON')
1231
+ .option('--home <path>', 'Override the home directory for editor config checks')
1232
+ .addHelpText('after', `
1233
+ Examples:
1234
+ switchman verify-setup
1235
+ switchman verify-setup --json
1236
+
1237
+ Use this after setup or whenever editor/config wiring feels off.
1238
+ `)
1239
+ .action((opts) => {
1240
+ const repoRoot = getRepo();
1241
+ const report = collectSetupVerification(repoRoot, { homeDir: opts.home || null });
1242
+
1243
+ if (opts.json) {
1244
+ console.log(JSON.stringify(report, null, 2));
1245
+ if (!report.ok) process.exitCode = 1;
1246
+ return;
1247
+ }
1248
+
1249
+ renderSetupVerification(report);
1250
+ if (!report.ok) process.exitCode = 1;
1251
+ });
1252
+
993
1253
 
994
1254
  // ── mcp ───────────────────────────────────────────────────────────────────────
995
1255
 
@@ -1001,6 +1261,11 @@ mcpCmd
1001
1261
  .option('--windsurf', 'Write Windsurf MCP config to ~/.codeium/mcp_config.json')
1002
1262
  .option('--home <path>', 'Override the home directory for config writes (useful for testing)')
1003
1263
  .option('--json', 'Output raw JSON')
1264
+ .addHelpText('after', `
1265
+ Examples:
1266
+ switchman mcp install --windsurf
1267
+ switchman mcp install --windsurf --json
1268
+ `)
1004
1269
  .action((opts) => {
1005
1270
  if (!opts.windsurf) {
1006
1271
  console.error(chalk.red('Choose an editor install target, for example `switchman mcp install --windsurf`.'));
@@ -1030,6 +1295,12 @@ mcpCmd
1030
1295
  // ── task ──────────────────────────────────────────────────────────────────────
1031
1296
 
1032
1297
  const taskCmd = program.command('task').description('Manage the task list');
1298
+ taskCmd.addHelpText('after', `
1299
+ Examples:
1300
+ switchman task add "Fix login bug" --priority 8
1301
+ switchman task list --status pending
1302
+ switchman task done task-123
1303
+ `);
1033
1304
 
1034
1305
  taskCmd
1035
1306
  .command('add <title>')
@@ -1127,10 +1398,15 @@ taskCmd
1127
1398
 
1128
1399
  taskCmd
1129
1400
  .command('next')
1130
- .description('Get and assign the next pending task (compatibility shim for lease next)')
1401
+ .description('Get the next pending task quickly (use `lease next` for the full workflow)')
1131
1402
  .option('--json', 'Output as JSON')
1132
1403
  .option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
1133
1404
  .option('--agent <name>', 'Agent identifier for logging (e.g. claude-code)')
1405
+ .addHelpText('after', `
1406
+ Examples:
1407
+ switchman task next
1408
+ switchman task next --json
1409
+ `)
1134
1410
  .action((opts) => {
1135
1411
  const repoRoot = getRepo();
1136
1412
  const worktreeName = getCurrentWorktreeName(opts.worktree);
@@ -1159,7 +1435,13 @@ taskCmd
1159
1435
 
1160
1436
  // ── queue ─────────────────────────────────────────────────────────────────────
1161
1437
 
1162
- const queueCmd = program.command('queue').description('Land finished work safely back onto main, one item at a time');
1438
+ const queueCmd = program.command('queue').alias('land').description('Land finished work safely back onto main, one item at a time');
1439
+ queueCmd.addHelpText('after', `
1440
+ Examples:
1441
+ switchman queue add --worktree agent1
1442
+ switchman queue status
1443
+ switchman queue run --watch
1444
+ `);
1163
1445
 
1164
1446
  queueCmd
1165
1447
  .command('add [branch]')
@@ -1170,6 +1452,12 @@ queueCmd
1170
1452
  .option('--max-retries <n>', 'Maximum automatic retries', '1')
1171
1453
  .option('--submitted-by <name>', 'Operator or automation name')
1172
1454
  .option('--json', 'Output raw JSON')
1455
+ .addHelpText('after', `
1456
+ Examples:
1457
+ switchman queue add feature/auth-hardening
1458
+ switchman queue add --worktree agent2
1459
+ switchman queue add --pipeline pipe-123
1460
+ `)
1173
1461
  .action((branch, opts) => {
1174
1462
  const repoRoot = getRepo();
1175
1463
  const db = getDb(repoRoot);
@@ -1179,7 +1467,7 @@ queueCmd
1179
1467
  if (opts.worktree) {
1180
1468
  const worktree = listWorktrees(db).find((entry) => entry.name === opts.worktree);
1181
1469
  if (!worktree) {
1182
- throw new Error(`Worktree ${opts.worktree} is not registered.`);
1470
+ throw new Error(`Workspace ${opts.worktree} is not registered.`);
1183
1471
  }
1184
1472
  payload = {
1185
1473
  sourceType: 'worktree',
@@ -1207,7 +1495,7 @@ queueCmd
1207
1495
  submittedBy: opts.submittedBy || null,
1208
1496
  };
1209
1497
  } else {
1210
- throw new Error('Provide a branch, --worktree, or --pipeline.');
1498
+ throw new Error('Choose one source to land: a branch name, `--worktree`, or `--pipeline`.');
1211
1499
  }
1212
1500
 
1213
1501
  const result = enqueueMergeItem(db, payload);
@@ -1223,7 +1511,7 @@ queueCmd
1223
1511
  if (result.source_worktree) console.log(` ${chalk.dim('worktree:')} ${result.source_worktree}`);
1224
1512
  } catch (err) {
1225
1513
  db.close();
1226
- console.error(chalk.red(err.message));
1514
+ printErrorWithNext(err.message, 'switchman queue add --help');
1227
1515
  process.exitCode = 1;
1228
1516
  }
1229
1517
  });
@@ -1266,6 +1554,19 @@ queueCmd
1266
1554
  .command('status')
1267
1555
  .description('Show an operator-friendly merge queue summary')
1268
1556
  .option('--json', 'Output raw JSON')
1557
+ .addHelpText('after', `
1558
+ Plain English:
1559
+ Use this when finished branches are waiting to land and you want one safe queue view.
1560
+
1561
+ Examples:
1562
+ switchman queue status
1563
+ switchman queue status --json
1564
+
1565
+ What it helps you answer:
1566
+ - what lands next
1567
+ - what is blocked
1568
+ - what command should I run now
1569
+ `)
1269
1570
  .action((opts) => {
1270
1571
  const repoRoot = getRepo();
1271
1572
  const db = getDb(repoRoot);
@@ -1281,18 +1582,77 @@ queueCmd
1281
1582
  return;
1282
1583
  }
1283
1584
 
1284
- console.log(`Queue: ${items.length} item(s)`);
1285
- console.log(` ${chalk.dim('queued')} ${summary.counts.queued} ${chalk.dim('validating')} ${summary.counts.validating} ${chalk.dim('rebasing')} ${summary.counts.rebasing} ${chalk.dim('merging')} ${summary.counts.merging} ${chalk.dim('retrying')} ${summary.counts.retrying} ${chalk.dim('blocked')} ${summary.counts.blocked} ${chalk.dim('merged')} ${summary.counts.merged}`);
1286
- if (summary.next) {
1287
- console.log(` ${chalk.dim('next:')} ${summary.next.id} ${summary.next.source_type}:${summary.next.source_ref} ${chalk.dim(`retries:${summary.next.retry_count}/${summary.next.max_retries}`)}`);
1288
- }
1289
- for (const item of summary.blocked) {
1290
- console.log(` ${statusBadge(item.status)} ${item.id} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`);
1291
- if (item.last_error_summary) console.log(` ${chalk.red('why:')} ${item.last_error_summary}`);
1292
- if (item.next_action) console.log(` ${chalk.yellow('next:')} ${item.next_action}`);
1585
+ const queueHealth = summary.counts.blocked > 0 ? 'block' : summary.counts.retrying > 0 ? 'warn' : 'healthy';
1586
+ const queueHealthColor = colorForHealth(queueHealth);
1587
+ const focus = summary.blocked[0] || summary.retrying[0] || summary.next || null;
1588
+ const focusLine = focus
1589
+ ? `${focus.id} ${focus.source_type}:${focus.source_ref}${focus.last_error_summary ? ` ${chalk.dim(`• ${focus.last_error_summary}`)}` : ''}`
1590
+ : 'Nothing waiting. Landing queue is clear.';
1591
+
1592
+ console.log('');
1593
+ console.log(queueHealthColor('='.repeat(72)));
1594
+ console.log(`${queueHealthColor(healthLabel(queueHealth))} ${chalk.bold('switchman queue status')} ${chalk.dim('• landing mission control')}`);
1595
+ console.log(queueHealthColor('='.repeat(72)));
1596
+ console.log(renderSignalStrip([
1597
+ renderChip('queued', summary.counts.queued, summary.counts.queued > 0 ? chalk.yellow : chalk.green),
1598
+ renderChip('retrying', summary.counts.retrying, summary.counts.retrying > 0 ? chalk.yellow : chalk.green),
1599
+ renderChip('blocked', summary.counts.blocked, summary.counts.blocked > 0 ? chalk.red : chalk.green),
1600
+ renderChip('merging', summary.counts.merging, summary.counts.merging > 0 ? chalk.blue : chalk.green),
1601
+ renderChip('merged', summary.counts.merged, summary.counts.merged > 0 ? chalk.green : chalk.white),
1602
+ ]));
1603
+ console.log(renderMetricRow([
1604
+ { label: 'items', value: items.length, color: chalk.white },
1605
+ { label: 'validating', value: summary.counts.validating, color: chalk.blue },
1606
+ { label: 'rebasing', value: summary.counts.rebasing, color: chalk.blue },
1607
+ { label: 'target', value: summary.next?.target_branch || 'main', color: chalk.cyan },
1608
+ ]));
1609
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
1610
+
1611
+ const queueFocusLines = summary.next
1612
+ ? [
1613
+ `${renderChip('NEXT', summary.next.id, chalk.green)} ${summary.next.source_type}:${summary.next.source_ref} ${chalk.dim(`retries:${summary.next.retry_count}/${summary.next.max_retries}`)}`,
1614
+ ` ${chalk.yellow('run:')} switchman queue run`,
1615
+ ]
1616
+ : [chalk.dim('No queued landing work right now.')];
1617
+
1618
+ const queueBlockedLines = summary.blocked.length > 0
1619
+ ? summary.blocked.slice(0, 4).flatMap((item) => {
1620
+ const lines = [`${renderChip('BLOCKED', item.id, chalk.red)} ${item.source_type}:${item.source_ref} ${chalk.dim(`retries:${item.retry_count}/${item.max_retries}`)}`];
1621
+ if (item.last_error_summary) lines.push(` ${chalk.red('why:')} ${item.last_error_summary}`);
1622
+ if (item.next_action) lines.push(` ${chalk.yellow('next:')} ${item.next_action}`);
1623
+ return lines;
1624
+ })
1625
+ : [chalk.green('Nothing blocked.')];
1626
+
1627
+ const queueWatchLines = items.filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status)).length > 0
1628
+ ? items
1629
+ .filter((item) => ['retrying', 'merging', 'rebasing', 'validating'].includes(item.status))
1630
+ .slice(0, 4)
1631
+ .flatMap((item) => {
1632
+ const lines = [`${renderChip(item.status.toUpperCase(), item.id, item.status === 'retrying' ? chalk.yellow : chalk.blue)} ${item.source_type}:${item.source_ref}`];
1633
+ if (item.last_error_summary) lines.push(` ${chalk.dim(item.last_error_summary)}`);
1634
+ return lines;
1635
+ })
1636
+ : [chalk.green('No in-flight queue items right now.')];
1637
+
1638
+ const queueCommandLines = [
1639
+ `${chalk.cyan('$')} switchman queue run`,
1640
+ `${chalk.cyan('$')} switchman queue status --json`,
1641
+ ...(summary.blocked[0] ? [`${chalk.cyan('$')} switchman queue retry ${summary.blocked[0].id}`] : []),
1642
+ ];
1643
+
1644
+ console.log('');
1645
+ for (const block of [
1646
+ renderPanel('Landing focus', queueFocusLines, chalk.green),
1647
+ renderPanel('Blocked', queueBlockedLines, summary.counts.blocked > 0 ? chalk.red : chalk.green),
1648
+ renderPanel('In flight', queueWatchLines, queueWatchLines[0] === 'No in-flight queue items right now.' ? chalk.green : chalk.blue),
1649
+ renderPanel('Next commands', queueCommandLines, chalk.cyan),
1650
+ ]) {
1651
+ for (const line of block) console.log(line);
1652
+ console.log('');
1293
1653
  }
1654
+
1294
1655
  if (recentEvents.length > 0) {
1295
- console.log('');
1296
1656
  console.log(chalk.bold('Recent Queue Events:'));
1297
1657
  for (const event of recentEvents) {
1298
1658
  console.log(` ${chalk.cyan(event.queue_item_id)} ${chalk.dim(event.event_type)} ${chalk.dim(event.status || '')} ${chalk.dim(event.created_at)}`.trim());
@@ -1302,13 +1662,19 @@ queueCmd
1302
1662
 
1303
1663
  queueCmd
1304
1664
  .command('run')
1305
- .description('Process queued merge items serially')
1665
+ .description('Process landing-queue items one at a time')
1306
1666
  .option('--max-items <n>', 'Maximum queue items to process', '1')
1307
1667
  .option('--target <branch>', 'Default target branch', 'main')
1308
1668
  .option('--watch', 'Keep polling for new queue items')
1309
1669
  .option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '1000')
1310
1670
  .option('--max-cycles <n>', 'Maximum watch cycles before exiting (mainly for tests)')
1311
1671
  .option('--json', 'Output raw JSON')
1672
+ .addHelpText('after', `
1673
+ Examples:
1674
+ switchman queue run
1675
+ switchman queue run --watch
1676
+ switchman queue run --watch --watch-interval-ms 1000
1677
+ `)
1312
1678
  .action(async (opts) => {
1313
1679
  const repoRoot = getRepo();
1314
1680
 
@@ -1379,7 +1745,7 @@ queueCmd
1379
1745
  db.close();
1380
1746
 
1381
1747
  if (!item) {
1382
- console.error(chalk.red(`Queue item ${itemId} is not retryable.`));
1748
+ printErrorWithNext(`Queue item ${itemId} is not retryable.`, 'switchman queue status');
1383
1749
  process.exitCode = 1;
1384
1750
  return;
1385
1751
  }
@@ -1402,7 +1768,7 @@ queueCmd
1402
1768
  db.close();
1403
1769
 
1404
1770
  if (!item) {
1405
- console.error(chalk.red(`Queue item ${itemId} does not exist.`));
1771
+ printErrorWithNext(`Queue item ${itemId} does not exist.`, 'switchman queue status');
1406
1772
  process.exitCode = 1;
1407
1773
  return;
1408
1774
  }
@@ -1413,6 +1779,12 @@ queueCmd
1413
1779
  // ── pipeline ──────────────────────────────────────────────────────────────────
1414
1780
 
1415
1781
  const pipelineCmd = program.command('pipeline').description('Create and summarize issue-to-PR execution pipelines');
1782
+ pipelineCmd.addHelpText('after', `
1783
+ Examples:
1784
+ switchman pipeline start "Harden auth API permissions"
1785
+ switchman pipeline exec pipe-123 "/path/to/agent-command"
1786
+ switchman pipeline status pipe-123
1787
+ `);
1416
1788
 
1417
1789
  pipelineCmd
1418
1790
  .command('start <title>')
@@ -1450,6 +1822,14 @@ pipelineCmd
1450
1822
  .command('status <pipelineId>')
1451
1823
  .description('Show task status for a pipeline')
1452
1824
  .option('--json', 'Output raw JSON')
1825
+ .addHelpText('after', `
1826
+ Plain English:
1827
+ Use this when one goal has been split into several tasks and you want to see what is running, stuck, or next.
1828
+
1829
+ Examples:
1830
+ switchman pipeline status pipe-123
1831
+ switchman pipeline status pipe-123 --json
1832
+ `)
1453
1833
  .action((pipelineId, opts) => {
1454
1834
  const repoRoot = getRepo();
1455
1835
  const db = getDb(repoRoot);
@@ -1463,20 +1843,74 @@ pipelineCmd
1463
1843
  return;
1464
1844
  }
1465
1845
 
1846
+ const pipelineHealth = result.status === 'blocked'
1847
+ ? 'block'
1848
+ : result.counts.failed > 0
1849
+ ? 'warn'
1850
+ : result.counts.in_progress > 0
1851
+ ? 'warn'
1852
+ : 'healthy';
1853
+ const pipelineHealthColor = colorForHealth(pipelineHealth);
1854
+ const failedTask = result.tasks.find((task) => task.status === 'failed');
1855
+ const runningTask = result.tasks.find((task) => task.status === 'in_progress');
1856
+ const nextPendingTask = result.tasks.find((task) => task.status === 'pending');
1857
+ const focusTask = failedTask || runningTask || nextPendingTask || result.tasks[0] || null;
1858
+ const focusLine = focusTask
1859
+ ? `${focusTask.title} ${chalk.dim(focusTask.id)}`
1860
+ : 'No pipeline tasks found.';
1861
+
1862
+ console.log('');
1863
+ console.log(pipelineHealthColor('='.repeat(72)));
1864
+ console.log(`${pipelineHealthColor(healthLabel(pipelineHealth))} ${chalk.bold('switchman pipeline status')} ${chalk.dim('• pipeline mission control')}`);
1466
1865
  console.log(`${chalk.bold(result.title)} ${chalk.dim(result.pipeline_id)}`);
1467
- console.log(` ${chalk.dim('done')} ${result.counts.done} ${chalk.dim('in_progress')} ${result.counts.in_progress} ${chalk.dim('pending')} ${result.counts.pending} ${chalk.dim('failed')} ${result.counts.failed}`);
1468
- for (const task of result.tasks) {
1866
+ console.log(pipelineHealthColor('='.repeat(72)));
1867
+ console.log(renderSignalStrip([
1868
+ renderChip('done', result.counts.done, result.counts.done > 0 ? chalk.green : chalk.white),
1869
+ renderChip('running', result.counts.in_progress, result.counts.in_progress > 0 ? chalk.blue : chalk.green),
1870
+ renderChip('pending', result.counts.pending, result.counts.pending > 0 ? chalk.yellow : chalk.green),
1871
+ renderChip('failed', result.counts.failed, result.counts.failed > 0 ? chalk.red : chalk.green),
1872
+ ]));
1873
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
1874
+
1875
+ const runningLines = result.tasks.filter((task) => task.status === 'in_progress').slice(0, 4).map((task) => {
1469
1876
  const worktree = task.worktree || task.suggested_worktree || 'unassigned';
1470
1877
  const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
1471
1878
  const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
1472
- console.log(` ${statusBadge(task.status)} ${task.id} ${task.title}${type} ${chalk.dim(worktree)}${blocked}`);
1879
+ return `${chalk.cyan(worktree)} -> ${task.title}${type} ${chalk.dim(task.id)}${blocked}`;
1880
+ });
1881
+
1882
+ const blockedLines = result.tasks.filter((task) => task.status === 'failed').slice(0, 4).flatMap((task) => {
1883
+ const type = task.task_spec?.task_type ? ` ${chalk.dim(`[${task.task_spec.task_type}]`)}` : '';
1884
+ const lines = [`${renderChip('BLOCKED', task.id, chalk.red)} ${task.title}${type}`];
1473
1885
  if (task.failure?.summary) {
1474
1886
  const reasonLabel = humanizeReasonCode(task.failure.reason_code);
1475
- console.log(` ${chalk.red('why:')} ${task.failure.summary} ${chalk.dim(`(${reasonLabel})`)}`);
1476
- }
1477
- if (task.next_action) {
1478
- console.log(` ${chalk.yellow('next:')} ${task.next_action}`);
1887
+ lines.push(` ${chalk.red('why:')} ${task.failure.summary} ${chalk.dim(`(${reasonLabel})`)}`);
1479
1888
  }
1889
+ if (task.next_action) lines.push(` ${chalk.yellow('next:')} ${task.next_action}`);
1890
+ return lines;
1891
+ });
1892
+
1893
+ const nextLines = result.tasks.filter((task) => task.status === 'pending').slice(0, 4).map((task) => {
1894
+ const worktree = task.suggested_worktree || task.worktree || 'unassigned';
1895
+ const blocked = task.blocked_by?.length ? ` ${chalk.dim(`blocked by ${task.blocked_by.join(', ')}`)}` : '';
1896
+ return `${renderChip('NEXT', task.id, chalk.green)} ${task.title} ${chalk.dim(worktree)}${blocked}`;
1897
+ });
1898
+
1899
+ const commandLines = [
1900
+ `${chalk.cyan('$')} switchman pipeline exec ${result.pipeline_id} "/path/to/agent-command"`,
1901
+ `${chalk.cyan('$')} switchman pipeline pr ${result.pipeline_id}`,
1902
+ ...(result.counts.failed > 0 ? [`${chalk.cyan('$')} switchman pipeline status ${result.pipeline_id}`] : []),
1903
+ ];
1904
+
1905
+ console.log('');
1906
+ for (const block of [
1907
+ renderPanel('Running now', runningLines.length > 0 ? runningLines : [chalk.dim('No tasks are actively running.')], runningLines.length > 0 ? chalk.cyan : chalk.green),
1908
+ renderPanel('Blocked', blockedLines.length > 0 ? blockedLines : [chalk.green('Nothing blocked.')], blockedLines.length > 0 ? chalk.red : chalk.green),
1909
+ renderPanel('Next up', nextLines.length > 0 ? nextLines : [chalk.dim('No pending tasks left.')], chalk.green),
1910
+ renderPanel('Next commands', commandLines, chalk.cyan),
1911
+ ]) {
1912
+ for (const line of block) console.log(line);
1913
+ console.log('');
1480
1914
  }
1481
1915
  } catch (err) {
1482
1916
  db.close();
@@ -1666,6 +2100,14 @@ pipelineCmd
1666
2100
  .option('--retry-backoff-ms <ms>', 'Base backoff in milliseconds between retry attempts', '0')
1667
2101
  .option('--timeout-ms <ms>', 'Default command timeout in milliseconds when a task spec does not provide one', '0')
1668
2102
  .option('--json', 'Output raw JSON')
2103
+ .addHelpText('after', `
2104
+ Plain English:
2105
+ pipeline = one goal, broken into smaller safe tasks
2106
+
2107
+ Examples:
2108
+ switchman pipeline exec pipe-123 "/path/to/agent-command"
2109
+ switchman pipeline exec pipe-123 "npm test"
2110
+ `)
1669
2111
  .action(async (pipelineId, agentCommand, opts) => {
1670
2112
  const repoRoot = getRepo();
1671
2113
  const db = getDb(repoRoot);
@@ -1699,20 +2141,34 @@ pipelineCmd
1699
2141
  console.log(chalk.dim(result.pr.markdown.split('\n')[0]));
1700
2142
  } catch (err) {
1701
2143
  db.close();
1702
- console.error(chalk.red(err.message));
2144
+ printErrorWithNext(err.message, `switchman pipeline status ${pipelineId}`);
1703
2145
  process.exitCode = 1;
1704
2146
  }
1705
2147
  });
1706
2148
 
1707
2149
  // ── lease ────────────────────────────────────────────────────────────────────
1708
2150
 
1709
- const leaseCmd = program.command('lease').description('Manage active work leases');
2151
+ const leaseCmd = program.command('lease').alias('session').description('Manage active work sessions and keep long-running tasks alive');
2152
+ leaseCmd.addHelpText('after', `
2153
+ Plain English:
2154
+ lease = a task currently checked out by an agent
2155
+
2156
+ Examples:
2157
+ switchman lease next --json
2158
+ switchman lease heartbeat lease-123
2159
+ switchman lease reap
2160
+ `);
1710
2161
 
1711
2162
  leaseCmd
1712
2163
  .command('acquire <taskId> <worktree>')
1713
- .description('Acquire a lease for a pending task')
2164
+ .description('Start a tracked work session for a specific pending task')
1714
2165
  .option('--agent <name>', 'Agent identifier for logging')
1715
2166
  .option('--json', 'Output as JSON')
2167
+ .addHelpText('after', `
2168
+ Examples:
2169
+ switchman lease acquire task-123 agent2
2170
+ switchman lease acquire task-123 agent2 --agent cursor
2171
+ `)
1716
2172
  .action((taskId, worktree, opts) => {
1717
2173
  const repoRoot = getRepo();
1718
2174
  const db = getDb(repoRoot);
@@ -1722,7 +2178,7 @@ leaseCmd
1722
2178
 
1723
2179
  if (!lease || !task) {
1724
2180
  if (opts.json) console.log(JSON.stringify({ lease: null, task: null }));
1725
- else console.log(chalk.red(`Could not acquire lease. The task may not exist or is not pending.`));
2181
+ else printErrorWithNext('Could not start a work session. The task may not exist or may already be in progress.', 'switchman task list --status pending');
1726
2182
  process.exitCode = 1;
1727
2183
  return;
1728
2184
  }
@@ -1742,10 +2198,16 @@ leaseCmd
1742
2198
 
1743
2199
  leaseCmd
1744
2200
  .command('next')
1745
- .description('Claim the next pending task and acquire its lease')
2201
+ .description('Start the next pending task and open a tracked work session for it')
1746
2202
  .option('--json', 'Output as JSON')
1747
- .option('--worktree <name>', 'Worktree to assign the task to (defaults to current worktree name)')
2203
+ .option('--worktree <name>', 'Workspace to assign the task to (defaults to the current folder name)')
1748
2204
  .option('--agent <name>', 'Agent identifier for logging')
2205
+ .addHelpText('after', `
2206
+ Examples:
2207
+ switchman lease next
2208
+ switchman lease next --json
2209
+ switchman lease next --worktree agent2 --agent cursor
2210
+ `)
1749
2211
  .action((opts) => {
1750
2212
  const repoRoot = getRepo();
1751
2213
  const worktreeName = getCurrentWorktreeName(opts.worktree);
@@ -1753,7 +2215,7 @@ leaseCmd
1753
2215
 
1754
2216
  if (!task) {
1755
2217
  if (opts.json) console.log(JSON.stringify({ task: null, lease: null }));
1756
- else if (exhausted) console.log(chalk.dim('No pending tasks.'));
2218
+ else if (exhausted) console.log(chalk.dim('No pending tasks. Add one with `switchman task add "Your task"`.'));
1757
2219
  else console.log(chalk.yellow('Tasks were claimed by other agents during assignment. Run again to get the next one.'));
1758
2220
  return;
1759
2221
  }
@@ -1816,7 +2278,7 @@ leaseCmd
1816
2278
 
1817
2279
  if (!lease) {
1818
2280
  if (opts.json) console.log(JSON.stringify({ lease: null }));
1819
- else console.log(chalk.red(`No active lease found for ${leaseId}`));
2281
+ else printErrorWithNext(`No active work session found for ${leaseId}.`, 'switchman lease list --status active');
1820
2282
  process.exitCode = 1;
1821
2283
  return;
1822
2284
  }
@@ -1832,9 +2294,14 @@ leaseCmd
1832
2294
 
1833
2295
  leaseCmd
1834
2296
  .command('reap')
1835
- .description('Expire stale leases, release their claims, and return their tasks to pending')
2297
+ .description('Clean up abandoned work sessions and release their file locks')
1836
2298
  .option('--stale-after-minutes <minutes>', 'Age threshold for staleness')
1837
2299
  .option('--json', 'Output as JSON')
2300
+ .addHelpText('after', `
2301
+ Examples:
2302
+ switchman lease reap
2303
+ switchman lease reap --stale-after-minutes 20
2304
+ `)
1838
2305
  .action((opts) => {
1839
2306
  const repoRoot = getRepo();
1840
2307
  const db = getDb(repoRoot);
@@ -1919,11 +2386,20 @@ leasePolicyCmd
1919
2386
 
1920
2387
  // ── worktree ───────────────────────────────────────────────────────────────────
1921
2388
 
1922
- const wtCmd = program.command('worktree').description('Manage worktrees');
2389
+ const wtCmd = program.command('worktree').alias('workspace').description('Manage registered workspaces (Git worktrees)');
2390
+ wtCmd.addHelpText('after', `
2391
+ Plain English:
2392
+ worktree = the Git feature behind each agent workspace
2393
+
2394
+ Examples:
2395
+ switchman worktree list
2396
+ switchman workspace list
2397
+ switchman worktree sync
2398
+ `);
1923
2399
 
1924
2400
  wtCmd
1925
2401
  .command('add <name> <path> <branch>')
1926
- .description('Register a worktree with switchman')
2402
+ .description('Register a workspace with Switchman')
1927
2403
  .option('--agent <name>', 'Agent assigned to this worktree')
1928
2404
  .action((name, path, branch, opts) => {
1929
2405
  const repoRoot = getRepo();
@@ -1935,7 +2411,7 @@ wtCmd
1935
2411
 
1936
2412
  wtCmd
1937
2413
  .command('list')
1938
- .description('List all registered worktrees')
2414
+ .description('List all registered workspaces')
1939
2415
  .action(() => {
1940
2416
  const repoRoot = getRepo();
1941
2417
  const db = getDb(repoRoot);
@@ -1944,7 +2420,7 @@ wtCmd
1944
2420
  db.close();
1945
2421
 
1946
2422
  if (!worktrees.length && !gitWorktrees.length) {
1947
- console.log(chalk.dim('No worktrees found.'));
2423
+ console.log(chalk.dim('No workspaces found. Run `switchman setup --agents 3` or `switchman worktree sync`.'));
1948
2424
  return;
1949
2425
  }
1950
2426
 
@@ -1964,7 +2440,7 @@ wtCmd
1964
2440
 
1965
2441
  wtCmd
1966
2442
  .command('sync')
1967
- .description('Sync git worktrees into the switchman database')
2443
+ .description('Sync Git workspaces into the Switchman database')
1968
2444
  .action(() => {
1969
2445
  const repoRoot = getRepo();
1970
2446
  const db = getDb(repoRoot);
@@ -1981,12 +2457,20 @@ wtCmd
1981
2457
 
1982
2458
  program
1983
2459
  .command('claim <taskId> <worktree> [files...]')
1984
- .description('Claim files for a task (warns if conflicts exist)')
2460
+ .description('Lock files for a task before editing')
1985
2461
  .option('--agent <name>', 'Agent name')
1986
2462
  .option('--force', 'Claim even if conflicts exist')
2463
+ .addHelpText('after', `
2464
+ Examples:
2465
+ switchman claim task-123 agent2 src/auth.js src/server.js
2466
+ switchman claim task-123 agent2 src/auth.js --agent cursor
2467
+
2468
+ Use this before editing files in a shared repo.
2469
+ `)
1987
2470
  .action((taskId, worktree, files, opts) => {
1988
2471
  if (!files.length) {
1989
- console.log(chalk.yellow('No files specified. Use: switchman claim <taskId> <worktree> file1 file2 ...'));
2472
+ console.log(chalk.yellow('No files specified.'));
2473
+ console.log(`${chalk.yellow('next:')} switchman claim <taskId> <workspace> file1 file2`);
1990
2474
  return;
1991
2475
  }
1992
2476
  const repoRoot = getRepo();
@@ -2000,7 +2484,8 @@ program
2000
2484
  for (const c of conflicts) {
2001
2485
  console.log(` ${chalk.yellow(c.file)} → already claimed by worktree ${chalk.cyan(c.claimedBy.worktree)} (task: ${c.claimedBy.task_title})`);
2002
2486
  }
2003
- console.log(chalk.dim('\nUse --force to claim anyway, or resolve conflicts first.'));
2487
+ console.log(chalk.dim('\nUse --force to claim anyway, or pick different files first.'));
2488
+ console.log(`${chalk.yellow('next:')} switchman status`);
2004
2489
  process.exitCode = 1;
2005
2490
  return;
2006
2491
  }
@@ -2009,7 +2494,7 @@ program
2009
2494
  console.log(`${chalk.green('✓')} Claimed ${files.length} file(s) for task ${chalk.cyan(taskId)} (${chalk.dim(lease.id)})`);
2010
2495
  files.forEach(f => console.log(` ${chalk.dim(f)}`));
2011
2496
  } catch (err) {
2012
- console.error(chalk.red(err.message));
2497
+ printErrorWithNext(err.message, 'switchman task list --status in_progress');
2013
2498
  process.exitCode = 1;
2014
2499
  } finally {
2015
2500
  db.close();
@@ -2181,6 +2666,12 @@ program
2181
2666
  .description('Scan all workspaces for conflicts')
2182
2667
  .option('--json', 'Output raw JSON')
2183
2668
  .option('--quiet', 'Only show conflicts')
2669
+ .addHelpText('after', `
2670
+ Examples:
2671
+ switchman scan
2672
+ switchman scan --quiet
2673
+ switchman scan --json
2674
+ `)
2184
2675
  .action(async (opts) => {
2185
2676
  const repoRoot = getRepo();
2186
2677
  const db = getDb(repoRoot);
@@ -2296,12 +2787,21 @@ program
2296
2787
  .option('--watch', 'Keep refreshing status in the terminal')
2297
2788
  .option('--watch-interval-ms <n>', 'Polling interval for --watch mode', '2000')
2298
2789
  .option('--max-cycles <n>', 'Maximum refresh cycles before exiting', '0')
2790
+ .addHelpText('after', `
2791
+ Examples:
2792
+ switchman status
2793
+ switchman status --watch
2794
+ switchman status --json
2795
+
2796
+ Use this first when the repo feels stuck.
2797
+ `)
2299
2798
  .action(async (opts) => {
2300
2799
  const repoRoot = getRepo();
2301
2800
  const watch = Boolean(opts.watch);
2302
2801
  const watchIntervalMs = Math.max(100, Number.parseInt(opts.watchIntervalMs, 10) || 2000);
2303
2802
  const maxCycles = Math.max(0, Number.parseInt(opts.maxCycles, 10) || 0);
2304
2803
  let cycles = 0;
2804
+ let lastSignature = null;
2305
2805
 
2306
2806
  while (true) {
2307
2807
  if (watch && process.stdout.isTTY && !opts.json) {
@@ -2316,8 +2816,16 @@ program
2316
2816
  } else {
2317
2817
  renderUnifiedStatusReport(report);
2318
2818
  if (watch) {
2819
+ const signature = buildWatchSignature(report);
2820
+ const watchState = lastSignature === null
2821
+ ? chalk.cyan('baseline snapshot')
2822
+ : signature === lastSignature
2823
+ ? chalk.dim('no repo changes since last refresh')
2824
+ : chalk.green('change detected');
2825
+ const updatedAt = formatClockTime(report.generated_at);
2826
+ lastSignature = signature;
2319
2827
  console.log('');
2320
- console.log(chalk.dim(`Watching every ${watchIntervalMs}ms${maxCycles > 0 ? ` • cycle ${cycles}/${maxCycles}` : ''}`));
2828
+ console.log(chalk.dim(`Live watch • updated ${updatedAt || 'just now'}${watchState}${maxCycles > 0 ? ` • cycle ${cycles}/${maxCycles}` : ''} • refresh ${watchIntervalMs}ms`));
2321
2829
  }
2322
2830
  }
2323
2831
 
@@ -2332,6 +2840,14 @@ program
2332
2840
  .command('doctor')
2333
2841
  .description('Show one operator-focused health view: what is running, what is blocked, and what to do next')
2334
2842
  .option('--json', 'Output raw JSON')
2843
+ .addHelpText('after', `
2844
+ Plain English:
2845
+ Use this when the repo feels risky, noisy, or stuck and you want the health summary plus exact next moves.
2846
+
2847
+ Examples:
2848
+ switchman doctor
2849
+ switchman doctor --json
2850
+ `)
2335
2851
  .action(async (opts) => {
2336
2852
  const repoRoot = getRepo();
2337
2853
  const db = getDb(repoRoot);
@@ -2356,66 +2872,83 @@ program
2356
2872
  return;
2357
2873
  }
2358
2874
 
2359
- const badge = report.health === 'healthy'
2360
- ? chalk.green('HEALTHY')
2361
- : report.health === 'warn'
2362
- ? chalk.yellow('ATTENTION')
2363
- : chalk.red('BLOCKED');
2364
- console.log(`${badge} ${report.summary}`);
2365
- console.log(chalk.dim(repoRoot));
2366
- console.log('');
2875
+ const doctorColor = colorForHealth(report.health);
2876
+ const blockedCount = report.attention.filter((item) => item.severity === 'block').length;
2877
+ const warningCount = report.attention.filter((item) => item.severity !== 'block').length;
2878
+ const focusItem = report.attention[0] || report.active_work[0] || null;
2879
+ const focusLine = focusItem
2880
+ ? `${focusItem.title || focusItem.task_title}${focusItem.detail ? ` ${chalk.dim(`• ${focusItem.detail}`)}` : ''}`
2881
+ : 'Nothing urgent. Repo health looks steady.';
2367
2882
 
2368
- console.log(chalk.bold('At a glance:'));
2369
- console.log(` ${chalk.dim('tasks')} ${report.counts.pending} pending, ${report.counts.in_progress} in progress, ${report.counts.done} done, ${report.counts.failed} failed`);
2370
- console.log(` ${chalk.dim('leases')} ${report.counts.active_leases} active, ${report.counts.stale_leases} stale`);
2371
- console.log(` ${chalk.dim('merge')} CI ${report.merge_readiness.ci_gate_ok ? chalk.green('clear') : chalk.red('blocked')} AI ${report.merge_readiness.ai_gate_status}`);
2372
-
2373
- if (report.active_work.length > 0) {
2374
- console.log('');
2375
- console.log(chalk.bold('Running now:'));
2376
- for (const item of report.active_work.slice(0, 5)) {
2883
+ console.log('');
2884
+ console.log(doctorColor('='.repeat(72)));
2885
+ console.log(`${doctorColor(healthLabel(report.health))} ${chalk.bold('switchman doctor')} ${chalk.dim('• repo health mission control')}`);
2886
+ console.log(chalk.dim(repoRoot));
2887
+ console.log(chalk.dim(report.summary));
2888
+ console.log(doctorColor('='.repeat(72)));
2889
+ console.log(renderSignalStrip([
2890
+ renderChip('blocked', blockedCount, blockedCount > 0 ? chalk.red : chalk.green),
2891
+ renderChip('watch', warningCount, warningCount > 0 ? chalk.yellow : chalk.green),
2892
+ renderChip('leases', report.counts.active_leases, report.counts.active_leases > 0 ? chalk.blue : chalk.green),
2893
+ renderChip('stale', report.counts.stale_leases, report.counts.stale_leases > 0 ? chalk.red : chalk.green),
2894
+ renderChip('merge', report.merge_readiness.ci_gate_ok ? 'clear' : 'hold', report.merge_readiness.ci_gate_ok ? chalk.green : chalk.red),
2895
+ ]));
2896
+ console.log(renderMetricRow([
2897
+ { label: 'tasks', value: `${report.counts.pending}/${report.counts.in_progress}/${report.counts.done}/${report.counts.failed}`, color: chalk.white },
2898
+ { label: 'AI gate', value: report.merge_readiness.ai_gate_status, color: report.merge_readiness.ai_gate_status === 'blocked' ? chalk.red : report.merge_readiness.ai_gate_status === 'warn' ? chalk.yellow : chalk.green },
2899
+ ]));
2900
+ console.log(`${chalk.bold('Focus now:')} ${focusLine}`);
2901
+
2902
+ const runningLines = report.active_work.length > 0
2903
+ ? report.active_work.slice(0, 5).map((item) => {
2377
2904
  const leaseId = activeLeases.find((lease) => lease.task_id === item.task_id && lease.worktree === item.worktree)?.id || null;
2378
2905
  const boundary = item.boundary_validation
2379
- ? ` ${chalk.dim(`validation:${item.boundary_validation.status}`)}`
2906
+ ? ` ${renderChip('validation', item.boundary_validation.status, item.boundary_validation.status === 'accepted' ? chalk.green : chalk.yellow)}`
2380
2907
  : '';
2381
2908
  const stale = (item.dependency_invalidations?.length || 0) > 0
2382
- ? ` ${chalk.dim(`stale:${item.dependency_invalidations.length}`)}`
2909
+ ? ` ${renderChip('stale', item.dependency_invalidations.length, chalk.yellow)}`
2383
2910
  : '';
2384
- console.log(` ${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${leaseId ? ` ${chalk.dim(`lease:${leaseId}`)}` : ''}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`);
2385
- }
2386
- }
2911
+ return `${chalk.cyan(item.worktree)} -> ${item.task_title} ${chalk.dim(item.task_id)}${leaseId ? ` ${chalk.dim(`lease:${leaseId}`)}` : ''}${item.scope_summary ? ` ${chalk.dim(item.scope_summary)}` : ''}${boundary}${stale}`;
2912
+ })
2913
+ : [chalk.dim('Nothing active right now.')];
2914
+
2915
+ const attentionLines = report.attention.length > 0
2916
+ ? report.attention.slice(0, 6).flatMap((item) => {
2917
+ const lines = [`${item.severity === 'block' ? renderChip('BLOCKED', item.kind || 'item', chalk.red) : renderChip('WATCH', item.kind || 'item', chalk.yellow)} ${item.title}`];
2918
+ if (item.detail) lines.push(` ${chalk.dim(item.detail)}`);
2919
+ lines.push(` ${chalk.yellow('next:')} ${item.next_step}`);
2920
+ if (item.command) lines.push(` ${chalk.cyan('run:')} ${item.command}`);
2921
+ return lines;
2922
+ })
2923
+ : [chalk.green('Nothing urgent.')];
2924
+
2925
+ const nextStepLines = [
2926
+ ...report.next_steps.slice(0, 4).map((step) => `- ${step}`),
2927
+ '',
2928
+ ...report.suggested_commands.slice(0, 4).map((command) => `${chalk.cyan('$')} ${command}`),
2929
+ ];
2387
2930
 
2388
2931
  console.log('');
2389
2932
  console.log(chalk.bold('Attention now:'));
2390
- if (report.attention.length === 0) {
2391
- console.log(` ${chalk.green('Nothing urgent.')}`);
2392
- } else {
2393
- for (const item of report.attention.slice(0, 6)) {
2394
- const itemBadge = item.severity === 'block' ? chalk.red('block') : chalk.yellow('warn ');
2395
- console.log(` ${itemBadge} ${item.title}`);
2396
- if (item.detail) console.log(` ${chalk.dim(item.detail)}`);
2397
- console.log(` ${chalk.yellow('next:')} ${item.next_step}`);
2398
- if (item.command) console.log(` ${chalk.cyan('run:')} ${item.command}`);
2399
- }
2400
- }
2401
-
2402
- console.log('');
2403
- console.log(chalk.bold('Recommended next steps:'));
2404
- for (const step of report.next_steps) {
2405
- console.log(` - ${step}`);
2406
- }
2407
- if (report.suggested_commands.length > 0) {
2933
+ for (const block of [
2934
+ renderPanel('Running now', runningLines, chalk.cyan),
2935
+ renderPanel('Attention now', attentionLines, report.attention.some((item) => item.severity === 'block') ? chalk.red : report.attention.length > 0 ? chalk.yellow : chalk.green),
2936
+ renderPanel('Recommended next steps', nextStepLines, chalk.green),
2937
+ ]) {
2938
+ for (const line of block) console.log(line);
2408
2939
  console.log('');
2409
- console.log(chalk.bold('Suggested commands:'));
2410
- for (const command of report.suggested_commands) {
2411
- console.log(` ${chalk.cyan(command)}`);
2412
- }
2413
2940
  }
2414
2941
  });
2415
2942
 
2416
2943
  // ── gate ─────────────────────────────────────────────────────────────────────
2417
2944
 
2418
2945
  const gateCmd = program.command('gate').description('Safety checks for edits, merges, and CI');
2946
+ gateCmd.addHelpText('after', `
2947
+ Examples:
2948
+ switchman gate ci
2949
+ switchman gate ai
2950
+ switchman gate install-ci
2951
+ `);
2419
2952
 
2420
2953
  const auditCmd = program.command('audit').description('Inspect and verify the tamper-evident audit trail');
2421
2954